LoginSignup
47
26

More than 5 years have passed since last update.

サイドカーコンテナを含む Kubernetes Job を完了させる

Last updated at Posted at 2017-01-23

TL;DR

  • Kubernetes でマルチコンテナな Pod はよく使われる
    • Cloud SQL Proxy はサイドカーコンテナとして動く前提
  • Job の完了には Pod を構成するプロセスが完了する必要がある
    • PID Namespace は未実装
      • シグナルが送れない
    • 完了しないと不意に再開する
  • サイドカーコンテナを殺すためのプログラムを書いてみた

サイドカーコンテナパターン

Kubernetes は複数のコンテナからなる Pod を最小の単位として扱う。Google の社内コンテナ管理システムの Borg にも同様の仕組みがあり、その経験から「Design Patterns for Container-based Distributed Systems」でコンテナデザインパターンとして複数コンテナの連携のパターンを定義している。
このコンテナデザインパターンの中でも頻出するのが、メインのコンテナに機能性の拡張をするためにコンテナ間の共有ボリュームを利用するサイドカーコンテナパターンだ。

問題 サイドカーコンテナを含む Job が完了しない

GKE を使っていると下記のように Cloud SQL Proxy をサイドカーコンテナとして動かす Job が頻出する。
(注: Pod 内コンテナ間の内部ネットワーク通信を使って外部に接続するアンバサダーパターンも類似しているが、https://cloud.google.com/sql/docs/container-engine-connect#before_you_beginのでここではサイドカーとする。)

apiVersion: batch/v1
kind: Job
metadata:
  name: test-job
spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
      - name: mysql
        image: mysql:5.7
        command: ["mysql", "$(MYSQL_DB_NAME)", "--execute", "insert into job_test values()"]
        env:
          - name: MYSQL_UNIX_PORT
            value: /share/my-project:asia-northeast1:sandbox-cloudsql
          - name: MYSQL_DB_NAME
            value: job-test
        volumeMounts:
          - name: share
            mountPath: /share
      - name: sql-proxy
        image: b.gcr.io/cloudsql-docker/gce-proxy:1.05
        command: ["/cloud_sql_proxy", "-dir", "/share", "-projects", "my-project"]
        env:
          - name: GOOGLE_APPLICATION_CREDENTIALS
            value: /secret/sql-admin.json
        volumeMounts:
          - name: certs
            mountPath: /etc/ssl/certs
            readOnly: true
          - name: service-account
            mountPath: /secret
            readOnly: true
          - name: share
            mountPath: /share
      volumes:
      - name: certs
        hostPath:
          path: /etc/ssl/certs
      - name: share
        emptyDir: {}
      - name: service-account
        secret:
          secretName: service-account-secret

しかし、上のマニフェストから Job を作成すると下記のような結果となる。

$ kubectl apply -f job.yaml --namespace job-test
job "test-job" created

$ kubectl get job,pod --namespace job-test
NAME            DESIRED   SUCCESSFUL   AGE
jobs/test-job   1         0            30s

NAME                READY     STATUS      RESTARTS   AGE
po/test-job-kug33   1/2       Completed   1          30s

Job は SUCCESSFUL にならず、2つのコンテナのうち1つは終了せずに残っている。
残っているのは明示的に終了していない sql-proxy コンテナだ。

弊害

終了していないことで何が困るのか?例として2つあげる。

  • Job の完了を待って次のアクションをするようなフローを想定しているソフトウェアが動かなくなる。
    • Helm Hooks は Job の完了に依存しているので、完了しないといつまでも先に進めなくなる。
  • Job を構成する Pod は完全には完了していない扱いなので、終了した際に再起動してしまう。
    • 上記の例だと、再起動される度に insert が実行されてしまう。
    • DB アクセスする Job の Pod が未完了で溜まったところで Node トラブルなどが起こるとデータベースに大きな負荷が掛かったり、最悪データが壊れることもある。

一つの解決法

http://stackoverflow.com/a/38628708/7301398
この回答で Google の Tim Hockin が Stack Overflow で「emptyDir ボリュームにサイドカーに死ねって教えるファイルを置けば良いじゃん」と言っていたので、そのように動作するプログラムをかんたんに書いてみた。

https://github.com/aktsk/guillotine
業務時間中に書いて業務で使うつもりだったものの特にサポートするつもりはないので参考程度にどうぞ。

使ってみる

guillotine の動作を解説すると下記のようになる。

  • GUILLOTINE_WATCHED_FILE 環境変数に emptyDir ボリュームとして共有されているパスを指定
  • サイドカーコンテナは guillotine を通してプロセスを起動
  • Job の主な処理を行うコンテナは完了時に GUILLOTINE_WATCHED_FILE にファイルを作成
    • GUILLOTINE_WATCHED_FILE にファイルが作成された時点で guillotine は子プロセスごと死にサイドカーコンテナが終了

guillotine を使って前述の job.yaml を書き換えてみると下記のようになる。

apiVersion: batch/v1
kind: Job
metadata:
  name: test-job2
spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
      - name: mysql
        image: mysql:5.7
        command: ["sh", "-c", "mysql $(MYSQL_DB_NAME) -e 'insert into job_test values()' && touch $(GUILLOTINE_WATCHED_FILE)"]
        env:
          - name: MYSQL_UNIX_PORT
            value: /share/my-project:asia-northeast1:sandbox-cloudsql
          - name: MYSQL_DB_NAME
            value: job-test
          - name: GUILLOTINE_WATCHED_FILE
            value: /share/completed
        volumeMounts:
          - name: share
            mountPath: /share
      - name: sql-proxy
        image: asia.gcr.io/my-project/gce-proxy-guillotine:1.05
        command: ["/guillotine", "/cloud_sql_proxy", "-dir", "/share", "-projects", "my-project"]
        env:
          - name: GOOGLE_APPLICATION_CREDENTIALS
            value: /secret/sql-admin.json
          - name: GUILLOTINE_WATCHED_FILE
            value: /share/completed
        volumeMounts:
          - name: certs
            mountPath: /etc/ssl/certs
            readOnly: true
          - name: service-account
            mountPath: /secret
            readOnly: true
          - name: share
            mountPath: /share
      volumes:
      - name: certs
        hostPath:
          path: /etc/ssl/certs
      - name: service-account
        secret:
          secretName: service-account-secret
      - name: share
        emptyDir: {}

マニフェストから Job を作成してみると下記のようになる。

$ kubectl apply -f job2.yaml --namespace job-test
job "test-job2" created

$ kubectl get job,pod --namespace job-test
NAME             DESIRED   SUCCESSFUL   AGE
jobs/test-job2   1         1            6s

ちゃんと SUCCESSFUL になるようになった。
--show-all を付けて表示される完了済の Pod を delete しても再起動しないことも確認した。

別解・将来的に可能になる解

47
26
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
26