Red Hatでソリューションアーキテクトをしている田中司恩(@tnk4on)です。
Red Hatの製品にはミドルウェア製品が多数あるのですが、数も種類も多くて細かい機能までは把握しきれていません。 個人の観点ではこれまでIT業界の仕事はインフラ畑でやってきたのでミドルウェアについて深く触れることもありませんでした。 そこで、Podmanとコンテナの力を借りて、Red Hatのミドルウェアと仲良くなろうというのがこの企画の趣旨です。 今回はOpenJDKについて深堀していきたいと思います。
(追記:続きの記事を書きました。合わせてお読みください) rheb.hatenablog.com
-目次-
- OpenJDKとは
- Red Hatが提供するOpenJDKのコンテナイメージ
- OpenJDKのコンテナイメージの入手
- OpenJDKのコンテナイメージでjarファイルが実行される仕組み
- OpenJDKのコンテナイメージでjarファイルを実行する
- まとめ
- 参考:/opt/jboss/container/java/run/run-java.sh
OpenJDKとは
OpenJDKについてはRed HatのOpenJDKの製品*1ドキュメントに下記のように記載されています。
- Open Java Development Kit (OpenJDK) は、Java Platform Standard Edition (Java SE) のオープンソース実装です。Red Hat build of OpenJDK は、8u、11u、17u、21u の 4 つのバージョンで利用できます。
- Red Hat build of OpenJDK 向けパッケージは、Red Hat Enterprise Linux および Microsoft Windows で利用でき、Red Hat Ecosystem Catalog の JDK および JRE として同梱されています。
Red Hatが製品として提供しているOpenJDKは「Red Hat build of OpenJDK」が正式名称です。本記事執筆時点では「21.0.3」が最新です。
Red Hatが提供するOpenJDKのコンテナイメージ
Red Hatが提供するOpenJDKのコンテナイメージにはいくつか種類があります。
Ecosystem Catalogを検索していくつか出てきますが、関連度で上位に上がってくるのはUBIベースのものです。

