前処理をしたい Link to heading
アプリケーションの起動前にセットアップなどの前処理をしたいことってよくあります。Litestreamの復元とか。コンテナ環境で実行されるアプリケーションの場合、起動スクリプト entrypoint.sh
で前処理をしてからアプリケーションの実行をする方法が一般的かと思います。
KubernetesのPodなど、複数containerをまとめた単位として実行できる環境では、前処理を別のcontainerに分離することができます。 これにより、前処理のみに必要なソフトウェアをアプリケーション用のイメージにインストールする必要がなくなります。
initContainers Link to heading
この前処理のための機能をKubernetesではinitContainersという形で提供しています。
Podが実行されると、まずinitContainersが順番に実行され、全てのinitContainersが正常終了するとcontainers(アプリケーション)が実行されます。 initContainersはコンテナの正常終了が次のコンテナを起動するトリガーになります。
CloudRun Link to heading
ところが、CloudRunはKubernetesではないのでinitContainersがありません。どうも現時点でそれに相当する機能は提供していないようです(2023-10-26)。
その代わり、CloudRunでは複数コンテナの依存関係を記述する run.googleapis.com/container-dependencies
アノテーションを利用して起動順序を制御します。
CloudRunのcontainersはコンテナの起動完了が次のコンテナを起動するトリガーになるようです。
container-dependencies Link to heading
CloudRunでコンテナ起動の順序を制御するのが run.googleapis.com/container-dependencies
アノテーションです。
これを試してみましょう。 次のようなservice.yamlを用意します。 前処理は時間がかかる想定で10秒のsleepを入れています。 また、initContainersと異なり前処理コンテナinitが終了するとserviceが終了してしまうため、前処理コンテナ initは前処理が完了した後無限にsleepするようにしています。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: init-containers-sample
annotations:
run.googleapis.com/launch-stage: BETA
spec:
template:
metadata:
annotations:
run.googleapis.com/container-dependencies: '{"app": ["init"]}'
spec:
containers:
- image: busybox
name: init
command: ['/bin/sh']
args: ['-c', 'echo init container start && sleep 10 && echo init container finish && sleep infinity']
- image: nginx
name: app
ports:
- containerPort: 80
gcloud run services replace service.yaml --region ${REGION} --project ${PROJECT}
で実行した後、logを確認してみます。
Cloud loggingからjson形式でダウンロードしたログをjqで整形します。 今回はコンテナの実行順序を見たいため、1列目をコンテナ名、2列目をテキストにしました。
% cat ~/Downloads/downloaded-logs-20231027-110203.json | jq -c '.[] | [.labels.container_name, .textPayload]'
["app","/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration"]
["app","/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/"]
["app","/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh"]
["init","start"]
["app","10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf"]
["app","10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf"]
["app","/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh"]
["app","/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh"]
["app","/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh"]
["app","/docker-entrypoint.sh: Configuration complete; ready for start up"]
["app","2023/10/27 00:26:41 [notice] 1#1: using the \"epoll\" event method"]
["app","2023/10/27 00:26:41 [notice] 1#1: nginx/1.25.3"]
["app","2023/10/27 00:26:41 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) "]
["app","2023/10/27 00:26:41 [notice] 1#1: OS: Linux 4.4.0"]
["app","2023/10/27 00:26:41 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 25000:25000"]
["app","2023/10/27 00:26:41 [notice] 1#1: start worker processes"]
["app","2023/10/27 00:26:41 [notice] 1#1: start worker process 24"]
["app","2023/10/27 00:26:41 [notice] 1#1: start worker process 25"]
["init","finish"]
ログを見るとわかるように、前処理のinit containerより前にapp containerが起動しています。これはCloudRunがinitコンテナの起動完了を検知する術を持たないためでしょう。 前処理が完了してからアプリケーションが起動してほしいのでこれは意図通りではありません。
startupProbe Link to heading
initコンテナの起動が完了したことを伝えることで、initコンテナの起動完了後にappコンテナを起動するという動作を実現することができます。
CloudRunの公式ドキュメントより、startupProbeが設定されている場合は次のコンテナの起動がブロックされるようです。 CloudRunで利用できるstartupProbeはHTTP, TCP, gRPCで、残念ながらexecは利用できません。なのでここではportを開けてTCPで前処理の完了を確認することにします。 試してみましょう。
先のyamlファイルを次の様に修正します。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: init-containers-sample
annotations:
run.googleapis.com/launch-stage: BETA
spec:
template:
metadata:
annotations:
run.googleapis.com/container-dependencies: '{"app": ["init"]}'
spec:
containers:
- image: busybox
name: init
command: ['/bin/sh']
args: ['-c', 'echo init container start && sleep 10 && echo init container finish && nc -lkp 8081 -e echo "init container started"'] ## 修正
startupProbe: ## 追加
tcpSocket: ## 追加
port: 8081 ## 追加
- image: nginx
name: app
ports:
- containerPort: 80
gcloud run services replace service.yaml --region ${REGION} --project ${PROJECT}
で更新して実行します。
logを見てみましょう。
["init","init container start"]
["init","init container finish"]
["init","STARTUP TCP probe succeeded after 2 attempts for container \"init\" on port 8081."]
["app","/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration"]
["app","/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/"]
["app","/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh"]
["app","10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf"]
["app","10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf"]
["app","/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh"]
["app","/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh"]
["app","/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh"]
["app","/docker-entrypoint.sh: Configuration complete; ready for start up"]
["app","2023/10/27 02:35:53 [notice] 1#1: using the \"epoll\" event method"]
["app","2023/10/27 02:35:53 [notice] 1#1: nginx/1.25.3"]
["app","2023/10/27 02:35:53 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) "]
["app","2023/10/27 02:35:53 [notice] 1#1: OS: Linux 4.4.0"]
["app","2023/10/27 02:35:53 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 25000:25000"]
["app","2023/10/27 02:35:53 [notice] 1#1: start worker processes"]
["app","2023/10/27 02:35:53 [notice] 1#1: start worker process 24"]
["app","2023/10/27 02:35:53 [notice] 1#1: start worker process 25"]
["app","Default STARTUP TCP probe succeeded after 1 attempt for container \"app\" on port 80."]
意図通り、前処理の完了を待ってからアプリケーションコンテナが起動していることがわかります。
また、appコンテナにはstartupProbeは指定していませんでしたが、 containerPort
を設定することによってデフォルトでTCPのstartupProbeが設定されていることもわかります。
まとめ Link to heading
CloudRunで複数コンテナの依存関係を指定するためcontainers-dependencies
を設定します。
startupProbe
によるヘルスチェックが設定されていない場合、依存関係に関わらず次のコンテナが起動してしまいます。
今回は前処理の後にTCPサーバを立てることでstartupProbeに応答するよう設定しました。もっと良い方法があれば教えてください。