KubernetesのPodでDockerコンテナを実行する
KubernetesのPodでDockerコンテナを実行する方法を紹介します。
Dockerコンテナ内でコンテナを起動する方法のことをDinD (Docker inside Docker)などと言われていますが、同じことをKubernetesのPodで実行するには少し工夫する必要があります。
アプローチとして2種類のやり方があり、双方にメリットとデメリットがあるため状況に合わせて適切なものを選択する必要があります。
アプローチ
1つめのやり方はPodが動いているホストマシンのDockerデーモンを共有する方法で、DooD (Docker outside of Docker)とよばれるものをPodに適応させたパターンです。
2つめのやり方はPod内でDockerデーモンを動かす、DinD (Docker inside Docker)と呼ばれるものを適応させたパターンです。
それぞれの方法のやり方とメリット・デメリットについて紹介します。
DooDパターン
DooDパターンをk8sに適応させるのは簡単で、ホストの /var/run
を共有してDockerデーモンにアクセスできるようにするだけです。
apiVersion: v1 kind: Pod metadata: name: dood spec: containers: - name: docker image: docker:19.03 command: ["docker", "run", "nginx:latest"] volumeMounts: - mountPath: /var/run name: docker-sock volumes: - name: docker-sock hostPath: path: /var/run
DooDパターンの場合、コンテナ内で作成されたコンテナはホストが管理するその他のコンテナと同列の扱いになることに注意する必要があります。
つまり、コンテナを作成したPodと同列の扱いのコンテナとなります。
しかしながらPod内で作成したコンテナはk8sの管理下にはありません。
また、もし他にもコンテナを作成するPodが存在した場合は、コンテナ名などのリソースが競合する可能性があるので注意しなければいけません。
偶然発生する可能性は低いですが、k8sのリソースと競合する可能性もあります。
コンテナのポートをマッピングする場合も注意が必要で、デーモンが動いているのはホストマシンになるためポートマッピングされるのもホストになります。
そのためコンテナを作成したPodのIPで作成したコンテナにはアクセスできません。ホストのIPでアクセスする必要があります。
また、k8sの管理から外れるため、Podに指定したリソース制限が適用されません。
さらに -d
としてコンテナを起動した場合はPodの削除時に作成したコンテナが削除されません。
このように様々な落とし穴があるため、k8sのPodの場合はDooDパターンはオススメしません。次に解説するDinDパターンを使うべきです。
メリット
- 特別な権限を与える必要がない
- コンテナを動かしたいイメージにDockerコマンドをインストールするだけで動かすことができる
デメリット
- Podの中ではなくホストマシンの中にコンテナが作成される
- k8sの管理下からはずれたところにコンテナが作成される
- ホストでリソースの競合が発生する可能性がある
DinDパターン
DinDパターンはDooDパターンと比べると少しだけ複雑です。
dockerコマンドを実行するコンテナのサイドカーとしてDockerデーモンを動かし、dockerコマンドがサイドカーに対してアクセスするように設定します。
以下がその設定をしたマニフェストファイルです。
apiVersion: v1 kind: Pod metadata: name: dind spec: containers: - name: docker image: docker:18.09 command: ["docker", "run", "nginx:latest"] env: - name: DOCKER_HOST value: tcp://localhost:2375 - name: dind-daemon image: docker:18.09-dind resources: requests: cpu: 20m memory: 512Mi securityContext: privileged: true
Dockerデーモンを動かすコンテナは公式のイメージがあるのでそれを使います。この公式イメージは2375ポートでDockerのREST APIを公開しています。
そこに対してDocker CLIがアクセスするように環境変数 DOCKER_HOST
を設定します。
DinDパターンでは作成したコンテナはDockerデーモンが動くサイドカーコンテナの中に作成されるため、Podに設定したCPUやメモリなどのリソースの制限を受けた状態でコンテナを作成できます。
また、このPodを削除したときに作成したコンテナも同時に削除されます。
ポートマッピングを使用したときもDockerデーモンが動いているネットワーク、つまりはPodのネットワークに対して行われるため、ホストのリソースを汚すことなくPodのIPでアクセスできます。
同様にコンテナ名などのリソース競合もPod内でしかおこりません。
ただし一つだけ意識しなければならないことがあり、Dockerデーモンが動くコンテナは privileged
権限で実行しなければいけません。ここが障害にならない限りDinDパターンを選択することをオススメします。
メリット
- Pod内にコンテナを作成できる
- ホストに影響を与えることなくPod内で完結する
- Podの設定(リソースやネットワーク)を引き継ぐことができる
デメリット
- サイドカーコンテナが1つ増える
- privilegedでコンテナを動かす必要がある
まとめ
privilegedで動かしても問題ない環境であればDinDパターンがオススメです。
DooDを使う場合はリソース競合など、ホスト側でコンテナが動いていることを忘れずに使う必要があります。
QtでMacアプリを作ってみる
QtでMacのアプリをビルドし、.app
の形式として書き出します。
環境
事前準備
使用するXcodeのCommand Line Toolsを設定します。
$ sudo xcode-select -s /Applications/Xcode10.2.1.app
Command Line Toolsが正しく設定されているか確認します。
$ xcode-select -p /Applications/Xcode10.2.1.app/Contents/Developer
qtのインストール
Qtの公式サイトからオープンソース版をダウンロードします。
ダウンロードしたら流れにそってインストールします。
ここでQt本体にチェックをつけないとDeveloper Toolsしかインストールされないのでチェックを入れます。
ダウンロードが終わったらインストール完了です。
プロジェクトの作成
インストールが完了するとQt Creator
が起動するので、New Project
をクリックしてQt Widgets Application
を選択します。
適当なプロジェクト名を入力すれば作成されます。
Qt Creatorでビルドを実行するとこのような画面が作られます。
アプリ(.app)化する
Qt Creatorからはアプリを実行できたので次は.app
として書き出します。
書き出すにはqmake
というツールを使うため、qmake
にパスを通します。
.bashrc
や.zshrc
などに以下を追加します。
export PATH="$HOME/Qt/5.12.3/clang_64/bin:$PATH"
プロジェクトのあるディレクトリに移動して以下のコマンドを実行すれば、プロジェクトのあるディレクトリに.app
が作られます。
$ qmake -project
$ qmake
$ make
この環境ではmake
を実行するときにエラーになりました。
.pro
ファイルに以下を追加するとmake
が動くようになります。
QT += widgets
参考
DockerのCMDでログファイルをtailする
Dockerは標準出力に出力したログなどはlogs
コマンドで確認することができますが、Dockerで動かすアプリケーションがファイルにログを出力している場合はlogs
コマンドで確認することができません。
このような場合は、CMDでtail -f
することが多いと思います。
CMD tail -f /var/log/cron.log
しかしこれだとlogs
コマンドに何も流れてきません。
そこでログファイルの中身をexec
コマンドで確認してみると正しくログが出力されています。
なにが起こっているのでしょうか。
Dockerのファイルシステム
原因はDockerが採用しているファイルシステムにあります。
DockerはUnion File System
というファイルシステムを採用していて、このファイルシステムはcopy-on-write
という方法で動作しています。
Union File System
やcopy-on-write
についてはこの記事がわかりやすいです。
直接的な原因は、CMDでtailしているログファイルはDocker imageに保存されているファイルであり、コピーされてログが書き込まれるようになったファイルではないためです。
対策
CMDでtailする前にcopy-on-writeを発生させればいいだけです。
やり方は色々あると思いますが、1つの例としてはこのようにCMDを書けば意図した通りに動きます。
CMD : >> /var/log/cron.log && tail -f /var/log/cron.log
なお、このようにCMDで&&
を使って複数コマンドを実行するときは、DockerのPID1とゾンビプロセスのことを考慮する必要が出てくるので注意しましょう。
参考
dockerfile - Output of `tail -f` at the end of a docker CMD is not showing - Stack Overflow
自作したiOSライブラリの更新手順
自作したiOSのライブラリをCocoaPodsとCarthageで公開する記事はたくさんありますが、新バージョンの公開に関する記事はほとんど無く毎回困るのでまとめます。
Carthage
Carthageで公開しているライブラリの新バージョンをリリースするのは簡単で、GitHubで新しいrelease(タグ打ち)をするだけです。
そのため、準備が整ったらmasterにマージしてタグを打てばOKです。
CocoaPods
CocoaPodsは少し作業が発生します。ただ、公開するときに実行したコマンドの一部をもう一度実行するだけなので、忘れなければ簡単な作業です。
podspecの更新
podspecファイルに書いてあるバージョンをリリース予定のバージョンに更新します。
そのほかの内容は基本的に更新する必要はありません。もちろん、ここが変わるような変更を加えたリリースなら更新する必要があります。
.swift-versinoの更新
最近のXcodeには複数のバージョンのSwiftが付属しているため、ライブラリが使用するSwiftのバージョンを教えてあげないと正しく動かない場合があります。そのため.swift-version
というファイルを作成してリポジトリのルートに配置します。新バージョンのリリースでSwiftのバージョンが変わる場合はこのファイルに書いてあるバージョンも更新します。
バリデーション
更新したpodspecファイルに問題ないかチェックします。
pod lib lint
かpod spec lint
を実行します。
pod spec lint
はリポジトリのチェックまで行うため、新しいバージョンのタグが存在しない場合はエラーになります。未公開の場合はpod lib lint
を実行します。こちらはローカルにある情報だけでチェックします。
新バージョンの公開
ドキュメントの更新なども済ませたらmasterにマージして新バージョンのタグを打ちます。
その後、pod trunk push
を実行してpodsの公開は完了です。
前回のリリースから時間がたっているとpodsの認証が切れてしまっているので、pod trunk register hoge@hoge.com 'Hoge Huga'
と実行して再度認証し、その後もう一度リリースのコマンドを実行します。
複数のPodのログをまとめて表示する
Kubernetesを開発環境として使用しプロダクションと同じような環境で開発すると、複数のアプリケーションが可動することになると思います。
そうするとログも複数の場所に出力されることになります。
複数個のターミナルを立ち上げ1つずつログを表示してもいいですが、場所を取りますし確認する箇所が増えて面倒なので、まとめて表示する方法を紹介します。
今回は以下の2つの構成に合わせたやり方を紹介します。
- ログを標準出力に書き出している場合
- ログをファイルに書き出している場合
ログを標準出力に書き出している場合
これは stern
というアプリケーションを使えば簡単にできます。
使い方は簡単でPodの名前を引数として渡すだけです。
正規表現でいい感じに検索してくれて、オプションを入れればコンテナで絞り込むこともできます。
Dockerの標準に沿ったアプリケーションならこれを使えば解決です。
ログをファイルに書き出している場合
様々な理由によりDockerの標準に合わせることができず、ファイルにログを書き出している場合もあると思います。
この場合は kubectl exec
で tail
することになりますが、複数のPodのログをまとめるためには少し工夫をしなければいけません。
名前付きパイプを使う
名前付きパイプを使って複数の kubectl exec
の出力を1つにまとめます。
名前付きパイプについての説明はこの記事がわかりやすいです。
名前付きパイプに kubectl exec
を出力し、名前付きパイプを cat
することでログをまとめて表示します。
シェルスクリプトで一連の処理を書きます。
#! /usr/bin/env bash FIFO="/tmp/shout" mkfifo ${FIFO} trap 'rm ${FIFO}; exit 1' 1 2 3 15 PODA=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-a")].metadata.name}') PODB=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-b")].metadata.name}') PODC=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-c")].metadata.name}') kubectl exec -i ${PODA} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \ kubectl exec -i ${PODB} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \ kubectl exec -i ${PODC} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \ cat ${FIFO}
stern
と比べると見やすさや使いやすさで数段劣りますが、同じようにまとめて出力することができます。
GKEでプリエンプティブインスタンスを使いこなす
GCPにあるプリエンプティブ インスタンスをGKEでうまいこと使えないか、試行錯誤した結果をまとめます。
プリエンプティブ インスタンスとは
一言で言ってしまえば、AWSにあるスポットインスタンスのGCP版です。
公式ドキュメントにはこのように書いてあります。
プリエンプティブ VM は、最長持続時間が 24 時間で、可用性が保証されない Google Compute Engine VM インスタンスです。プリエンプティブ VM は標準的な Compute Engine VM よりも低価格で、同じマシンタイプとオプションを使用できます。
ドキュメントに書かれているように低価格で使用できるのが最大のメリットです。
だいたい1/3ぐらいの価格で使用することができます。
しかし、様々な制限があります。
意識しておかなければならないこととして、次のようなことがあります。
- いつ終了するかわからない
- 最大でも24時間でシャットダウンされる
- 常に使用できるとは限らない
これらの注意点と上手に付き合いながらGKEのNodeとして使用していきます。
使いこなす
シャットダウンの対策
まず、シャットダウンによりNode数が減ってしまう問題はGKEを使っている上では問題ありません。
KubernetesがNode数の減少を検知して、即座に元と同じ数になるようにNodeを起動し直してくれます。
しかし、一時的とはいえNodeが減るので、そこで可動しているPodは止まってしまします。
すぐに復帰するので、冗長化されているPodであればそこまで問題でもありません。せいぜい、一時的(数秒から数十秒)に別のPodに負荷が集中するぐらいです。
アプリケーションの構成などの理由により冗長化できないPodや一時的でも減っては困るPodに対しては、プリエンプティブ インスタンスに配置されないように設定をします。
プリエンプティブ インスタンスの回避
Kubernetesのtaintsとtolerationsという機能を使用してプリエンプティブ インスタンスを回避します。
まず、通常のインスタンスで構成されるノードプールとは別に、プリエンプティブ インスタンスだけで構成されたノードプールを作成します。 プリエンプティブのノードプールを作成するときに、ノードtaintを設定します。
そして、プリエンプティブ インスタンスに配置されても問題ないPodにtolerationを設定します。
spec: template: spec: containers: ... tolerations: - key: gke-preemptible operator: Equal value: "true" effect: NoSchedule
これでこの設定がされているPodのみがプリエンプティブ インスタンスのノードプールに配置されるようになります。
taintsとtolerations
ちょっと仕組みがわかりにくいので、taintsとtolerationsの概念を説明します。
Nodeにtaints(よごれ)をつけ、そのtaintsをtolerations(寛容、黙許)できないPodをNoSchedule(スケジュールしない)と設定しています。
そしてPodにtolerationするtaintの内容を記述することで、taintのついたNodeに配置されるようになります。
24時間でシャットダウンされる
上で書いたプリエンプティブ インスタンスの回避の設定をしていれば24時間でシャットダウンされる制限も問題ないように感じるかもしれません。
しかし、24時間たつ前にシャットダウンされることがなかった場合、ノードプール内の複数のインスタンスが同時に24時間を経過し、同じタイミングでインスタンスがシャットダウンする可能性があります。
これだと、いくら冗長化していても運しだいですべてのPodが消えてしまう可能性があり、リスクが高くなります。
この問題の解決方法は簡単で、ノードプールを追加後、起動時間がバラけるように一部のNodeをシャットダウンしてしまえば良いです。
そうすることで複数のNodeが同時に24時間を迎えてシャットダウンされることが防げます。
常に使用できるかわからない
プリエンプティブ インスタンスがシャットダウンされたあと、GCPの状況によってはプリエンプティブ インスタンスが起動できないことがあります。
この場合、通常のインスタンスで構成されるノードプールにクラスターのオートスケールを設定しておくことで、そっちにインスタンスが追加されPodが配置されます。
tolerationsの設定がされていれも問題なく通常のインスタンスに配置されます。
なぜならtolerationsはtaintsを受け入れる設定なので、taintsが無いNodeにも配置されます。
まとめ
- 通常のインスタンスとプリエンプティブ インスタンスでノードプールを分ける
- プリエンプティブ インスタンスのノードプールにはtaintsを設定する
- 複数のプリエンプティブ インスタンスが同時に24時間をむかえないように起動時間を調整する
これを意識することでGKEを低価格で運用することができます。
ぜひ試してみてください。
GKEからCloud SQLに接続する
GKEからCloud SQLに接続するやり方としてCloud SQL Proxyを使う方法が推奨されています。
公式ドキュメントで解説されているやり方は、Cloud SQLにアクセスしたいコンテナが入っているPodにサイドカーとしてProxyを入れる方法です。
公式の例だと、wordpressのコンテナとProxyのコンテナを同じPodにしています。
GKE内にCloud SQLに接続するコンテナが一種類であれば公式のやり方で問題ありませんが、複数のサービスがCloud SQLにアクセスするとなると、全てのコンテナにサイドカーとして入れることになってしまい、あまりうれしくありません。
そこでProxyを単体のPodとして作成し、Serivceを経由してアクセスする方法をとります。
Deploymentの作成
基本的には公式ドキュメントと同じように進め、サイドカーとなっている部分だけを残し、適切なポートを開放してあげれば完了です。
注意点として、他のPodからのアクセスを受け入れる場合は、起動スクリプトでIPを指定しなければなりません。
ポート番号だけだと、ローカルホストからのアクセスしか受け付けてくれません。
具体的にはこのようにします。
command: ["/cloud_sql_proxy", "-instances=sample-165109:asia-northeast1:sample=tcp:0.0.0.0:3306", "-credential_file=/secrets/cloudsql/credentials.json"]
あとは適切にServiceを作成すれば完成です。
デメリット
サイドカーではなくPod単体として起動させるということは、クラスタ内のDBにアクセスするPodが全てこのPodを経由することになります。
そのため、このPodが起動していないと、全てのPodはDBにアクセスできなくなってしまいますので、きちんと冗長化しておきましょう。
定義ファイル
最後に定義ファイルの全体を貼っておきます。
apiVersion: apps/v1 kind: Deployment metadata: name: sql-proxy labels: app: sql-proxy spec: replicas: 2 selector: matchLabels: app: sql-proxy template: metadata: labels: app: sql-proxy spec: containers: - name: cloudsql-proxy image: gcr.io/cloudsql-docker/gce-proxy:1.11 ports: - name: mysql containerPort: 3306 command: ["/cloud_sql_proxy", "-instances=sample-165109:asia-northeast1:sample=tcp:0.0.0.0:3306", "-credential_file=/secrets/cloudsql/credentials.json"] volumeMounts: - name: cloudsql-instance-credentials mountPath: /secrets/cloudsql readOnly: true volumes: - name: cloudsql-instance-credentials secret: secretName: cloudsql-instance-credentials --- apiVersion: v1 kind: Service metadata: name: sql-proxy spec: selector: app: sql-proxy ports: - name: mysql protocol: TCP port: 3306 targetPort: mysql