https://catalog.redhat.com/search?gs&q=openjdk&searchType=containers
このUBIベースのOpenJDKイメージは、ベースイメージのUBIと同じくUBI EULAに基づいて無償で使えます。ただし、このOpenJDKのイメージをベースに作成した新たなイメージを再配布する場合は、UBIと同じライセンス上の制限がある点はご注意ください。
詳細はUBIのFAQを参照
PodmanやSkopeoを使ってコンテナイメージ中に設定されているライセンス条項を確認できます。 (Podmanについては後述)
$ skopeo inspect docker://registry.access.redhat.com/ubi9/openjdk-21 --format '{{ index .Labels "com.redhat.license_terms" }}'
https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI
エコシステムカタログを検索するとOpenJDKのコンテナイメージにはいろいろバリエーションがありますが、完全に無償で使えるのはregistry.access.redhat.comでホストされているものです。(例:registry.access.redhat.com/ubi9/openjdk-21)
registry.redhat.io/openjdkの使用には適格なサブスクリプションが必要です(コンテナレジストリへのログインが必要)
OpenJDKのコンテナイメージの入手
まずはOpenJDKのコンテナイメージを入手します。registry.access.redhat.comはログイン不要でコンテナイメージをpullできます。ここでは最新のopenjdk-21を利用します。
$ podman pull registry.access.redhat.com/ubi9/openjdk-21 $ podman images openjdk-21:latest REPOSITORY TAG IMAGE ID CREATED SIZE registry.access.redhat.com/ubi9/openjdk-21 latest 985daf3b31fb 10 days ago 422 MB
イメージのレイヤーを確認します。
$ podman image tree openjdk-21:latest Image ID: 985daf3b31fb Tags: [registry.access.redhat.com/ubi9/openjdk-21:latest] Size: 422.4MB Image Layers ├── ID: 256cf40975c5 Size: 104.3MB └── ID: 25d8d0ec0c6b Size: 318.1MB Top Layer of: [registry.access.redhat.com/ubi9/openjdk-21:latest]
podman inspect(podman image inspect)でイメージの詳細を確認します。ポイントだけピックアップします。
$ podman inspect openjdk-21:latest
...
"Config": {
"User": "185",
...
"Cmd": [
"/usr/local/s2i/run"
],
"WorkingDir": "/home/default",
...
- User:UID=185でユーザーが作成されている
- Cmd:コンテナのデフォルトコマンドは
/usr/local/s2i/runが実行される - WorkingDir:
/home/defaultがデフォルトの作業ディレクトリ
ライセンス条項を確認するには下記のようにフィルタすることで確認できます。
$ podman inspect registry.access.redhat.com/ubi9/openjdk-21 --format '{{ index .Config.Labels "com.redhat.license_terms" }}'
https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI
コンテナイメージの履歴を確認します(出力が多いので一部のみ表示)。
$ podman history openjdk-21:latest ID CREATED CREATED BY SIZE COMMENT 985daf3b31fb 10 days ago /bin/sh -c #(nop) USER 185 318MB FROM registry.access.redhat.com/ubi9/ubi-minimal@sha256:ef6fb6b3b38ef6c85daebeabebc7ff3151b9dd1500056e6abc9c3295e4b78a51 <missing> 10 days ago /bin/sh -c mv -fZ /tmp/ubi.repo /etc/yum.r... 0B <missing> 10 days ago /bin/sh -c #(nop) USER root 0B <missing> 10 days ago /bin/sh -c #(nop) USER 185 0B <missing> 10 days ago /bin/sh -c rm -f /tmp/tls-ca-bundle.pem 0B <missing> 10 days ago /bin/sh -c rm -f '/etc/yum.repos.d/odcs-31... 0B ...
コメントにFROM registry.access.redhat.com/ubi9/ubi-minimalとあり、このOpenJDKのコンテナイメージはUBI-minimalをベースとして構築されていることがわかります。
OpenJDKのコンテナイメージでjarファイルが実行される仕組み
OpenJDKの製品ドキュメント*2には下記のような記載があります。
Red Hat build of OpenJDK イメージには、アプリケーション JAR ファイルを自動的に検出し、Java を起動するデフォルトの起動スクリプトがあります。 OpenJDK イメージの /deployments ディレクトリーの Java アプリケーションは、イメージの読み込み時に実行されます。
これがどのように実行されるのか調べてみます。まず、コンテナの実行コマンドファイル/usr/local/s2i/runを調べます。
podman image mountコマンドを使いコンテナイメージをファイルシステムにマウントしてファイルの中身を確認します。
ルートレスで実行している場合はユーザーネームスペースの中でpodman image mountを実行する必要があるので、podman unshareを実行します。
(macOSやWindowsで実行している場合はPodman machineにログインしてpodman unshareを実行する必要があります)
$ podman unshare
# mnt=$(podman image mount openjdk-21)
# cat $mnt/usr/local/s2i/run
#!/bin/bash
# Command line arguments given to this script
args="$*"
source "${JBOSS_CONTAINER_UTIL_LOGGING_MODULE}/logging.sh"
source "${JBOSS_CONTAINER_S2I_CORE_MODULE}/s2i-core"
# include our s2i_core_*() overrides/extensions
source "${JBOSS_CONTAINER_JAVA_S2I_MODULE}/s2i-core-hooks"
# Global S2I variable setup
s2i_core_env_init
export JAVA_OPTS
if [ -f "${S2I_TARGET_DEPLOYMENTS_DIR}/bin/run.sh" ]; then
echo "Starting the application using the bundled ${S2I_TARGET_DEPLOYMENTS_DIR}/bin/run.sh ..."
exec ${DEPLOYMENTS_DIR}/bin/run.sh $args ${JAVA_ARGS}
else
echo "Starting the Java application using ${JBOSS_CONTAINER_JAVA_RUN_MODULE}/run-java.sh $args..."
exec "${JBOSS_CONTAINER_JAVA_RUN_MODULE}/run-java.sh" $args ${JAVA_ARGS}
fi
/usr/local/s2i/runファイルをざっくり眺めると、${S2I_TARGET_DEPLOYMENTS_DIR}/bin/run.shファイルの有無で実行モードがS2IモードになるかJavaアプリの実行モードになるかの選択があるようです。つまり、このOpenJDKのコンテナイメージはS2Iのビルド用途にもJavaアプリの実行用途にもどちらでも使える共通イメージとして提供されているようです。
今回はS2Iについては深入りしないのでJavaアプリの実行用途についてもう少し探っていきます。
run-java.shがどこにあるか探します。
# cd $mnt # find . -name run-java.sh ./opt/jboss/container/java/run/run-java.sh
/opt/jboss/container/java/run/run-java.shにファイルがあるようです。run-java.shファイルの中身を確認します。
# cd ~ # cat $mnt/opt/jboss/container/java/run/run-java.sh
ファイルの行数が多いので全文は本記事の最後に載せておきます。
まず、確認する点がこちら。JAVA_APP_DIR環境変数に/deploymentsがセットされています。
# Default the application dir to the S2I deployment dir if [ -z "$JAVA_APP_DIR" ] then JAVA_APP_DIR=/deployments fi
製品ドキュメントに書いてあった/deploymentsのJavaアプリケーションが読み込まれるというのはここの設定のようです。
/opt/jboss/container/java/run/run-java.shの中でjarファイルの検出が行われ、最終的には${args}にファイルがセットされて実行される仕組みです。
# Start JVM
startup() {
...
exec -a "${procname}" java $(get_java_options) -cp "$(get_classpath)" ${args} $*
}
OpenJDKのコンテナイメージを引数なしで実行するとエラーメッセージの内容でここまでに確認したことがなんとなくわかります。
$ podman run --rm openjdk-21:latest Starting the Java application using /opt/jboss/container/java/run/run-java.sh ... ERROR Neither $JAVA_MAIN_CLASS nor $JAVA_APP_JAR is set and 0 JARs found in /deployments (1 expected) INFO exec -a "java" java -XX:MaxRAMPercentage=80.0 -XX:+UseParallelGC -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:+ExitOnOutOfMemoryError -cp "." -jar INFO running in /deployments Error: -jar requires jar file specification ...
作業が終わったらpodman image unmountコマンドでマウントを解除します。-aオプションで全てのマウントを解除します。
最後にexitでユーザーネームスペースを抜けます。
# podman image unmount -a 985daf3b31fb3a88c12a0f7e374bc2e29c5a6718c836f876eca99c0d91ff2ea8 # exit exit $
OpenJDKのコンテナイメージでjarファイルを実行する
jarファイルの実行の仕組みが分かったのでサンプルのjarファイルを作成してコンテナを実行してみます。
まず、Hello Worldを実行するJavaソースファイルを作成します。適当な作業ディレクトリを作ってその中で作業してください。後ほどコンテナにバインドマウントを行うため、HOME以外のカレントディレクトリに余計なファイルがない状態にするためです。
$ mkdir work
$ cd work
$ cat > hello.java <<EOF
public class hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
EOF
次に、OpenJDKのコンテナイメージを使ってコンパイルします。コンテナを使うことでローカルホストに直接OpenJDKをインストールする必要はありません。 OpenJDKのコンテナ内でJavaのコンパイルに必要なコマンドは揃っているようです。
$ podman run --rm -it openjdk-21 bash [default@2281f04e1b49 ~]$ ja jar jarsigner java javac javadoc javap
カレントディレクトリをバインドマウントしてコンパイルを実行します。
$ podman run --rm -it -v $PWD:$PWD:Z -w $PWD --user root openjdk-21 javac hello.java
--user root:OpenJDKのコンテナイメージはUID=185のdefaultというユーザーで実行されます。そのままバインドマウントを行うとローカルホスト上のUIDと異なりファイルの書き込みができません。また、実行ユーザーが固定されているので--userns=keep-idも使えません。そのため実行ユーザーをrootで上書きしてシンプルなコマンドで実行できるようにしています。
続けてjarファイルにパッケージ化します
$ podman run --rm -it -v $PWD:$PWD:Z -w $PWD --user root openjdk-21 jar cfe hello.jar hello hello.class
でき上がったファイルを確認します。
$ ls -l total 12 -rw-r--r--. 1 core core 417 Jun 11 02:22 hello.class -rw-r--r--. 1 core core 738 Jun 11 02:22 hello.jar -rw-r--r--. 1 core core 119 Jun 11 02:14 hello.java
jarファイルを指定して実行します。
$ podman run --rm -it -v $PWD:$PWD:Z -w $PWD openjdk-21 java -jar hello.jar Hello, World!
jarファイルができたのでOpenJDKのコンテナイメージに組み込んで新しいコンテナイメージを作成します。
jarファイルの実行の流れはこのようになります。

まず、下記のContainerfileを作成してビルドします。ベースイメージにOpenJDKコンテナを指定し、hello.jarを/deploymentsにコピーします。
$ cat > Containerfile <<EOF FROM registry.access.redhat.com/ubi9/openjdk-21 COPY hello.jar /deployments/hello.jar EOF $ podman build -t java-test . STEP 1/2: FROM registry.access.redhat.com/ubi9/openjdk-21 STEP 2/2: COPY hello.jar /deployments/hello.jar COMMIT java-test --> 3b11c122787a Successfully tagged localhost/java-test:latest 3b11c122787a0cd14d50ff2ef595a59e239c5731818a56b5f2f2328319a069c1
ビルドしたjava-testコンテナイメージを実行します。
$ podman run --rm java-test Starting the Java application using /opt/jboss/container/java/run/run-java.sh ... INFO exec -a "java" java -XX:MaxRAMPercentage=80.0 -XX:+UseParallelGC -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:+ExitOnOutOfMemoryError -cp "." -jar /deployments/hello.jar INFO running in /deployments Hello, World!
期待通りにHello Worldが実行されました。
まとめ
OpenJDKコンテナイメージを使うことで、コンテナ上でJavaのコンパイルを行ったりjarファイルの実行ができます。
また、OpenJDKコンテナイメージをベースイメージとして指定し、コンテナ内の/deploymentsにjarファイルを配置するだけでコンテナの実行時に自動で実行してくれます。
ベースのUBIイメージはRed Hatが定期的に更新しており、RHELの信頼性とセキュリティを担保された安心して使用できるコンテなイメージです。
UBIをベースイメージとしたOpenJDKのコンテナイメージは誰でも無償で使用できますので、Javaを使う方にはどなたにでもオススメのコンテナイメージです。
Podmanとコンテナを使うことでミドルウェアの理解がさらに深まりそうです。
参考:/opt/jboss/container/java/run/run-java.sh
run-java.shの出力結果を開く
root@localhost:~# cat $mnt/opt/jboss/container/java/run/run-java.sh
#!/bin/bash
# Fail on a single failed command
set -eo pipefail
export JBOSS_CONTAINER_UTIL_LOGGING_MODULE="${JBOSS_CONTAINER_UTIL_LOGGING_MODULE-/opt/jboss/container/util/logging}"
export JBOSS_CONTAINER_JAVA_RUN_MODULE="${JBOSS_CONTAINER_JAVA_RUN_MODULE-/opt/jboss/container/java/run}"
# Default the application dir to the S2I deployment dir
if [ -z "$JAVA_APP_DIR" ]
then JAVA_APP_DIR=/deployments
fi
source "$JBOSS_CONTAINER_UTIL_LOGGING_MODULE/logging.sh"
# ==========================================================
# Generic run script for running arbitrary Java applications
#
# This has forked (and diverged) from:
# at https://github.com/fabric8io/run-java-sh
#
# ==========================================================
# Error is indicated with a prefix in the return value
check_error() {
local msg=$1
if echo ${msg} | grep -q "^ERROR:"; then
log_error ${msg}
exit 1
fi
}
# detect Quarkus fast-jar package type (OPENJDK-631)
is_quarkus_fast_jar() {
if test -f quarkus-app/quarkus-run.jar; then
log_info "quarkus fast-jar package type detected"
echo quarkus-app/quarkus-run.jar
return 0
else
return 1
fi
}
# Try hard to find a sane default jar-file
auto_detect_jar_file() {
local dir=$1
# Filter out temporary jars from the shade plugin which start with 'original-'
local old_dir=$(pwd)
cd ${dir}
if [ $? = 0 ]; then
if quarkus="$(is_quarkus_fast_jar)"; then
echo "$quarkus"
return
fi
local nr_jars=`ls *.jar 2>/dev/null | grep -v '^original-' | wc -l | tr -d '[[:space:]]'`
if [ ${nr_jars} = 1 ]; then
ls *.jar | grep -v '^original-'
exit 0
fi
log_error "Neither \$JAVA_MAIN_CLASS nor \$JAVA_APP_JAR is set and ${nr_jars} JARs found in ${dir} (1 expected)"
cd ${old_dir}
else
log_error "No directory ${dir} found for auto detection"
fi
}
# Check directories (arg 2...n) for a jar file (arg 1)
get_jar_file() {
local jar=$1
shift;
if [ "${jar:0:1}" = "/" ]; then
if [ -f "${jar}" ]; then
echo "${jar}"
else
log_error "No such file ${jar}"
fi
else
for dir in $*; do
if [ -f "${dir}/$jar" ]; then
echo "${dir}/$jar"
return
fi
done
log_error "No ${jar} found in $*"
fi
}
load_env() {
# Configuration stuff is read from this file
local run_env_sh="run-env.sh"
# Load default default config
if [ -f "${JBOSS_CONTAINER_JAVA_RUN_MODULE}/${run_env_sh}" ]; then
source "${JBOSS_CONTAINER_JAVA_RUN_MODULE}/${run_env_sh}"
fi
# Check also $JAVA_APP_DIR. Overrides other defaults
# It's valid to set the app dir in the default script
if [ -f "${JAVA_APP_DIR}/${run_env_sh}" ]; then
source "${JAVA_APP_DIR}/${run_env_sh}"
fi
export JAVA_APP_DIR
# JAVA_LIB_DIR defaults to JAVA_APP_DIR
export JAVA_LIB_DIR="${JAVA_LIB_DIR:-${JAVA_APP_DIR}}"
if [ -z "${JAVA_MAIN_CLASS}" ] && [ -z "${JAVA_APP_JAR}" ]; then
JAVA_APP_JAR="$(auto_detect_jar_file ${JAVA_APP_DIR})"
check_error "${JAVA_APP_JAR}"
fi
if [ "x${JAVA_APP_JAR}" != x ]; then
local jar="$(get_jar_file ${JAVA_APP_JAR} ${JAVA_APP_DIR} ${JAVA_LIB_DIR})"
check_error "${jar}"
export JAVA_APP_JAR=${jar}
else
export JAVA_MAIN_CLASS
fi
}
# Combine all java options
get_java_options() {
local jvm_opts
local debug_opts
local proxy_opts
local opts
if [ -f "${JBOSS_CONTAINER_JAVA_JVM_MODULE}/java-default-options" ]; then
jvm_opts=$(${JBOSS_CONTAINER_JAVA_JVM_MODULE}/java-default-options)
fi
if [ -f "${JBOSS_CONTAINER_JAVA_JVM_MODULE}/debug-options" ]; then
debug_opts=$(${JBOSS_CONTAINER_JAVA_JVM_MODULE}/debug-options)
fi
if [ -f "${JBOSS_CONTAINER_JAVA_PROXY_MODULE}/proxy-options" ]; then
source "${JBOSS_CONTAINER_JAVA_PROXY_MODULE}/proxy-options"
proxy_opts="$(proxy_options)"
fi
opts=${JAVA_OPTS-${debug_opts} ${proxy_opts} ${jvm_opts} ${JAVA_OPTS_APPEND}}
# Normalize spaces with awk (i.e. trim and eliminate double spaces)
echo "${opts}" | awk '$1=$1'
}
# Read in a classpath either from a file with a single line, colon separated
# or given line-by-line in separate lines
# Arg 1: path to claspath (must exist), optional arg2: application jar, which is stripped from the classpath in
# multi line arrangements
format_classpath() {
local cp_file="$1"
local app_jar="$2"
local wc_out=`wc -l $1 2>&1`
if [ $? -ne 0 ]; then
log_error "Cannot read lines in ${cp_file}: $wc_out"
exit 1
fi
local nr_lines=`echo $wc_out | awk '{ print $1 }'`
if [ ${nr_lines} -gt 1 ]; then
local sep=""
local classpath=""
while read file; do
local full_path="${JAVA_LIB_DIR}/${file}"
# Don't include app jar if include in list
if [ x"${app_jar}" != x"${full_path}" ]; then
classpath="${classpath}${sep}${full_path}"
fi
sep=":"
done < "${cp_file}"
echo "${classpath}"
else
# Supposed to be a single line, colon separated classpath file
cat "${cp_file}"
fi
}
# Fetch classpath from env or from a local "run-classpath" file
get_classpath() {
local cp_path="."
if [ "x${JAVA_LIB_DIR}" != "x${JAVA_APP_DIR}" ]; then
cp_path="${cp_path}:${JAVA_LIB_DIR}"
fi
if [ -z "${JAVA_CLASSPATH}" ] && [ "x${JAVA_MAIN_CLASS}" != x ]; then
if [ "x${JAVA_APP_JAR}" != x ]; then
cp_path="${cp_path}:${JAVA_APP_JAR}"
fi
if [ -f "${JAVA_LIB_DIR}/classpath" ]; then
# Classpath is pre-created and stored in a 'run-classpath' file
cp_path="${cp_path}:`format_classpath ${JAVA_LIB_DIR}/classpath ${JAVA_APP_JAR}`"
else
# No order implied
cp_path="${cp_path}:${JAVA_APP_DIR}/*"
fi
elif [ "x${JAVA_CLASSPATH}" != x ]; then
# Given from the outside
cp_path="${JAVA_CLASSPATH}"
fi
echo "${cp_path}"
}
# Mask secrets before printing
mask_passwords() {
local content="$1"
local result=""
IFS=' ' read -r -a key_value_pairs <<< "$content"
for pair in "${key_value_pairs[@]}"; do
key=$(echo "$pair" | cut -d '=' -f 1)
value=$(echo "$pair" | cut -d '=' -f 2-)
if [[ $key =~ [Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd] ]]; then
result+="$key=***** "
else
result+="$pair "
fi
done
echo "${result% }"
}
# Start JVM
startup() {
# Initialize environment
load_env
local args
cd ${JAVA_APP_DIR}
if [ "x${JAVA_MAIN_CLASS}" != x ] ; then
args="${JAVA_MAIN_CLASS}"
else
args="-jar ${JAVA_APP_JAR}"
fi
local procname="${JAVA_APP_NAME-java}"
local masked_opts=$(mask_passwords "$(get_java_options)")
log_info "exec -a \"${procname}\" java ${masked_opts} -cp \"$(get_classpath)\" ${args} $*"
log_info "running in $PWD"
exec -a "${procname}" java $(get_java_options) -cp "$(get_classpath)" ${args} $*
}
# =============================================================================
# Fire up
startup $*
*1:https://docs.redhat.com/ja/documentation/red_hat_build_of_openjdk/21/html/release_notes_for_red_hat_build_of_openjdk_21.0.3/pr01
*2:https://docs.redhat.com/ja/documentation/red_hat_build_of_openjdk/21/html/packaging_red_hat_build_of_openjdk_21_applications_in_containers/openjdk-apps-in-containers