前処理をしたい 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に応答するよう設定しました。もっと良い方法があれば教えてください。