TL;DR
- Kubernetes でマルチコンテナな Pod はよく使われる
- Cloud SQL Proxy はサイドカーコンテナとして動く前提
- Job の完了には Pod を構成するプロセスが完了する必要がある
- PID Namespace は未実装
- シグナルが送れない
- 完了しないと不意に再開する
- 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 しても再起動しないことも確認した。
別解・将来的に可能になる解
- Pod 内のコンテナで Downward API から自分自身の Pod 名を取得し API クライアントから他のコンテナの完了を知ることが可能
- Pod 内のコンテナ間での PID Namespace の共有についての議論
- Pod 内のコンテナに状態の通知ができるようになる API の議論