まずは開発環境を更新せねばということで諸々アップデートして動作を確認していたのですが、相変わらずVSCode + LLDBという構成だとlibクレートの単体テストをデバッグするときにブレークポイントが効かない。binクレートの単体テストだと効く。なぜか日本語のブログではこの問題に触れているのを見たことがないんですよね(そういう入門記事はだいたいサンプルがbinクレートなので、この問題を踏むのはもっと後のステップではあるんですが)。
私の環境固有の問題かなとも少し思ったんですが、GitHubのIssueを漁ってみると同じ問題を踏んでそうな人をちらほら見かけたので、今回はちょっと時間を使って原因を調べてみることにしました。
問題の再現手順とかワークアラウンドのスクリプトは下記のリポジトリにも置いてあります。
https://github.com/kwhrtsk/rust-lldb-workaround
解決方法だけ知りたい人はVSCode (CodeLLDB) の場合を参照してください。
macOSにおいて、rust-lldb
でlibクレートのテストをデバッグしようとしたときに、ブレークポイントが効きません。VSCodeで CodeLLDB を使ってデバッグした場合も同様です。
【2020-03-06:追記】
masterには修正が入りました。詳細は末尾の追記を参照してください。
直接の原因は、cargo test
に--lib
オプションを付けてテストバイナリをビルドすると、LLDBのデバッグシンボル情報が書かれたファイル(.dSYM
)がtarget/debug/
ディレクトリに作成されないことです。
このため、ブレークポイントとして指定したシンボル情報をLLDBが解決できず、デバッガがブレークポイントで止まりません。
rust-lldb
を使った場合は、後述のようにエラーが表示されるのですが、VSCode上でデバッグしている場合、lldb.verboseLogging
を有効にして出力ペインを注意深く見ていないと気づけないと思います。
Cargo.toml
[package]name = "rust-lldb-workaround"version = "0.1.0"edition = "2018"
src/lib.rs
#[cfg(test)]mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); }}
it_works
にブレークポイントを仕掛けようとすると、下記のように失敗します。
下記のようなシェルスクリプトを実行するとこの様子を確認できます。
#!/bin/bashrm -rf targetbin=$(cargo test --lib --no-run --message-format=json | jq -r '.executable')rust-lldb $bin <<- EOFbreakpoint set --name tests::it_worksEOF
Compiling rust-lldb-workaround v0.1.0 (/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround) Finished test [unoptimized + debuginfo] target(s) in 0.51s(lldb) command script import "/Users/kawahara_taisuke/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_rust_formatters.py"(lldb) type summary add --no-value --python-function lldb_rust_formatters.print_val -x ".*" --category Rust(lldb) type category enable Rust(lldb) target create "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9"Current executable set to '/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9' (x86_64).(lldb) breakpoint set --name tests::it_worksBreakpoint 1: no locations (pending).WARNING: Unable to resolve breakpoint to any actual locations.
binクレートの場合、cargo test
は.dSYM
ファイルをtarget/debug/
ディレクトリに作成します。このファイルは実際にはtarget/debug/deps/
以下のファイルへのシンボリックリンクです。
% cargo test --bins --no-run --message-format=json 2> /dev/null | jq 'select(.target.kind | contains(["bin"])) | .filenames'[ "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-4aae58342b9c3866", "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-4aae58342b9c3866.dSYM"]% ls -l target/debugtotal 1720drwxr-xr-x 2 kawahara_taisuke staff 64 3 3 13:47 builddrwxr-xr-x 8 kawahara_taisuke staff 256 3 3 13:47 depsdrwxr-xr-x 2 kawahara_taisuke staff 64 3 3 13:47 examplesdrwxr-xr-x 4 kawahara_taisuke staff 128 3 3 13:47 incremental-rwxr-xr-x 2 kawahara_taisuke staff 874952 3 3 13:47 rust_lldb_workaround-4aae58342b9c3866-rw-r--r-- 1 kawahara_taisuke staff 282 3 3 13:47 rust_lldb_workaround-4aae58342b9c3866.dlrwxr-xr-x 1 kawahara_taisuke staff 47 3 3 13:47 rust_lldb_workaround-4aae58342b9c3866.dSYM -> deps/rust_lldb_workaround-4aae58342b9c3866.dSYM
しかし、libクレートを対象とするとこのシンボリックリンクが作成されません。rust-lldb
及びVSCodeのデバッガ(CodeLLDB)でブレークポイントが効かないのは直接的にはこれが原因です。
% cargo test --lib --no-run --message-format=json 2> /dev/null | jq 'select(.target.kind | contains(["lib"])) | .filenames'[ "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9"]% ls -l target/debugtotal 1720drwxr-xr-x 2 kawahara_taisuke staff 64 3 3 13:49 builddrwxr-xr-x 5 kawahara_taisuke staff 160 3 3 13:49 depsdrwxr-xr-x 2 kawahara_taisuke staff 64 3 3 13:49 examplesdrwxr-xr-x 3 kawahara_taisuke staff 96 3 3 13:49 incremental-rwxr-xr-x 2 kawahara_taisuke staff 874968 3 3 13:49 rust_lldb_workaround-6e0ca18365abb7b9-rw-r--r-- 1 kawahara_taisuke staff 201 3 3 13:49 rust_lldb_workaround-6e0ca18365abb7b9.d% ls -l target/debug/depstotal 1720-rwxr-xr-x 2 kawahara_taisuke staff 874968 3 3 13:49 rust_lldb_workaround-6e0ca18365abb7b9-rw-r--r-- 1 kawahara_taisuke staff 290 3 3 13:49 rust_lldb_workaround-6e0ca18365abb7b9.ddrwxr-xr-x 3 kawahara_taisuke staff 96 3 3 13:49 rust_lldb_workaround-6e0ca18365abb7b9.dSYM
cargo
の実装を見る限りこの挙動は意図したもので、テストの場合はdeps
の下を直接実行してもらう想定だったようです。(参考)
対処法としては次の方法が考えられます。
rust-lldb
でadd-dsym
コマンドを実行する。.dSYM
へのリンクをtarget/debug/
以下に作る。cargo test --message-format=json
の出力するexecutable
をdeps
の下に変更する。1の方法はCodeLLDBなど使用するVSCode拡張全てに対処が必要で、あまり筋がよくないと思ったので除外しました。
2と3は根本的に解決しようとすると両方ともcargo
の動作を変える必要があるのですが、2についてはcargo test
とrust-lldb
の間にシンボリックリンクを作る処理を挟めば良いので簡単そうです。
この記事では2の方法でのワークアラウンドを示します。
rust-lldb
コマンドを直接実行する場合先ほどのシェルスクリプトを下記のように修正します。
#!/bin/bashrm -rf targetbin=$(cargo test --lib --no-run --message-format=json | jq -r '.executable')# .dSYMディレクトリへのシンボリックリンクを作成(cd target/debug && for d in deps/*.dSYM; do ln -sf $d ./; done)rust-lldb $bin <<- EOFbreakpoint set --name tests::it_worksEOF
ブレークポイントの設置が成功するようになります。
Compiling rust-lldb-workaround v0.1.0 (/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround) Finished test [unoptimized + debuginfo] target(s) in 0.53s(lldb) command script import "/Users/kawahara_taisuke/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_rust_formatters.py"(lldb) type summary add --no-value --python-function lldb_rust_formatters.print_val -x ".*" --category Rust(lldb) type category enable Rust(lldb) target create "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9"Current executable set to '/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9' (x86_64).(lldb) breakpoint set --name tests::it_worksBreakpoint 1: where = rust_lldb_workaround-6e0ca18365abb7b9`rust_lldb_workaround::tests::it_works::h666f078a6b384dfd + 18 at lib.rs:7:8, address = 0x0000000100000cd2
launch.json
と tasks.json
を修正して、cargo test
の直後に.dSYM
のシンボリックリンクを自動で作成するようにします。
.vscode/launch.json
preLaunchTask
を追加した以外はVSCodeからLLVM
テンプレートで作ったものそのままです。
{ "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "Debug unit tests in library 'rust-lldb-workaround'", "cargo": { "args": [ "test", "--no-run", "--lib", "--package=rust-lldb-workaround" ], "filter": { "name": "rust-lldb-workaround", "kind": "lib" } }, "args": [], "cwd": "${workspaceFolder}", // この行を追加 "preLaunchTask": "symlink dSYM" } ]}
.vscode/tasks.json
launch.json
のpreLaunchTask
で参照されているタスクの定義です。このタスクでは、target/debug
ディレクトリに移動してtarget/debug/deps/
ディレクトリ以下の全ての*.dSYM
のシンボリックリンクを作成します。
{ "version": "2.0.0", "tasks": [ { "label": "symlink dSYM", "type": "shell", "command": "sh", "args": [ "-c", "cd ${workspaceFolder}/target/debug; for d in deps/*.dSYM; do ln -fs $d ./; done" ] } ]}
なお Rust Test Lens を使う場合は、上記のタスクを自動で実行することができません。この場合は事前に一度 cargo test --lib --no-run
を実行した後、手動で上記のタスクを実行して.dSYM
のシンボリックリンクを作っておく必要があります。
この問題はrust-lang/cargo#7960で既に起票されているので、将来的には修正されるかもしれません。
【2020-03-06:追記】
rust-lang/cargo#7965 で修正され、masterにもマージされました。
この修正は2020年4月23日リリース予定の Cargo 1.43 に反映される見込みです。
はじめにHelmについて簡単に紹介した後、前回の Kubernetes応用編で作成したマニフェストをもとにHelmチャートの作り方を説明します。
サンプルコードは全て下記のリポジトリにあります。
https://github.com/kwhrtsk/rails-k8s-demoapp
まずサンプルアプリのコードをチェックアウトしてminikubeを起動してください。前回とほぼ同じですが、minikubeのメモリを3GBに増やしています。これは2GBだと後述するSentryの起動に失敗するためです。すでに2GBで作っている場合はminikubeインスタンスを作り直すか、Sentryを使ったhelm
コマンドのチュートリアルを飛ばしてください。Railsのサンプルアプリは2GBでも動作します。
また、今回は初めからIngress
アドオンを有効にしています。minikubeインスタンスを削除した場合は忘れずにもう一度有効にしてください。
# サンプルアプリのコードをチェックアウト$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git$ cd rails-k8s-demoapp# minikubeを起動$ minikube start --cpus=3 --memory=3072 --vm-driver=hyperkit --disk-size=12g# Ingressアドオンをインストール$ minikube addons enable ingress# kubernetesのダッシュボードをオープン$ minikube dashboard
Helm - The Kubernetes Package Manager
アプリケーションに必要なマニフェストファイル一式をパッケージ化して管理するためのツールです。これを使うと必要なコンテナをまとめてk8sにデプロイすることができます。このパッケージはChart
と呼ばれます。
HelmではマニフェストファイルをGoのtext/template形式でテンプレート化することができ、イメージのタグやレプリカの数など、マニフェストの一部をパラメータ化することができます。これらのパラメータは通常デフォルト値を持ち、Chartパッケージをk8sにデプロイ(インストール)する際に上書きできます。
Helmでは公式リポジトリで配布されているChartをk8sクラスタにデプロイすることもできるし、自分で開発しているプライベートなアプリのChartを作成して、Helmでk8sにデプロイすることもできます。
Helmはクライアント・サーバモデルのアプリケーションです。k8sクラスタ側にTillerと呼ばれるサーバモジュールをインストールします。クライアントはhelm
コマンドです。
minikubeの場合は下記のコマンドでTillerをインストールできます。
# インストールが完了するまでブロック$ helm init --wait
Tillerがインストールされているかどうかは下記のコマンドで確認できます。
$ helm versionClient: &version.Version{SemVer:"v2.9.1", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}Server: &version.Version{SemVer:"v2.9.1", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}
アンインストールは下記のコマンドです。
$ helm reset
この章ではSentry というWebアプリを題材に、Chartを使ってアプリケーションをminikubeにデプロイするデモをします。
Sentryはアプリケーションのエラー情報を収集してSlackなどに通知するためのサービスです。SentryのWebサービスはPythonで実装されていますが、Ruby用のクライアントもgemとして配布されており、Railsにも簡単に組み込むことができます。
SaaSを利用することもできるのですが、公式のDockerイメージが配布されており、Dockerを使って自前で運用することもできます。
Sentryサービスの内部構成はPythonのWebアプリ、バックグラウンドジョブのためのワーカー、PostgreSQLサーバ、Redisサーバとそれなりに複雑です。ただ、Helm Chartを使えば比較的簡単にセットアップすることが可能です。
この章ではhelm
コマンドのチュートリアルとしてminikube上にSentryをセットアップする手順を示します。
公式のChartリポジトリはここです。
https://github.com/kubernetes/charts
このリポジトリはstableとincubatorの二つに別れています。
helm
コマンドはローカルにこのリポジトリの情報をキャッシュしています。下記のコマンドでこのキャッシュを更新することができます。
$ helm repo update...Skip local chart repository...Successfully got an update from the "stable" chart repository...Successfully got an update from the "jenkins-x" chart repositoryUpdate Complete. ⎈ Happy Helming!⎈
helm search
コマンドでstableのChartを検索することができます。
$ helm search sentryNAME CHART VERSION APP VERSION DESCRIPTIONstable/sentry 0.1.14 8.17 Sentry is a cross-platform crash reporting and ...
少しややこしいですが、CHART VERSIONはマニフェストやパラメータ定義などこのChart自体のバージョンを示し、APP VERSIONはSentryのバージョンを示しています。
パラメータなしで実行すると全てのChartを表示します。
Chartのメタ情報やパラメータのデフォルト値、READMEなどを表示するコマンドがあります。
$ helm inspect stable/sentry
後述するhelm install
の-f
や--set
オプションで指定できるパラメータの一覧などを確認することができます。
下記のコマンドを実行すると、stable/sentry
というChartをminikubeにデプロイします。
$ helm install stable/sentry \ --name sentry \ --namespace sentry \ --wait \ --timeout 600 \ --version 0.1.14 \ --set service.type=NodePort \ --set ingress.enabled=true \ --set ingress.hostname=$(minikube ip).nip.io \ --set image.tag=8.22 \ --set email.host="" \ --set sentrySecret=sentry \ --set postgresql.postgresPassword=postgres \ --set redis.redisPassword=redis
色々オプションを指定していますが、実際のところはhelm install stable/sentry
だけでも動作します。今回は比較的よく使いそうなオプションをまとめて紹介します。
はじめに、特に重要な--name
オプションについて説明します。k8sにデプロイされたChartのインスタンスはRelease
と呼ばれ、一意な名前を持ちます(Helmでは同じChartのRelease
を二つ以上デプロイすることができます)。この名前を--name
オプションで指定することができます。この例ではsentry
という名前をつけていますが、本来はstaging
やproduction
など環境を示すような名前をつける方が良いかもしれません。
名前を指定しない場合はhappy-panda
のように辞書から単語を二つ適当に拾ってきてくっつけたみたいな名前が自動的に与えられます。helm upgrade
やhelm delete
など、特定のRelease
を対象とするコマンドはこの名前で対象を指定するので、できるだけわかりやすい名前を自分でつけるのがおすすめです。
その他のオプションの意味は次の通りです。
--namespace
オプションでは、このChartのAPIオブジェクトを作成するNamespace
をsentry
にしています。Namespace
については後述します。--wait
オプションをつけると、各オブジェクトの状態が準備完了になるまで待機してからコマンドを終了します。--timeout
オプションは--wait
のタイムアウト値(秒)です。筆者の環境ではstable/sentry
の初期化に5分強かかるので、10分に拡張しています。--version
オプションはインストールするChartのバージョンです。指定しないと最新版がインストールされます。--set
オプションはChartに定義されているパラメータを入力しています。この例では次のパラメータの値をデフォルトから変更しています。service.type
: Service
オブジェクトのタイプです。デフォルトはLoadBalancer
なのですが、minikubeが対応していないためNodePort
に変更しています。--wait
オプションをつけている場合、LoadBalancer
のままだとService
オブジェクトの状態が準備完了にならず、コマンドがタイムアウトして失敗します。ingress.enabled
: デフォルトではIngress
を使わない設定になっていますが、この例では有効にしています。ingress.hostname
: minikube ip
とワイルドカードDNSを組み合わせたURLにしています。image.tag
: sentry
イメージのタグです。Sentry自体のバージョンを意味します。デフォルトは8.17ですが、新しいバージョンが出ているので8.22に変更しています。email.host
: 招待メールやレポートなどメールを送信するためのSMTPサーバを指定します。空文字を指定するとメール送信機能が無効化されます。sentrySecret
: Sentry内部での暗号処理の鍵です。省略するとランダムな文字列が自動的に割り当てられます。*2postgres.postgresPassword
: PostgreSQLサーバのパスワードです。省略するとランダムな文字列が自動的に割り当てられます。*2redis.redisPassword
: Redisサーバのパスワードです。省略するとランダムな文字列が自動的に割り当てられます。*2*2: これらのパラメータを省略してランダム文字列にした場合、email.host
のような別のパラメータを更新した場合にもランダム文字列が再生成されてDBに接続できなくなるなどの問題が起きるため、継続的に運用する予定なら明示的に設定しておくのが無難です。
--set
では見ての通り複数のパラメータを指定できます。パラメータの一覧を書いたYAMLファイルを-f
オプションに渡すことでも同様のことができます。
# values-custom.yamlservice: type: NodePortingress: enabled: trueimage: tag: 8.22email: host: ""sentrySecret: sentrypostgresql: postgresPassword: postgresredis: redisPassword: redis
上記のようなYAMLファイルを用意して、-f
オプションに渡します。この例では、ingress.hostname
のみminikube ip
コマンドの出力を使いたいので--set
で指定しています。
$ helm install stable/sentry \ --name sentry \ --namespace sentry \ --wait \ --timeout 600 \ --version 0.1.14 \ --f values-custom.yaml \ --set ingress.hostname=$(minikube ip).nip.io
このChartのソースコードは下記にあります。他にどのようなパラメータがあるかなどはここを参照してください。
kubernetes/charts | stable/sentry
結構時間がかかるので、別の端末にログを表示して進捗を確認できるようにしておくと良いでしょう。
$ stern --namespace sentry ".*"
完了すると下記のようなメッセージが表示されます。
NAME: sentryLAST DEPLOYED: Tue May 22 00:07:12 2018NAMESPACE: sentrySTATUS: DEPLOYEDRESOURCES:==> v1/SecretNAME TYPE DATA AGEsentry-postgresql Opaque 1 5msentry-redis Opaque 1 5msentry-sentry Opaque 3 5m==> v1/PersistentVolumeClaimNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEsentry-postgresql Bound pvc-a3af7917-5d08-11e8-9576-9a07dd1ebfb4 8Gi RWO standard 5msentry-redis Bound pvc-a3b0850d-5d08-11e8-9576-9a07dd1ebfb4 8Gi RWO standard 5msentry-sentry Bound pvc-a3b159e9-5d08-11e8-9576-9a07dd1ebfb4 10Gi RWO standard 5m==> v1/ServiceNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEsentry-postgresql ClusterIP 10.106.179.194 <none> 5432/TCP 5msentry-redis ClusterIP 10.104.155.56 <none> 6379/TCP 5msentry-sentry NodePort 10.101.218.50 <none> 9000:31416/TCP 5m==> v1beta1/DeploymentNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGEsentry-postgresql 1 1 1 1 5msentry-redis 1 1 1 1 5msentry-sentry-cron 1 1 1 1 5msentry-sentry-web 1 1 1 1 5msentry-sentry-worker 2 2 2 2 5m==> v1beta1/IngressNAME HOSTS ADDRESS PORTS AGEsentry-sentry 192.168.64.29.nip.io 192.168.64.29 80 5m==> v1/Pod(related)NAME READY STATUS RESTARTS AGEsentry-postgresql-5795779885-hh88k 1/1 Running 0 5msentry-redis-6f8d889d4d-r5wbh 1/1 Running 0 5msentry-sentry-cron-8578df4d69-2kzd9 1/1 Running 0 5msentry-sentry-web-6b857b74c8-96fsq 1/1 Running 1 5msentry-sentry-worker-66cf9bb6db-2qzkh 1/1 Running 0 5msentry-sentry-worker-66cf9bb6db-x9plg 1/1 Running 0 5mNOTES:1. Get the application URL by running these commands: export NODE_PORT=$(kubectl get --namespace sentry -o jsonpath="{.spec.ports[0].nodePort}" services sentry-sentry) export NODE_IP=$(kubectl get nodes --namespace sentry -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT/auth/login/sentry2. Log in with USER: admin@sentry.local Get login password with kubectl get secret --namespace sentry sentry-sentry -o jsonpath="{.data.user-password}" | base64 --decode
作成されたAPIオブジェクトのレポートと、Chart側で設定された初期メッセージがNOTES
以下に表示されています。
1. Get the application URL...
にはログインURLの取得方法が書いてありますが、これはNodePort
を使う場合のものです。今回はパラメータでIngress
を有効にしているので、ログインURLは open http://$(minikube ip).nip.io/
でOKです。2. Log in with
手順に従って初期ユーザのパスワードを取得し、Sentryの管理画面にログインできることを確認してください。ユーザ名は admin@sentry.local
で、パスワードは上記のkubectl get secret
コマンドを実行して表示されるテキストです。最後に%
が化けて表示されるようですが、その前までのテキストを入力してください。
公式のChartはだいたいインストール後にこのようなヘルプメッセージが表示されます。このメッセージもGoのテンプレート構文で記述されています。
kubernetes/charts | stable/sentry/templates/NOTES.txt
k8sクラスタにインストール済みのReleaseの一覧は下記のコマンドで確認できます。
$ helm listNAME REVISION UPDATED STATUS CHART NAMESPACEsentry 1 Tue May 22 00:07:12 2018 DEPLOYED sentry-0.1.14 sentry
Release
名を指定して、
$ helm upgrade sentry
作成したReleaseによって作られたAPIオブジェクトを削除する場合は、下記のコマンドを実行します。
$ helm delete --purge sentry
パラメータにはReleaseの名前を指定します。--purge
オプションをつけるとRelease自体も削除します。このオプションをつけない場合、APIオブジェクトだけが削除され、Releaseは削除されずにHelmの管理DBに残る点に注意してください。purgeするまでその名前のReleaseを新しく作ることはできません。
なお、stable/sentry
の場合は少し厄介で、READMEにも書かれている通りhelm delete
しただけではJob
のオブジェクトが残ります。
$ kubectl get all --namespace sentryNAME READY STATUS RESTARTS AGEpod/sentry-db-init-9mnqq 0/1 Completed 0 22hpod/sentry-db-init-ckgzj 0/1 Error 0 22hpod/sentry-user-create-8dgbj 0/1 Completed 0 22hNAME DESIRED SUCCESSFUL AGEjob.batch/sentry-db-init 1 1 22hjob.batch/sentry-user-create 1 1 22h
これらのオブジェクトは下記のように個別に削除する必要があります。
$ kubectl delete job/sentry-db-init job/sentry-user-create
sentry-db-init
のsentry
部分はRelease
の名前です。別の名前でhelm install
するとここも変わるので注意してください。後述するようにネームスペースごと削除してしまう方が簡単です。
$ kubectl delete ns sentry
k8sにはNamespace
というオブジェクトがあり、Deployment
やService
など主要なオブジェクトは基本的にいずれかのNamespace
に属しています。前回の記事まで全くNamespace
を指定してこなかったのですが、ずっとdefault
という組み込みのNamespace
が操作の対象になっていました。
# defaultネームスペースのPodのリストを表示$ kubectl get pods# sentryネームスペースのPodのリストを表示$ kubectl -n sentry get pods
また、これまでのサンプルではService
のエンドポイントにアクセスする際にはそのサービスの名前をDNS名として指定しました。同一のネームスペース内のオブジェクトから接続する場合はそれで良いのですが、異なるネームスペースのオブジェクトから接続する場合には${サービス名}.${ネームスペース名}
のようにドメインを追加する必要があります。
ところで、default
以外のネームスペースを主に使う場合に、kubectl
のパラメータにその都度-n
オプションを指定するのは面倒です。
kubectl
コマンドにはContext
という概念があり、コマンド側で接続先のk8sクラスタやデフォルトのネームスペースを記憶しています。
Context
という単位で保存Context
は複数保存でき、有効になっているのはそのうちの一つ(CURRENT
)Context
を切り替えることで接続先クラスタやデフォルトのネームスペースを変更可能これらの設定は ~/.kube/config
に記録されています。
minikubeしか使っていない場合は下記のようになっていると思います。
$ kubectl config get-contextsCURRENT NAME CLUSTER AUTHINFO NAMESPACE* minikube minikube minikube
$ kubectl config current-contextminikube
~/.kube/config
の中身を表示しているだけです。
$ kubectl config viewapiVersion: v1clusters:- cluster: certificate-authority: /Users/kawahara_taisuke/.minikube/ca.crt server: https://192.168.64.29:8443 name: minikubecontexts:- context: cluster: minikube user: minikube name: minikubecurrent-context: minikubekind: Configpreferences: {}users:- name: minikube user: client-certificate: /Users/kawahara_taisuke/.minikube/client.crt client-key: /Users/kawahara_taisuke/.minikube/client.key
現在のContext
のネームスペースをsentry
に変更するにはこうします。
$ kubectl config set-context $(kubectl config current-context) --namespace=sentryContext "minikube" modified.# 変わっていることを確認$ kubectl config get-contextsCURRENT NAME CLUSTER AUTHINFO NAMESPACE* minikube minikube minikube sentry
無指定(default)に戻す場合はこうします。
$ kubectl config unset contexts.minikube.namespaceProperty "contexts.minikube.namespace" unset.# 戻っていることを確認$ kubectl config get-contextsCURRENT NAME CLUSTER AUTHINFO NAMESPACE* minikube minikube minikube
存在しない名前を指定すると新しいContext
を作ります。次の例はクラスタがminikubeでネームスペースがsentryであるContext
の例です。
$ kubectl config set-context minikube-sentry --cluster=minikube --user=minikube --namespace=sentry$ kubectl config get-contextsCURRENT NAME CLUSTER AUTHINFO NAMESPACE* minikube minikube minikube minikube-sentry minikube minikube sentry
$ kubectl config use-context minikube-sentry% kubectl config get-contextsCURRENT NAME CLUSTER AUTHINFO NAMESPACE minikube minikube minikube* minikube-sentry minikube minikube sentry
指定したコンテキストを削除できます。
$ kubectl config delete-context minikube-sentry
Context
とネームスペースの切り替えは見ての通り煩雑です。kubectxとkubensというツールを使えばもっと簡単に切り替えの操作を行うことができます。
macOSの場合はHomebrewでインストール可能です。
$ brew install kubectx
インストール時に--with-short-name
オプションをつけると、コマンド名をkctx
とkns
に変更できます。kubectl
との前方一致部分が減るのでシェルでの補完が少し楽になります。
kctx(kubectx)
はContext
の一覧表示と切り替え、削除、リネームが可能です。
$ kctx # パラメータ無しだとContext一覧を表示(有効なものは黄色で表示)$ kctx minikube-sentry # 指定したContextに切り替え(Tabで補完が効く)$ kctx - # 一つ前のContextに戻る$ kctx -d minikube-sentry # Contextを削除$ kctx sentry=minikube-sentry # minikube-sentry を sentry にリネーム$ kctx sentry=. # 現在のContextを sentry にリネーム
kns(kubens)
は現在のContext
に対して、ネームスペースの一覧表示と切り替えが可能です。
$ kns # ネームスペースの一覧表示(現在のネームスペースを黄色で表示)$ kns sentry # 指定したネームスペースをデフォルトに変更(Tabで補完が効く)$ kns - # 一つ前のネームスペースに変更
なお、kubectx(kctx)
ではContext
の作成や削除はできませんのでkubectl
コマンドを使う必要があります。ただ、知っての通りminikubeの場合はminikube start
の際に自動的にContext
が作成されますし、GCPのGKEの場合も指定したGKEクラスタに接続するためのContext
を作成する機能がgcloud
コマンドに組み込まれています。
gcloud container clusters get-credentials | Cloud SDK | Google Cloud
minikubeやGKEを使っている限りではContext
を作成したり編集したりする機会はそれほど多くないと思いますので、kctx
とkns
の使い方を覚えておけばほとんどの場合十分です。
helm install
で--namespace
オプションを指定すると、当該ネームスペースがない場合は自動的に作成されます。このネームスペースはhelm delete
しても残ります。
ネームスペースを削除する場合にはkubectl delete
を使います。
$ kubectl delete namespace sentry# または$ kubectl delete ns sentry
ネームスペース内のオブジェクトが全て削除される点に注意してください。
永続ボリュームも確認無しで削除されるため、事故によるデータロストに十分注意する必要があります。PV
オブジェクトが削除された場合に、データの実体(GCPのPersistentDiskやAWSのEBSなど)が削除されず残るようにするためには、StorageClass
のreclaimPolicyをRetain
に変更しておく必要があります。
前節では既存のパッケージを使う方法を見てきました。今節では自分でパッケージを作り、k8sクラスタにデプロイする方法を見ていきます。
具体的には、第2回で取り上げたサンプルアプリのHelm Chartを作成します。構成はStep4のマニフェストファイルと同等にします。
最終的なコードは下記に置いてあります。
なお、このドキュメントでは作ったHelm Chartをアプリのパッケージとして公開するというより、プライベートなアプリのk8sマニフェストをHelmで管理できるようにChart化する、という使い方を想定しています。
このような使い方においては、作成したChartをリポジトリサーバに登録する必要はなく、helm
コマンドでローカルFS上のChartディレクトリを直接指定することでChartのデプロイを行うことができます。
サンプルコードのChartをデプロイしてみましょう。
もしもまだであれば、準備の節を参照して、サンプルコードのチェックアウトとminikubeの起動を済ませて置いてください。
次に、サンプルアプリのイメージをminikube上でビルドしておいてください。
$ cd rails-k8s-demoapp$ (eval $(minikube docker-env) && $ docker build . -t demoapp:0.0.1)
また、Step4と同じくIngress
のTLS証明書用のSecret
オブジェクトはkubectl
コマンドで作成します。証明書と鍵の内容をChartのパラメータ化することも不可能ではないのですが煩雑なので、公式リポジトリのChartもSecret
オブジェクトの名前だけをパラメータ化しているものが多いようです。今回はそれにならいます。
ちょっと手を抜いてMakefileを利用して作成します。手順はStep4の時と同じなので詳細は前回の記事を参照してください。
cd k8s/chart$ make kubectl-create-secret-tls
Tiller(Helmのサーバモジュール)のインストールがまだであれば下記を実行します。
$ helm init --wait
次に、helm upgrade
コマンドでChartをminikubeにデプロイします。
$ helm upgrade staging . --install --wait \ --set ingress.host=demoapp-puma.$(minikube ip).nip.io
staging
はRelease
の名前です。--install
オプションをつけると、Release
が存在しない場合のみhelm install
のような動作をするようになります。初回と2回目以降でコマンドを分けずに済むので便利です。
--set
では、ingress.host
というパラメータを指定しています。
最後にブラウザでWebアプリを開きます。
$ open https://demoapp-puma.$(minikube ip).nip.io/
Makefileに同様のタスクを定義しているので、下記のコマンドでも同じことができます。
$ make helm-init$ make minikube-docker-build$ make kubectl-create-secret-tls$ make helm-upgrade$ make open# または単に$ make
Release
と証明書の削除は次のコマンドです。
make clean
Chartに含まれるテンプレートが最終的にどのようなYAMLデータになるかを事前に確認するには、helm template
コマンドを使います。
$ cd rails-k8s-demoapp/k8s/chart$ helm template . --name staging
上記のコマンドは、すべてのテンプレートにパラメータを埋め込んだ結果を表示します。
.
はChartディレクトリのパスです。stable/sentry
のようにリポジトリのChartを指定することもできます。--name
オプションはRelease
の名前です。この他に、--set
や-f
でパラメータを指定することもできます。レンダリングの際はそれらの値を埋め込んだ結果が表示されます。また、特定のテンプレートのみレンダリングしたい場合は-x
オプションを指定します。
$ helm template . --name staging -x templates/puma-deploy.yaml
-x
オプションの値は、カレントディレクトリからの相対パスではなく、Chartのルートディレクトリからの相対パスを指定する必要がある点に注意してください。
demoapp
という名前のChartを作るには下記のようにします。
$ cd rails-k8s-demoapp/k8s$ helm create demoapp
次のような構成のディレクトリが作られます。
demoapp/ Chart.yaml values.yaml charts/ templates/ NOTES.txt _helpers.tpl deployment.yaml ingress.yaml service.yaml
作成後、demoapp
ディレクトリはリネームしても問題ありません。
Chart.yaml
には、Chartの名前やバージョンなどの情報が記述されます。values.yaml
には、Chartのパラメータのデフォルト値が記述されます。charts/
には、このChartが依存している別のChartを保存します。今回は扱いません。templates/
には、次のファイルを保存します。NOTES.txt
は、Chartのインストール後に表示するメッセージです。_helpers.tpl
には、テンプレート全般で再利用するための部分テンプレートを書きます。*.yaml
がマニフェストのテンプレートです。helm create
で作成した直後のChartは、Deployment
とService
を一つずつ作成して、Ingress
経由でそれを外部に公開するというk8sの基本構成が記述されています。なので、それらを一旦削除します。
rm demoapp/values.yaml demoapp/templates/*.yaml
次に、Step4のマニフェスト一式をdemoapp/templates/
以下にコピーします。
cp manifests-step4/*.yaml demoapp/templates/
さて、これでおおよそ動くChartができました。Ingress
以外はこのままでも動作します *1。demoapp/templates/
に置くのはテンプレートなので、テンプレートとしての構文を一切使わないYAMLのままでもとりあえず動くのです。マニフェストファイルが存在するプロジェクトをHelm Chart化する場合には、このように一旦templates/
ディレクトリに置いてから各マニフェストファイルをテンプレート構文で書き換えていくことになります。
そうやって作成したChartが下記のパスにあります。
次節以降では、このChartの中のテンプレートとStep4のマニフェストを比較しつつ、Chartの書き方を解説します。
*1: Ingress
については、ホスト名を示すパラメータが$(minikube ip)
というコマンドで取得するまでわからないため、下記のように元々sed
で${MINIKUBE_IP}
を書き換えてからkubectl apply -f -
に流し込む前提のマニフェストでした。Chart化する際には、テンプレートの機能を使ってパラメータとしてこのホスト名を指定できるようにします。
# k8s/manifests-step4/puma-ing.yaml---apiVersion: extensions/v1beta1kind: Ingressmetadata: name: demoapp-puma # 省略spec: tls: - hosts: - demoapp-puma.${MINIKUBE_IP}.nip.io secretName: demoapp-puma-tls rules: - host: demoapp-puma.${MINIKUBE_IP}.nip.io # 省略
export MINIKUBE_IP=$(minikube ip)cat *.yaml | sed s/\${MINIKUBE_IP}/$(MINIKUBE_IP)/ | kubectl apply --record -f -
詳細な説明は 前回の記事を参照してください。
Helm Chartのテンプレート構文は、ほぼGo言語のtext/templateです。ただし、下記のように拡張されています。
はじめに、Step4時点でのpuma
のDeployment
を例に、Chartのテンプレート化の勘所とテンプレート記法の基本を説明します。
元になるマニフェストは下記です。
# k8s/manifests-step4/puma-deploy.yaml---apiVersion: apps/v1kind: Deploymentmetadata: name: demoapp-puma labels: app: demoapp component: pumaspec: replicas: 2 selector: matchLabels: app: demoapp component: puma template: metadata: labels: app: demoapp component: puma spec: restartPolicy: Always containers: - name: puma image: demoapp:0.0.1 imagePullPolicy: IfNotPresent command: - ./bin/start-puma livenessProbe: httpGet: path: /health_check/full port: 3000 initialDelaySeconds: 30 readinessProbe: httpGet: path: /health_check/full port: 3000 initialDelaySeconds: 30 envFrom: - configMapRef: name: demoapp-rails-env - secretRef: name: demoapp-rails-env
これをテンプレート化したものが下記です。
# k8s/chart/templates/puma-deploy.yaml---apiVersion: apps/v1kind: Deploymentmetadata: name: {{ template "demoapp.puma.name" . }} labels: app: {{ template "demoapp.name" . }} chart: {{ template "demoapp.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} component: pumaspec: replicas: {{ .Values.puma.replicas }} selector: matchLabels: app: {{ template "demoapp.name" . }} release: {{ .Release.Name }} component: puma template: metadata: labels: app: {{ template "demoapp.name" . }} release: {{ .Release.Name }} component: puma annotations: checksum/rails-env-cm: {{ include (print $.Template.BasePath "/rails-env-cm.yaml") . | sha256sum }} checksum/rails-env-secret: {{ include (print $.Template.BasePath "/rails-env-secret.yaml") . | sha256sum }} spec: restartPolicy: Always containers: - name: puma image: {{ .Values.rails.image.repository }}:{{ .Values.rails.image.tag }} imagePullPolicy: IfNotPresent command: - ./bin/start-puma livenessProbe: httpGet: path: /health_check/full port: 3000 initialDelaySeconds: 30 readinessProbe: httpGet: path: /health_check/full port: 3000 initialDelaySeconds: 30 envFrom: - configMapRef: name: {{ template "demoapp.rails-env.name" . }} - secretRef: name: {{ template "demoapp.rails-env.name" . }}
このようにテンプレートでは{{ }}
の中にコード片を埋め込んでいきます。
いっぱい差分があるように見えますが、書き換えられているものは次の四つです。
次節以降で、一つずつ詳細を見ていきます。
Chart版のマニフェストでは、レプリカの数とイメージの名前がパラメータ化されています。Step4のマニフェストとChartのテンプレートから関連する部分だけを抜き出して比較します。
# before: k8s/manifests-step4/puma-deploy.yamlspec: replicas: 2 template: spec: containers: - name: puma image: demoapp:0.0.1# after: k8s/chart/templates/puma-deploy.yamlspec: replicas: {{ .Values.puma.replicas }} template: spec: containers: - name: puma image: {{ .Values.rails.image.repository }}:{{ .Values.rails.image.tag }}
Values
という組み込みオブジェクトの値を参照しています。これはChartの利用者側で任意に変更可能なパラメータを管理するオブジェクトです。パラメータのデフォルト値はChartディレクトリのvalues.yaml
ファイルに記述します。この値はhelm install
またはhelm upgrade
の際に--set
オプションや-f
オプションで上書きすることができます。(詳細は前半のSentryのデモを参照してください)
サンプルコードのvalues.yaml
から今回関連のある部分だけを以下に抜き出しています。
puma: replicas: 2rails: image: repository: demoapp tag: 0.0.1
このYAMLデータの中身を.Values.puma.replicas
のように.
区切りで指定することができます。
次に、オブジェクトの名前に関する部分だけを抜き出して比較します。
# before: k8s/manifests-step4/puma-deploy.yamlmetadata: name: demoapp-pumaspec: template: spec: containers: - name: puma envFrom: - configMapRef: name: demoapp-rails-env - secretRef: name: demoapp-rails-env# after: k8s/chart/templates/puma-deploy.yamlmetadata: name: {{ template "demoapp.puma.name" . }}spec: template: spec: containers: - name: puma envFrom: - configMapRef: name: {{ template "demoapp.rails-env.name" . }} - secretRef: name: {{ template "demoapp.rails-env.name" . }}
template
というのは、定義済みの部分テンプレートを呼び出すAction
です。Helmの場合、templates/_helpers.tpl
でdefine
により定義した値を埋め込むことになります。この例では、demoapp.puma.name
という定義を参照しています。
以下、templates/_helpers.tpl
から関連のある部分だけ抜き出します。
{{/* k8s/chart/templates/_helpers.tpl */}}{{- define "demoapp.fullname" -}}{{- if .Values.fullnameOverride -}}{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}{{- else -}}{{- $name := default .Chart.Name .Values.nameOverride -}}{{- if contains $name .Release.Name -}}{{- .Release.Name | trunc 63 | trimSuffix "-" -}}{{- else -}}{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}{{- end -}}{{- end -}}{{- end -}}{{- define "demoapp.puma.name" -}}{{- include "demoapp.fullname" . -}}-puma{{- end -}}{{- define "demoapp.rails-env.name" -}}{{- include "demoapp.fullname" . -}}-rails-env{{- end -}}
demoapp.fullname
、demoapp.puma.name
, demoapp.rials-env.name
の三つのdefine
があります。前者はhelm create
した時から存在するもので、残り二つは筆者が追加したものです。
結構ボリュームがあるのですが、ほとんどの状況では次のようなシンプルな結果になります。
demoapp.fullname
は、Chart.yamlに書かれたname
フィールドの値にRelease
の名前をプレフィクスとしてつけた文字列を返します。サンプルの場合は、Release
の名前がstaging
ならstaging-demoapp
になります。demoapp.puma.name
は、Release
がstaging
の場合はstaging-demoapp-puma
になります。include
は、define
定義の中でさらに別の部分テンプレートを参照する時に使います。demoapp.rails-env.name
は、Release
がstaging
の場合はstaging-demoapp-rails-env
になります。つまりChart化した後のオブジェクト名には、元々のマニフェストでの名前であるdemoapp-puma
にプレフィクスとしてRelease
の名前をつけていることになります。Helm Chartでは、このようにオブジェクトの名前にRelease
の名前をプレフィクスとしてつけるのが慣例です。
なお、define
を追加せずdemoapp.fullname
だけを使って下記のように書くことも可能です。
metadata: name: {{ template "demoapp.fullname" . }}-pumaspec: template: spec: containers: - name: puma envFrom: - configMapRef: name: {{ template "demoapp.fullname" . }}-rails-env - secretRef: name: {{ template "demoapp.fullname" . }}-rails-env
公式のHelmチャートにはこのように書かれているものも多いですが、typoに気づきにくいという問題があるので、このサンプルでは前記のようにdefine
して共通化しています。
ところで、template
関数の呼び出しの際には名前を示す一つ目のパラメータの後ろに.
がありますが、これはカレントスコープを渡していることを示しています。テンプレート定義の内部で.Values
や.Release
などの組み込みオブジェクトを参照する場合には、呼び出し時にスコープを渡す必要がある点に注意してください。これはinclude
も同様です。
SETTING THE SCOPE OF A TEMPLATE
次に、ラベルに関連するところだけを抜き出して比較します。
# before: k8s/manifests-step4/puma-deploy.yamlmetadata: labels: app: demoapp component: pumaspec: selector: matchLabels: app: demoapp component: puma# after: k8s/chart/templates/puma-deploy.yamlmetadata: labels: app: {{ template "demoapp.name" . }} chart: {{ template "demoapp.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} component: pumaspec: selector: matchLabels: app: {{ template "demoapp.name" . }} release: {{ .Release.Name }} component: puma
元々app
とcomponent
だったところに、chart
、release
、heritage
が追加されています。これらのラベルは、Helmの公式ドキュメントでBest Practiceのページに標準的なラベルとして定義されているものです。
STANDARD LABELS - Chart Best Practices
chart
はChartの名前、release
はRelease
の名前を示すラベルです。heritage
は常にTiller
という文字列になります。これはこのオブジェクトがHelm(Tiller)によって管理されているということを示します。
release
のみ、.spec.selector.matchLabels
にも追加されている点に注意してください。これは同一のChartが同じNamespace
内に複数のRelease
としてデプロイされた場合に、適切に分離されるために必要です。chart
とheritage
は動作に直接影響しませんが、慣例として推奨されているラベルです。
最後にアノテーションについて説明します。
下記のように、マニフェストにはなかった.spec.template.metadata.annotations
エントリがテンプレートには追加されています。
# after: k8s/chart/templates/puma-deploy.yamlspec: template: metadata: annotations: checksum/rails-env-cm: {{ include (print $.Template.BasePath "/rails-env-cm.yaml") . | sha256sum }} checksum/rails-env-secret: {{ include (print $.Template.BasePath "/rails-env-secret.yaml") . | sha256sum }}
アノテーションは、オブジェクトの識別以外に使われるメタ情報です(識別に使うのはラベル)。ラベルと同様にKey-Value形式の構造で、通常はビルドやリリースに関する情報やモニタリングのための情報などを記録します。
ところで、ConfigMap
またはSecret
から環境変数などの設定情報を参照しているDeployment
には、運用上少し面倒な制限があります。それは、ConfigMap
やSecret
の内容を更新したとしても、それを参照するDeployment
はローリングアップデートされるわけではないということです。
一方、Deployment
はこのアノテーションが更新された場合もローリングリスタートが行われます。そこでそれを利用して、ConfigMap
またはSecret
の更新後にkubectl patch
コマンドなどを用いてアノテーションを更新することで強制的にローリングリスタートをかけるというテクニックが知られています。
kubectl patch deploy demoapp-puma -p \ "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"config-updated-at\":\"`date +'%s'`\"}}}}}"
Helmでテンプレート使う場合には、先に挙げた例のようにConfigMap
およびSecret
のマニフェストのダイジェストをアノテーションに含めることで、これらの設定オブジェクトの内容が変わったときに自動的にローリングリスタートがかかるようにできます。このダイジェスト計算はレンダリング後のマニフェストに対して行われるため、例えばrails-env-cm.yaml
に全く変更が無い場合でも、values.yaml
を編集するなどしてrails-env-cm.yaml
内部で参照しているValues
の値が変わればDeployment
はリスタートされます。
Automatically Roll Deployments When ConfigMaps or Secrets change
同様のアノテーションをsidekiq-deploy.yaml
にも追加しています。なお、mysql-deploy.yaml
でもConfigMap
とSecret
を参照していますが、こちらは初回起動時に作成するDBやユーザなどの情報が主で運用中に変更することを想定していないため、アノテーションは追加していません。
関数とパイプラインを使いこなすことでテンプレートはより便利になります。
Secret
オブジェクトの定義では、データの値を次に示すようにBASE64エンコードして記述する必要がありました。
# k8s/manifests-step4/rails-env-secret.yaml---apiVersion: v1kind: Secretmetadata: name: demoapp-rails-envdata: SECRET_KEY_BASE: MTIz # echo -n "123" | base64 MYSQL_PASSWORD: c2VjcmV0 # echo -n "secret" | base64
いちいちエンコードしてからファイルに記述するのは面倒です。
Helmのテンプレートでは先に述べたようにSprigの関数が組み込まれているため、b64enc
という関数を使って次のようにSecret
を定義することができます。
# k8s/chart/templates/rails-env-secret.yaml---apiVersion: v1kind: Secretmetadata: name: {{ template "demoapp.rails-env.name" . }}data: SECRET_KEY_BASE: {{ .Values.env.secret.SECRET_KEY_BASE | b64enc }} MYSQL_PASSWORD: {{ .Values.env.secret.MYSQL_PASSWORD | b64enc }}
# k8s/chart/values.yamlenv: secret: MYSQL_PASSWORD: secret SECRET_KEY_BASE: "123"
{{ }}
の中身はパイプラインであり、|
演算子を使って別の関数のパラメータとすることができます。関数が二つ以上のパラメータをとる場合、|
の後ろの関数には最後のパラメータとして|
の前の結果が渡されます。
比較的よく使われる関数にdefault
とquote
があります。これを使った例を一つ見てみましょう。
{{ .Values.rails.testValue | default "abc" | b64enc | quote }}
default
は、二つ目のパラメータが空の場合は一つ目の値を、空でない場合は二つ目の値を返す関数です。quote
は、パラメータの前後に"
をつけた文字列を返す関数です。上記の例では、.Values.rails.testValue
をBASE64エンコードして"
で囲むという処理です。.Values.rails.testValue
が空だった場合はabc
という文字列を代わりにエンコードしてクォートします。
今回のサンプルでは特に使っていないのですが、テンプレートではif/else
による分岐やrange
によるループを使用できます。
stable/sentry
のingress.yaml
が参考になると思います。
https://github.com/kubernetes/charts/blob/master/stable/sentry/templates/ingress.yaml
ここではvalues.yaml
について説明します。サンプルコードのvalues.yaml
は次のような内容です。
mysql: image: repository: mysql tag: 5.7.21 storage: size: 8Gi className: standardredis: image: repository: redis tag: 4.0.9 storage: size: 8Gi className: standardpuma: replicas: 2sidekiq: replicas: 1 timeout: 60rails: image: repository: demoapp tag: 0.0.1 setupDbTag: 0.0.1ingress: tlsSecretName: demoapp-puma-tls # required from --set parameter # helm upgrade release-name . --set ingress.host=demoapp-puma.${MINIKUBE_IP}.nip.io # host: demoapp-puma.${MINIKUBE_IP}.nip.ioenv: configmap: MYSQL_USER: demoapp MYSQL_DATABASE: demoapp_production RAILS_SERVE_STATIC_FILES: "true" RAILS_LOG_TO_STDOUT: "true" secret: MYSQL_PASSWORD: secret MYSQL_ROOT_PASSWORD: topsecret SECRET_KEY_BASE: "123"
注意が必要なのは下記の点です。
Job
オブジェクトで参照するイメージのタグはDeployment
と分ける必要があります。具体的には.rails.image.tag
と.rails.image.setupDbTag
です。Job
のイメージは変更不可なので、分けておかないとアプリを更新できなくなります。.env.secret
の下にまとめています。こうしておくとsopsやyaml_vaultでファイルの一部だけ暗号化するのが簡単になります。これについては次節で詳細を説明します。参考:
先の例では、DBのパスワードやRailsのSECRET_KEY_BASE
などの秘密情報がvalues.yaml
に平文で書き込まれていました。プライベートなアプリの場合でも、こういった秘密情報は平文のままでリポジトリにコミットするべきではありません。
ここでは、YAMLファイルを暗号化する方法を二つ紹介します。
紹介する方法は、いずれもGCP KMSまたはAWS KMSで暗号鍵を管理できます。今回はGCP KMSを使う方法を紹介します。
Cloud SDKをインストールした後、適当なプロジェクトを作成しください。その後、下記のコマンドを実行して鍵を作成してください。
# ログイン$ gcloud auth login# キーリングの作成$ gcloud kms keyrings create demoapp --location global# キーの作成$ gcloud kms keys create values-key --location global --keyring demoapp --purpose encryption# 作成したキーの確認$ gcloud kms keys list --location global --keyring demoappNAME PURPOSE LABELS PRIMARY_ID PRIMARY_STATEprojects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key ENCRYPT_DECRYPT 1 ENABLED
この例では、プロジェクトIDはrails-k8s-demoapp
です。各自の環境に合わせて読み替えてください。
途中、下記のようなメッセージが出た場合は、ブラウザでリンクを開いてKMSを有効化してください。
ERROR: (gcloud.kms.keyrings.create) FAILED_PRECONDITION: Google Cloud KMS API has not been used in this project before, or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudkms.googleapis.com/overview?project=123456789012 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
使い終わった鍵は忘れずに破棄しておいてください。使っていなくても費用が発生します。
https://cloud.google.com/kms/pricing?hl=ja
また、下記のページを参照してサービスアカウントキーを作成してください。
https://cloud.google.com/docs/authentication/getting-started
役割は、Cloud KMS - クラウドKMS暗号鍵の暗号化/復号化
を選択します。JSON形式でダウンロードした鍵を kms-key.json
という名前で保存してください。
yaml_vaultはruby製の暗号化ツールです。AWS KMSまたはGCP KMSで暗号鍵を管理することができます。
gemでインストールできます。GCP KMSを使う場合は、google-api-clientも必要です。
$ gem install yaml_vault google-api-client
Gemfileに追加しても良いですが、そのままだとDockerイメージのサイズが50MBほど増えるため、少し工夫が必要です。
下記のコマンドを実行すると、values.yaml
を暗号化してencrypted_values.yaml
に出力します。その際、暗号化するキーを--key
オプションで限定している点に注目してください。このオプションを指定しない場合、YAMLの全ての値が暗号化されます。
$ yaml_vault encrypt values.yaml -o encrypted_values.yaml --cryptor=gcp-kms \ --gcp-kms-resource-id=projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key \ --gcp-credential-file=kms-key.json \ --key '$.env.secret'encrypted values.yaml -> encrypted_values.yaml
暗号化されたファイルは次のような内容になります。
# encrypted_values.yamlmysql: image: repository: mysql tag: 5.7.21 storage: size: 8Gi className: standardredis: image: repository: redis tag: 4.0.9 storage: size: 8Gi className: standardpuma: replicas: 2sidekiq: replicas: 1 timeout: 60rails: image: repository: demoapp tag: 0.0.1 setupDbTag: 0.0.1ingress: tlsSecretName: demoapp-puma-tlsenv: configmap: MYSQL_USER: demoapp MYSQL_DATABASE: demoapp_production RAILS_SERVE_STATIC_FILES: "true" RAILS_LOG_TO_STDOUT: "true" secret: MYSQL_PASSWORD: CiQA/TkaHXJd4HsdRnMTJ9tgso7tYdCbIJxuWbGgj5UJfpkbI+gSOADQcvdxFdICOfR8oiQa1GOM9UX0koe0AC9TM5C+fpV1nRuaajJGAF0IfFRf9KtKKHyz/Qp4ad3E MYSQL_ROOT_PASSWORD: CiQA/TkaHbi65E4QSHRKwpxRQcD43rMwo4rr9aUgAj8ezPjgtGcSOwDQcvdxaMRAJ+yE87zeGDIBCbeZVzyZMXAg83Odxj2y9fbL4NELzViD5bqx8XoxWe0Y7nhWYJomNpmC SECRET_KEY_BASE: "CiQA/TkaHfsk/7i7IrlJVhheCPifd0bCvho9CJgRbiR10lnqc1oSMgDQcvdxijXncXs0gYuMfk9/IdeSiL8fApLEDd6zISmgnmZh3d7KxNioeliHZcWApaai"
復号するときは次のようにします。
$ yaml_vault decrypt encrypted_values.yaml -o values.yaml --cryptor=gcp-kms \ --gcp-kms-resource-id=projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key \ --gcp-credential-file=kms-key.json \ --key '$.env.secret'
yaml_vaultを使って運用する場合には、values.yaml
はコミットせず、暗号文のencrypted_values.yaml
をコミットして運用します。
なお、暗号化を行う際には、元の平文に変更がなかったとしても毎回暗号文の部分は値が変わってしまう点に注意が必要です。
sopsはmozilla製の暗号化ツールです。
使用するためには、まずGCPの鍵を環境変数で指定する必要があります。
$ export GOOGLE_APPLICATION_CREDENTIALS=kms-key.json
暗号化は下記のように行います。
$ sops --encrypt --gcp-kms projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key values.yaml > encrypted_values.yaml
ファイル全体が下記のように暗号化されます。
mysql: image: repository: ENC[AES256_GCM,data:eD0o4pw=,iv:3DbUfobOeqAL0kowEKVqbMjtEC88J9/nMysGdMf4nqk=,tag:wEwatUbf9v1/LVPLYkunPA==,type:str] tag: ENC[AES256_GCM,data:PgWECj+7,iv:UlUPjWbxO+lW6bAm/2F5JTfAbbWa3/wCjyOIyGmEG7I=,tag:vPmfEGQkKKxE7nsJsZfYZA==,type:str] storage: size: ENC[AES256_GCM,data:Zn9a,iv:OaFk2rRTiNB3doRZBo5SvL/7j5G1PLKA0H7mBXpa/Ew=,tag:04A9OIJgYlXWOwk35kPHqg==,type:str] className: ENC[AES256_GCM,data:k4J42uu3fiE=,iv:YrXlk57NgadQg9VQaTbTjo8E5nT5XWZXz6UvNXiSO6E=,tag:hCOuXuQBY3rSxzsY4sx+0A==,type:str]redis: image: repository: ENC[AES256_GCM,data:wjqwX64=,iv:hqxguyQImpZFgGz83CyQhfi/tGZbIefTHbpXMiMuL3w=,tag:Y5kdJOxdH4R2HMrYeFv5xA==,type:str] tag: ENC[AES256_GCM,data:tCM+YHQ=,iv:owoFufImNd81KqP5XVvEdA+3ZHpZMYp1Q56pHm7lW/o=,tag:VuiJh+zPegQvjfMMySRWTA==,type:str] storage: size: ENC[AES256_GCM,data:g5n2,iv:AVbPlOkfrBFq9UzuILE6fgt6Xww6311KbyFhXHnpiAk=,tag:S+xMDWCQ3XlT7PCQlB4kiw==,type:str] className: ENC[AES256_GCM,data:yrqW0RSihnc=,iv:O9wpDqHd3zYNdoS8VJI1y2giLnSByaGAvGrDMrF8ba8=,tag:hmE475vdibBfUc8R21AMzg==,type:str]puma: replicas: ENC[AES256_GCM,data:oA==,iv:RkUoYXEivf5vzDrWBD7kWaGtyr1k4lApn1T0qWA67K0=,tag:Z4fPAiGEtfuxxaqQM2nXQw==,type:int]sidekiq: replicas: ENC[AES256_GCM,data:QQ==,iv:NWZPcXVz7tcuXIRftmacJ2SegYfDgh/iwS82ovlYeNA=,tag:J2WSY3vjOkJcNvIJrLFy7w==,type:int] timeout: ENC[AES256_GCM,data:G5s=,iv:t/866qY7jTnGSRUQAhgXWEw9lcdtHll8fYIQsyARo4A=,tag:yZ2cIFXdJO2xis4B9mzWSA==,type:int]rails: image: repository: ENC[AES256_GCM,data:x4dh5BigFg==,iv:C7jm5zl6rr7PeLwgX2i73Yo1yKWp+lv3eA0zCJipuk4=,tag:2FhxjAmnnIYVTsN6WkFoBQ==,type:str] tag: ENC[AES256_GCM,data:WaWw06E=,iv:S8Tx8rQPfzfk00of3HiNJVJYM3qeNQKNxxyHYyvTAFM=,tag:74upYPpg3fMoEya9YFTVog==,type:str] setupDbTag: ENC[AES256_GCM,data:zFo1gJ0=,iv:+P0F1hH5TagxLLe7yqCMAfoLtEsu+s5lLV7WKDJFp7s=,tag:T/Bix+hd1DZBIhA634mVrQ==,type:str]ingress: tlsSecretName: ENC[AES256_GCM,data:sJcZXlktHsdfsYm6BbkGQg==,iv:J+bmgm2qC0wIMSDzXec9xsujOl3kjfFVUv+qmk3FWhs=,tag:qyecdT2dDvsPI+rAGe9+oA==,type:str] #ENC[AES256_GCM,data:H0TCFT5qkWAfK6E+j9sSjudYmCVAKckB7CQxgwbH,iv:kl625mCSUA/NNV35CU+xez9GOFggKnDm5nXYJ9waVMU=,tag:AKdSsCJNcfdC+I3nvXhStw==,type:comment] #ENC[AES256_GCM,data:cGNTwdT+KF0CoUCmNVwLxyvUqNozAZvz0xDWzoEB/qElfEMVRSqdPXMjMjsjEy1Apm0WOBfp5HFgZUFW16Q+vucAhgeFAn0mM7WhADNjKZGZUw==,iv:sMn+N9nHPbWB0Raps0559jxHKDMeo98CfnUIjIvTFjU=,tag:bR8mdcFEf2ZaJ2IdKHp9xg==,type:comment] #ENC[AES256_GCM,data:cl97jwOSlNZmfBAK+GjtzUZgqyABB9bXUN0J+xeFQpc5I3V3Ez2Olrc=,iv:jcrqo86rp+SVSB+lKLee/YdEz+8IfnULzYA+A0vj+SA=,tag:tqSfgPe//SlU0GJSog0PcQ==,type:comment]env: configmap: MYSQL_USER: ENC[AES256_GCM,data:ckkEsrePZg==,iv:ssnCPFBqnihIe5c9T79EJlA2vGWsihOI53RL1SwdklA=,tag:ewyXNB6S6Kd25FIcSGtF4w==,type:str] MYSQL_DATABASE: ENC[AES256_GCM,data:LmgD+B0MF2jaHXiLAbL57UdT,iv:0eivT7Dkkso4zcvbekb0Li5CBGEn4TcaAFzcC7QtvTs=,tag:/k2xCsM+BKkPF540QZNRqA==,type:str] RAILS_SERVE_STATIC_FILES: ENC[AES256_GCM,data:B7YMSg==,iv:XM6zzCChuIPUUYmOog/XvbaC88j+AZtNcfps2/e/2h0=,tag:AK9aHDYW2WaBTITLQuBA6Q==,type:str] RAILS_LOG_TO_STDOUT: ENC[AES256_GCM,data:HV9B/Q==,iv:mSKjUGQSmCXKcCutSmjnenb3lVzVHWEKrTa+N+nBexY=,tag:4bYviLxwkVCZgjjNZ3cvFQ==,type:str] secret: MYSQL_PASSWORD: ENC[AES256_GCM,data:aRQsAyGK,iv:tjhn1oDqbp83vNartgIL4S8SgveNLUL91gjUzSCBuNo=,tag:1PtcSk8gt0XBuboZzE93pQ==,type:str] MYSQL_ROOT_PASSWORD: ENC[AES256_GCM,data:QgUKdkPZ3URM,iv:iQl52u3fruHtoBh6uLCJ0ArzvLHbw7XbNVQcKuBbaVs=,tag:u/V3k0D15Ne0TQLfRcTF/g==,type:str] SECRET_KEY_BASE: ENC[AES256_GCM,data:0Bsu,iv:1MfHBuEa2fF84H+IJXHeufBWH+dJWGEE4zJqts3EJrw=,tag:VAgJgAS+xlgAKnPeff1eHA==,type:str]sops: kms: [] gcp_kms: - resource_id: projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key created_at: '2018-05-28T14:55:44Z' enc: CiQA/TkaHVv9JRDpzqLYXfGD8QDW92/4xSZhMcuao5VlStYzBAwSSQDQcvdxZjXWoG1h8nEbj9o1TNWvdSlz5rxY/l9WEd/WieUiORJLVxI+MCsZ764vUksSk9jrmPxnLqlrfKy/Klz/6yKGi3pyx/g= lastmodified: '2018-05-28T14:55:45Z' mac: ENC[AES256_GCM,data:zoHHamGXApQLsCJSxYuuVkYFo8xAROv+ier9wZYEpn2jDu9vwDZObwEb52Ewx4UTEJphq4KWi/DKodcUZHfyhWIEjqpwwUxqlIXlWmq1AO5XERa/XS0egi6gK3VnKYHsxbRSKF/hY/DOS6bO5A6Fnh0lBfENUZ8qStt5gKtwPOE=,iv:FK4V/TOHJ6cY3mPk4rlrgrSWm8s+HFazY+62d8Cck58=,tag:HkgWUGc4O4rcHZ1jDY+viQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.0.5
このファイルを編集するときは、下記のように直接sops
コマンドで指定します。
$ sops encrypted_values.yaml
するとファイルの内容が復号され、環境変数EDITORで指定したエディタ上で編集可能になります。保存してエディタを閉じると再び暗号化された状態で保存されます。
$ sops --decrypt --gcp-kms projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key encrypted_values.yaml > values.yaml
sopsもyaml_vaultのようにファイルの一部分だけを暗号化することもできますが、「YAMLデータのキーにサフィックスをつけて指定した部分だけを平文にする」という仕組みであり、values.yaml
の構造に直接影響を与える方法なので使いにくいです。
Encrypting only part of a file
sopsを使う場合、values.yaml
には秘密情報を以外の値だけを書くようにし、secret-values.yaml
に秘密情報を全て取り出して、常に-f
オプションでsecret-values.yaml
も指定するという方法が良いと思います。
Helm Chartでは、他のChartをSubchartとして使用することができます。
例えばstable/sentry
パッケージはstable/postgres
とstable/redis
をSubchartとして使用しており、PostgreSQLとRedisのセットアップはこれらのChartに依存しています。
今回、rails-k8s-demoapp
のChartではあえてstable/mysql
とstable/redis
を使わずに自前でテンプレートを用意しました。この理由は公式のChartと言えどまだ枯れているとは言えないと判断したためです。
例えばstable/redis
ではパスワードを示すパラメータの名前が途中で変わっており、stable/sentry
の現時点での最新版(0.1.14)は古いバージョンのstable/redis
(0.10.1)に依存しています。そのため最新のstable/redis
のドキュメントを見て設定してしまうと動きません。(バージョンを指定してドキュメントを見られるようなサービスもありません)
また、当然Values
で変更できる部分以外の動作は変更できなくなるため、Chart側で十分な柔軟性が確保されていない場合には、自分のアプリケーションの要件を満たせないあるいは途中で満たせなくなる可能性があります。
このあたりはChef
の(特に初期の)コミュニティクックブックに状況が似ていると思います。
公式のChartとして公開することを目指すのであればできるだけ他のChartを利用するべきだと思いますが、そうでない場合には無理に依存を増やす必要はありません。公式のChartは汎用性を高めるために多機能なものが多く、全貌を把握すること自体がコストになるケースもあります。簡単なものなら自作した方がトータルでは楽なことも多いです。
また、Subchartまで完全に自分でコントロール可能な場合はChartの分割を検討しても良いと思いますが、多くのChartに依存されたChartは更新が難しくなるので、そこはトレードオフになります。
この節では、プライベートなアプリのHelm Chartの作り方に関して、k8sのプレーンなマニフェストをどのようにテンプレート化するかについて、puma-deploy.yaml
を例に説明しました。
その他のオブジェクトのマニフェストについてもだいたい同じような手順でテンプレート化できるため、それらについての詳細な説明は割愛します。コードを見てみてください。
以下についてはすでに説明しました。
この節では、アプリケーションを運用する上でさらに必要になるであろういくつかのオペレーションを説明します。
コマンド自体は初回デプロイと同じhelm upgrade
を使いますが、アプリケーションを更新する場合には次のいずれかの方法でDeployment
のイメージを変更する必要があります。
--set
オプションでrails.image.tag
を変更する。この場合、次のように新しいタグを指定して実行します。
$ helm upgrade staging . --install --wait \ --set ingress.host=demoapp-puma.$(minikube ip).nip.io \ --set rails.image.tag=0.0.2
values.yaml
ファイルの.rails.image.tag
とChart.yaml
のバージョン情報を書き換えてhelm upgrade
を実行する。差分は典型的には下記のようになります。
diff --git a/k8s/chart/Chart.yaml b/k8s/chart/Chart.yamlindex 43907c6..a6283b0 100644--- a/k8s/chart/Chart.yaml+++ b/k8s/chart/Chart.yaml@@ -1,5 +1,5 @@ apiVersion: v1-appVersion: "0.0.1"+appVersion: "0.0.2" description: A Helm chart for Kubernetes name: demoapp-version: 0.0.1+version: 0.0.2diff --git a/k8s/chart/values.yaml b/k8s/chart/values.yamlindex 3f6eea1..96610f8 100644--- a/k8s/chart/values.yaml+++ b/k8s/chart/values.yaml@@ -24,7 +24,7 @@ sidekiq: rails: image: repository: demoapp- tag: 0.0.1+ tag: 0.0.2 setupDbTag: 0.0.1 ingress:
ファイルを書き換えたら、初回デプロイと全く同じコマンドを実行します。
$ helm upgrade staging . --install --wait \ --set ingress.host=demoapp-puma.$(minikube ip).nip.io
簡単なのは1の方法ですが、現在デプロイされているイメージの情報がgit側に残らないというデメリットがあります。
長期的には2の方法がおすすめです。Chart.yaml
のversion
も更新しておくと、後に紹介するhelm history
とgitのログでインフラ構成の履歴を追うのが楽になります。
指定したRelease
の更新履歴を表示します。
$ helm history stagingREVISION UPDATED STATUS CHART DESCRIPTION1 Mon May 28 00:01:37 2018 SUPERSEDED demoapp-0.0.1 Install complete2 Mon May 28 00:07:00 2018 DEPLOYED demoapp-0.0.2 Upgrade complete
REVISION
は、そのRelease
の現在のバージョンを示す数値で、1から始まりhelm upgrade
するたびに1ずつ増えていきます。
Releaseの状態を指定したリビジョンに戻します。
$ helm rollback staging 1Rollback was a success! Happy Helming!$ helm history stagingREVISION UPDATED STATUS CHART DESCRIPTION1 Mon May 28 00:01:37 2018 SUPERSEDED demoapp-0.0.1 Install complete2 Mon May 28 00:07:00 2018 SUPERSEDED demoapp-0.0.2 Upgrade complete3 Mon May 28 00:10:31 2018 DEPLOYED demoapp-0.0.1 Rollback to 1
指定したRelease
に関する下記の情報を表示することができます。
values.yaml
と--set
や-f
オプションなどの値をマージした最終的なValues
オブジェクト$ helm get stagingREVISION: 1RELEASED: Mon May 28 00:01:37 2018CHART: demoapp-0.0.1USER-SUPPLIED VALUES:ingress: host: demoapp-puma.192.168.64.29.nip.ioCOMPUTED VALUES:env: configmap: MYSQL_DATABASE: demoapp_production MYSQL_USER: demoapp RAILS_LOG_TO_STDOUT: "true" RAILS_SERVE_STATIC_FILES: "true"(省略)HOOKS:MANIFEST:---# Source: demoapp/templates/mysql-env-secret.yamlapiVersion: v1kind: Secretmetadata: name: staging-demoapp-mysql-envdata: MYSQL_PASSWORD: c2VjcmV0 MYSQL_ROOT_PASSWORD: dG9wc2VjcmV0(省略)
helm
コマンドはプラグイン機構により拡張することが可能です。
利用可能なプラグインは下記のページに掲載されています。
ここではプラグインの一つHelm Diffを紹介します。
helm plugin
コマンドでインストールできます。
$ helm plugin install https://github.com/databus23/helm-diff
helm diff
コマンドを使うと、helm upgrade
により発生する差分をdiff
形式で表示することができます。
% helm diff upgrade staging . --set ingress.host=demoapp-puma.$(minikube ip).nip.io --set rails.image.tag=0.0.2staging-demoapp-sidekiq, Deployment (apps/v1) has changed: # Source: demoapp/templates/sidekiq-deploy.yaml apiVersion: apps/v1 kind: Deployment(省略)- image: demoapp:0.0.1+ image: demoapp:0.0.2(省略)staging-demoapp-puma, Deployment (apps/v1) has changed: # Source: demoapp/templates/puma-deploy.yaml apiVersion: apps/v1 kind: Deployment(省略)- image: demoapp:0.0.1+ image: demoapp:0.0.2(省略)
また、helm rollback
で発生する差分や、過去のリビジョン同士の差分を表示することもできます。詳細はHelm Diffのドキュメントを参照してください。
$ helm diff -h
Railsアプリ開発のためのDocker/Kubernetes入門を主題として、Docker/Kubernetesの基本と、小さなRailsアプリケーションをHelmでk8sクラスタにデプロイする方法について書きました。
Docker/Kubernetesを本番サービスへ投入するのであれば、さらに下記について学ぶと良いと思います。
Pod
をどのノードに配置するかを制御する方法(Assigning Pods to Nodes、Taints and Tolerations)Pod
に割り当てるCPUやメモリなどの計算資源を制御する方法(Managing Compute Resources for Containers)Pod
の数を増減させる方法(Horizontal Pod Autoscaler)クラウドを使うならGKEが簡単でおすすめですが、GKEでHelmを使おうとするとRBACが最初のハードルになると思います。次回はその辺りについて書きたいと思います。
]]>前回のKubernetes基礎編では、Step1として第2回Docker Compose/Dockerfile編のdocker-compose-preview.yml
に相当する構成をDeployment
, Service
, ConfigMap
, Secret
の4種のAPIオブジェクトで記述しました。マニフェストファイル一式はサンプルコードの k8s/manifests-step1/ ディレクトリにあります。
この構成には下記に挙げる三つの制約があります。
rails db:setup
が並列実行されてエラーになる。今回はこれらの制約をStep2からStep4で解消していきます。
サンプルコードは全て下記のリポジトリにあります。
https://github.com/kwhrtsk/rails-k8s-demoapp
まずサンプルアプリのコードをチェックアウトしてminikubeを起動してください。(前回と同じなのですでにやっている人は読み飛ばしてください)
# サンプルアプリのコードをチェックアウト$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git$ cd rails-k8s-demoapp# minikubeを起動$ minikube start --cpus=3 --memory=2048 --vm-driver=hyperkit --disk-size=12g# kubernetesのダッシュボードをオープン$ minikube dashboard
サンプルコードは全て下記のディレクトリにあります。
Step1の構成ではpumaのレプリカの数を1に指定していましたが、一定以上の性能を出そうとするとレプリカの数は2以上に設定する必要があります。ところが、このままの構成でpumaコンテナを複数起動すると、rails db:setup
が並列実行されてエラーが発生します。
次のような手順でこれを確認できます。
まずpuma-deploy.yaml
を書き換えてreplicasを2にします。
diff --git a/k8s/manifests-step1/puma-deploy.yaml b/k8s/manifests-step1/puma-deploy.yamlindex 003f484..c17f9bb 100644--- a/k8s/manifests-step1/puma-deploy.yaml+++ b/k8s/manifests-step1/puma-deploy.yaml@@ -7,7 +7,7 @@ metadata: app: demoapp component: puma spec:- replicas: 1+ replicas: 2 selector: matchLabels: app: demoapp
次にstern
でログを監視しつつ、別の端末でkubectl apply
を実行し、APIオブジェクトを作成します。
$ cd k8s/manifests-step1# デプロイ済みのオブジェクトがある場合は削除$ cat *.yaml | kubectl delete -f -# ログを監視$ stern "demoapp.*"# 別の端末で実行$ cat *.yaml | kubectl apply -f -
タイミング依存なのでエラーが発生しない場合もありますが、それなりの確率で下記のようなエラーがsternの端末上に表示されるはずです。
demoapp-puma-749c456c87-2wk5c puma + ./bin/wait-for demoapp-redis 6379demoapp-puma-749c456c87-2wk5c puma + ./bin/rails db:setup_if_not_yetdemoapp-puma-749c456c87-lwwb7 puma + ./bin/wait-for demoapp-redis 6379demoapp-puma-749c456c87-lwwb7 puma + ./bin/rails db:setup_if_not_yetdemoapp-puma-749c456c87-lwwb7 puma Database 'demoapp_production' already existsdemoapp-puma-749c456c87-2wk5c puma Database 'demoapp_production' already existsdemoapp-puma-749c456c87-lwwb7 puma -- create_table("messages", {:options=>"ENGINE=InnoDB DEFAULT CHARSET=utf8", :force=>:cascade})demoapp-puma-749c456c87-2wk5c puma -- create_table("messages", {:options=>"ENGINE=InnoDB DEFAULT CHARSET=utf8", :force=>:cascade})demoapp-puma-749c456c87-lwwb7 puma -> 0.0505sdemoapp-puma-749c456c87-2wk5c puma -> 0.0925sdemoapp-puma-749c456c87-2wk5c puma rails aborted!demoapp-puma-749c456c87-2wk5c puma ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry '20180411125010' for key 'PRIMARY': INSERT INTO `schema_migrations` (version) VALUES (20180411125010)
2つのpuma用のPodがほぼ同じタイミングで./bin/rails db:setup_if_not_yet
を実行していることがわかります。このRakeタスクは第二回 Docker Compose/Dockerfile編で説明しましたが、下記のような内容です。
# lib/tasks/db.rakenamespace :db do desc "Invoke db:setup task only if the task has never been invoked yet." task setup_if_not_yet: [:environment] do begin if !ActiveRecord::SchemaMigration.table_exists? # データベースは存在するが、必ずあるはずのテーブルがない Rake::Task["db:setup"].invoke exit 0 end rescue ActiveRecord::NoDatabaseError # データベースが存在しない Rake::Task["db:setup"].invoke exit 0 end endend
rails db:setup
の初回実行時に作成されるテーブルの有無を確認して、テーブルが存在しない場合のみrails db:setup
を実行していますが、テーブルの確認とタスクの実行の間がクリティカルセクションになっているため、何らかの方法で排他制御しなければrails db:setup
が2重に実行される可能性があります。前述のエラーはまさにこれが原因です。
MySQLやRedisを使ってロックを取るなどこれを排他制御することは技術的には可能なのですが、rails db:setup
のように一度きりしか実行しないような処理を記述するには Job
というAPIオブジェクトが適任ですので、これを例に使い方を説明します。
ところで、典型的なRailsのワークフローには、DBの初期化を行うrails db:setup
に加えて、スキーマの変更などを行うマイグレーション用のタスクrails db:migrate
があります。一見するとこれもJob
で管理するのが良さそうなのですが、Job
は一度APIオブジェクトを作成するとDeployment
のようにイメージを変更できないという制約があります。そのため、新しいマイグレーションを作成するたびにJob
オブジェクトも新規に作成する必要があり、運用が煩雑になります。また、rails db:setup
とは異なり、rails db:migrate
には(少なくともDBがMySQLかPostgreSQLの場合には)RDBMSのロック機構を使ってマイグレーションが多重実行されないように排他制御する仕組みが組み込まれているため、puma
の起動前に毎回実行しても実質的な問題が発生しません。
そこで Step2 では全体の構成を下記のように変更します。
rails db:setup
はJob
として定義し、アプリを最初にk8sにデプロイした時に一度だけ実行する。rails db:migrate
はpuma
の起動前に毎回実行する。(複数のpuma
コンテナにより同時に起動される可能性があるが問題ない)まずは rails db:setup
を実行するための Job
のマニフェストを書きます。
# k8s/manifests-step2/setup-db-job.yaml---apiVersion: batch/v1 # Deploymentのapps/v1とは異なる点に注意kind: Jobmetadata: name: demoapp-setup-dbspec: backoffLimit: 4 # ジョブを失敗したと見なすまでの再試行回数 template: metadata: labels: app: demoapp component: setup-db spec: restartPolicy: Never # OnFailureにするとbackoffLimitが効かない場合があるのでNeverにする containers: - name: setup-db image: demoapp:0.0.1 command: - ./bin/setup-db envFrom: - configMapRef: name: demoapp-rails-env - secretRef: name: demoapp-rails-env
内容はpuma
やsidekiq
のDeployment
と似ています。まず同じようにdemoapp
イメージを指定します。また、.spec.template
以下の内容もほぼ同じですが、livenessProbe
などの項目が無い分Deployment
よりもシンプルな内容になります。
Job
固有の設定値について簡単に補足します。
backoffLimit
のデフォルト値は6です。4を指定しているのは単にサンプルとして例示するためで深い意味はありません。restartPolicy
はOnFailure
かNever
を指定できます。OnFailure
の場合はジョブが失敗したときにPod
はそのままで内部のコンテナだけ作り直します。Never
の場合はPod
ごと作り直します。今回の場合どちらでも良いのですが、既知の問題によりNever
だとbackoffLimit
が無視されるケースがあると公式ドキュメントにも書いてあるのでNever
を指定しています。詳細は下記のドキュメントを参照してください。
command
に指定している ./bin/setup-db
は下記のような内容で、単にmysql
の起動を待ち受けてから(Step1ではpuma
で実行していた)rails db:setup_if_not_yet
を実行します。
#!/bin/bash -xucd $(dirname $0)/.../bin/wait-for $MYSQL_HOST 3306./bin/rails db:setup_if_not_yet
また、puma-deploy.yaml
も次のように修正します。
--- k8s/manifests-step1/puma-deploy.yaml 2018-05-11 23:30:49.000000000 +0900+++ k8s/manifests-step2/puma-deploy.yaml 2018-05-11 23:57:17.000000000 +0900@@ -7,7 +7,7 @@ app: demoapp component: puma spec:- replicas: 1+ replicas: 2 selector: matchLabels: app: demoapp@@ -24,7 +24,7 @@ image: demoapp:0.0.1 imagePullPolicy: IfNotPresent command:- - ./bin/setup-db-and-start-puma+ - ./bin/start-puma livenessProbe: httpGet: path: /health_check/full
replicas
を2に増やし、command
を./bin/start-puma
に変更しています。./bin/start-puma
は下記のような内容です。
#!/bin/bash -xcd $(dirname $0)/..trap "pkill -P $$" EXIT./bin/wait-for $MYSQL_HOST 3306./bin/wait-for $REDIS_HOST 6379./bin/rails db:try_migrate./bin/pumactl start
Step1ではpumactl
コマンドの実行前に./bin/rails db:setup_if_not_yet
を実行していましたが、代わりにrails db:try_migrate
を実行します。このRakeタスクは下記のような定義です。
# lib/tasks/db.rakenamespace :db do desc "Wait for db:setup task to complete." task wait_for_setup_completion: [:environment] do loop do begin if !ActiveRecord::InternalMetadata.table_exists? # データベースは存在するが、必ずあるはずのテーブルがない sleep 1 elsif !ActiveRecord::InternalMetadata.where(key: :environment).exists? # テーブルはあるがレコードが存在しない sleep 1 else break end rescue ActiveRecord::NoDatabaseError # データベースが存在しない sleep 1 end end end desc "Invoke db:migrate task and ignore errors caused by parallel execution." task try_migrate: [:wait_for_setup_completion] do begin Rake::Task["db:migrate"].invoke rescue ActiveRecord::ConcurrentMigrationError => e Rails.logger.info "Skip migrations because another migration process is currently running." end endend
行数は多いですがやっていることはrails db:setup
の完了を待ってからrails db:migrate
を実行しているだけです。
ただし、db:migrate
タスクでActiveRecord::ConcurrentMigrationError
という例外が発生した場合は、エラー終了しないように捕捉してINFOレベルでログに書き出すようにしています。
この例外は、マイグレーションが多重起動された際に、ロックを取れなかったプロセス側が発生させる例外で、実害はありません。単にそのプロセスではマイグレーションがスキップされたということを示しています。
Step2での修正はJob
の追加とpuma
用のDeployment
の定義の変更の2点だけですが、これを機にdemoapp
のイメージを更新してpuma
用のDeployment
に反映し、Pod
がローリングリスタートされる様子を確認してみましょう。demoapp
の更新は新規マイグレーションを含む内容にしてみます。
まずStep2のマニフェストでdemoapp
をminikubeにデプロイします。
# Railsアプリのイメージをビルド$ (eval $(minikube docker-env) && docker build . -t demoapp:0.0.1)$ cd k8s/manifests-step2# APIオブジェクトを作成$ cat *.yaml | kubectl apply -f -# puma deploymentの起動が完了するまで待機$ kubectl rollout status deploy demoapp-puma# puma serviceのエンドポイントをブラウザでオープン$ minikube service demoapp-puma
次に、messages
テーブルにlikes
というカラムを追加するだけの内容で新しいマイグレーションを作ります。
# RAILS_ROOTに移動してローカル環境にMySQLとRedisを起動しDBを初期化$ cd ../../$ rm -r tmp/mysql tmp/redis$ docker-compose up -d# しばらく待ってから$ ./bin/rails db:setup# 新しいマイグレーションを作成して、ローカル環境にマイグレーション実行$ ./bin/rails g migration AddLiksToMessage likes:integer$ ./bin/rails db:migrate
新しいdemoapp
のイメージを作りデプロイします。
# タグを0.0.2に変えてRailsアプリのイメージをビルド$ (eval $(minikube docker-env) && docker build . -t demoapp:0.0.2)# 新しいイメージをデプロイして更新が完了するのを待つ$ kubectl set image deploy/demoapp-puma puma=demoapp:0.0.2$ kubectl set image deploy/demoapp-sidekiq sidekiq=demoapp:0.0.2$ kubectl rollout status deploy demoapp-puma$ kubectl rollout status deploy demoapp-sidekiq
事前に別の端末で下記のコマンドをそれぞれ実行しておくとローリングリスタートに伴うPod
の入れ替わりの様子を確認しやすいと思います。
# puma用のPodのログを表示$ stern "demoapp-puma-.*"# APIオブジェクトの変化を監視して表示$ kubectl get deployments --watch$ kubectl get replicasets --watch$ kubectl get pods --watch
watchコマンドを用いる方法もおすすめです。watchコマンドは brew install watch
でインストールできます。
# 更新頻度を1秒に指定$ watch -n 1 kubectl get deployments,replicasets,pods
kubectl set image deploy/demoapp-puma puma=demoapp:0.0.2
を実行するとpuma
のDeployment
が更新され、ローリングアップデートが開始します(puma=demoapp:0.0.2
のpuma
は、puma-deploy.yaml
で定義したコンテナのname
です)。
この際、puma-deploy.yaml
のreplicas
が2だと下記のような動作になり、rails db:migrate
の競合は起きません。
Pod
を一つ起動開始Pod
が起動完了し、古いPod
のうち一つが停止し、さらに新しいPod
を一つ起動開始Pod
が起動完了し、残りの古いPod
が停止replicas
が4だと下記のような動作になり、高い確率でrails db:migrate
が競合します。(puma-deploy.yaml
を編集して試してみてください)
Pod
を一つ停止すると同時に新しいPod
を二つ起動開始Pod
二つがほぼ同時に起動完了し、古いPod
がさらに停止され、新しいPod
がさらに二つ起動開始Pod
二つがほぼ同時に起動完了し、最後のPod
が停止rails db:migrate
がほぼ同時に起動された場合には、遅い方のPod
のログに下記のようなメッセージが残るはずです。
INFO -- : Skip migrations because another migration process is currently running.
なお、Deployment
の更新の際のPod
の入れ替えはこのように徐々に新しいものに入れ替えるような動作になっていますが、これはマニフェストの.spec.strategy
で変更することができます。puma-deploy.yaml
の例では特に指定していないのでデフォルトのRollingUpdate
になっていますが、Recreate
を指定すると古いPod
を全て削除してから新しいPod
を立ち上げるようになります。これについてはStep3で取り上げます。
また、replicas
が4のケースだと、一時的に起動中のPod
の数が5つに、READYな状態のPod
は3つになっている点に注意してください。急激なサービスのスループット低下とインフラ負荷の増加を防ぐため、レプリカの数が多い場合はデフォルトでこのような動作をするようになっています。ローリングアップデート中におけるREADYな状態のPod
の割合と起動中(準備中含む)のPod
の割合は、それぞれ.spec.strategy.rollingUpdate.maxUnavailable
と.spec.strategy.rollingUpdate.maxSurge
で指定できます。デフォルト値はいずれも25%です。これについては本ドキュメントではこれ以上説明しないので、詳細は下記の資料を参照してください。
k8sのようにBlue-Greenデプロイメントでアプリケーションの更新を行う場合、新しいインスタンスを立ち上げつつ古い方のインスタンスは順次停止します。この際、実行中の処理はできるだけ強制終了せずに、完了するまで待ってからアプリケーションプロセスを停止することが望ましいです。
pumaとsidekiqの場合は次のようなことを意味します。
一般的にこのような停止処理のことをgraceful stopと呼びます。
前節で述べたローリングリスタートの際には、k8sは停止するPod
の全てのコンテナにSIGTERM
シグナルを送信し、コンテナのメインプロセスが停止するのを待ちます。そのため、コンテナで起動するプロセスはSIGTERM
を受け取った時にgraceful stopするように実装しておく必要があります。
pumaとsidekiqはいずれもSIGTERM
を受け取った際にgraceful stopする仕様なので、このままでも問題ない場合もあるのですが、下記の点に気をつける必要があります。
trap
コマンドなどでシグナルを捕捉して子プロセスにもSIGTERM
を送ること。サンプルは第2回を参照してください。SIGTERM
を受け取ってからジョブの強制終了まで8秒しか待たない。これが短すぎる場合は起動時の-t
オプションで変更しておくこと。サンプルは第4回の記事を参照してください。Deployment
のspec.template.spec.terminationGracePeriodSeconds
で変更しておくこと。(参考)pumaとsidekiqのシグナルの扱いについては下記のドキュメントを参照してください。
Step2のディレクトリにもMakefileを置いてあります。本節に出てきた各種操作は(マイグレーションの作成以外は)対応するタスクを定義してあるので、ローリングアップデートの動作を確認する際にはmake
コマンドを使うと簡単にオペレーションを実行することができます。
$ cd k8s/manifests-step2# タグを指定してイメージをビルドし、APIオブジェクト一式を作成(TAGは省略可)$ make TAG=0.0.1# タグを指定してイメージをビルドし、puma用Deploymentに反映$ make TAG=0.0.2 update# APIオブジェクトの削除$ make clean
MakefileはStep1のものをベースにいくつか修正を加えた内容です。miniube-docker-build
は、指定されたタグが存在する場合はビルドをスキップするようにしています。
# k8s/manifests-step2/MakefileSHELL = /bin/bashifeq ($(TAG),) tag := 0.0.1else tag := $(TAG)endifall: $(MAKE) minikube-docker-build $(MAKE) kubectl-apply $(MAKE) kubectl-rollout-status $(MAKE) minikube-serviceclean: kubectl-deleteupdate: $(MAKE) TAG=$(tag) minikube-docker-build $(MAKE) TAG=$(tag) deployminikube-docker-build: eval $$(minikube docker-env) && \ if [ "$$(docker image ls -q demoapp:$(tag))" == "" ]; then \ docker build ../../ -t demoapp:$(tag); \ fikubectl-apply: cat *.yaml | kubectl apply -f -kubectl-rollout-status: kubectl rollout status deploy demoapp-puma kubectl rollout status deploy demoapp-sidekiqminikube-service: minikube service demoapp-pumakubectl-delete: cat *.yaml | kubectl delete -f -deploy: kubectl set image deploy/demoapp-puma puma=demoapp:$(tag) kubectl set image deploy/demoapp-sidekiq sidekiq=demoapp:$(tag) kubectl rollout status deploy demoapp-puma kubectl rollout status deploy demoapp-sidekiqstern: stern demoapp.*
サンプルコードは全て下記のディレクトリにあります。
Step2の時点ではMySQLとRedisのデータ領域が永続化されていないため、何らかの障害でPod
が停止するとDBの内容が全て消えるという制約があります。
Step2のマニフェストでAPIオブジェクト一式を作成した後、mysql
のPod
をkubectl delete
コマンドで削除すると、puma
のヘルスチェックが失敗するようになるのを確認できます。
$ cd k8s/manifests-step2# APIオブジェクトを作成$ cat *.yaml | kubectl apply -f -# puma deploymentの起動が完了するまで待機$ kubectl rollout status deploy demoapp-puma# MySQL用のPodを削除$ kubectl delete pod -l "app=demoapp,component=mysql"# pumaのログを確認$ stern "demoapp-puma-.*"(省略)demoapp-puma-5cdbdbfc76-d6ftm puma I, [2018-05-13T12:11:56.303988 #33] INFO -- : [026a124c-fedb-4423-8587-40bec3481cde] health_check failed:demoapp-puma-5cdbdbfc76-d6ftm pumademoapp-puma-5cdbdbfc76-d6ftm puma Migrations are pending. To resolve this issue, run:demoapp-puma-5cdbdbfc76-d6ftm pumademoapp-puma-5cdbdbfc76-d6ftm puma bin/rails db:migrate RAILS_ENV=production
そこでStep3では、MySQLとRedisにPersistentVolume
(以後、PV
)というAPIオブジェクトを割り当ててデータボリュームとして使用し、DBの内容がPod
の生死に関わらず維持されるようにします。
PV
は永続ストレージを抽象化したAPIオブジェクトです。PV
の実体はNFSやGlusterFSのようなネットワークストレージでも良いし、GCP上であればGCEのPersistentDisk、AWSであればEBSを使うこともできます。これをボリュームタイプと呼びます。
PV
のボリュームタイプはプラグインとして実装されていて、すでに主要なネットワークストレージとパブリッククラウドのストレージサービスに対応しています。
MySQLのようなデータベースをk8sで運用するためには、障害でPod
やk8sノードがクラッシュした場合に、新しいPod
にデータ領域を引き継ぐ必要があります。k8sでは先に挙げたようなドライバ経由でk8sクラスタの外にデータを保存することによってこれを実現しています。
PV
オブジェクトは直接マニフェストを書いて作ることもできますが、PersistentVolumeClaim
(以後、PVC
)というAPIオブジェクトのDynamic Provisioningという機能を経由して作る方が簡単なので、本ドキュメントではその方法で説明します。
まずMySQL用のPVC
オブジェクトのマニフェスト mysql-pvc.yaml
を作成します。
# k8s/manifests-step3/mysql-pvc.yaml---kind: PersistentVolumeClaimapiVersion: v1metadata: name: demoapp-mysql labels: app: demoapp component: mysqlspec: accessModes: - ReadWriteOnce # 同時に一つのプロセスからしか読み書きしないモードを指定 resources: requests: storage: 8Gi # 確保する永続ストレージのサイズを指定 storageClassName: standard # 後述
.spec.storageClassName
にはStorageClass
というAPIオブジェクトの名前を指定します。StorageClass
の詳細については後述します。
次に mysql-deploy.yaml
へこのPVC
を結びつけます。コメントをつけている行は、Step2での定義に追加した部分です。
# k8s/manifests-step3/mysql-deploy.yaml---apiVersion: apps/v1kind: Deploymentmetadata: name: demoapp-mysql labels: app: demoapp component: mysqlspec: replicas: 1 selector: matchLabels: app: demoapp component: mysql strategy: # 更新時の動作を指定 type: Recreate # 古いPodを停止してから新しいPodを起動 template: metadata: labels: app: demoapp component: mysql spec: restartPolicy: Always volumes: # - name: data # .volumeMountsの.nameで指定 persistentVolumeClaim: # claimName: demoapp-mysql # PVCオブジェクトの.metadata.nameを指定 initContainers: # 前処理用のコンテナ - name: "remove-lost-found" # データディレクトリの"lost+found"ディレクトリを削除 image: "busybox:1.25.0" command: - rm - -fr - /var/lib/mysql/lost+found volumeMounts: - name: data mountPath: /var/lib/mysql containers: - name: mysql image: mysql:5.7.21 livenessProbe: tcpSocket: port: 3306 readinessProbe: tcpSocket: port: 3306 volumeMounts: # - name: data # .volumesの.nameを指定 mountPath: /var/lib/mysql # MySQLのデータディレクトリを指定 envFrom: - configMapRef: name: demoapp-mysql-env - secretRef: name: demoapp-mysql-env
このようにPVC
オブジェクトを指定すると、Deployment
の作成時にStorageClass
に応じたPV
が自動的に作成され、指定したパスにマウントされます。この例では mysql-pvc.yaml
で定義したPVC
によって作られたPV
をMySQLのデータディレクトリである/var/lib/mysql
にマウントしています。
.spec.template.spec.initContainers
は前処理用のコンテナの定義です。メインのコンテナを起動する前に自動的に実行されます。この例ではMySQLのデータディレクトリである/var/lib/mysql
ディレクトリにlost+found
というディレクトリがあれば削除するという処理を入れています。PVC
で確保したボリュームの中身は後述するStorageClass
によって異なる場合があるのですが、mysql
イメージは/var/lib/mysql
ディレクトリが空でない場合、DBの初期化処理がエラーで失敗します。minikubeの場合は空なのですが、GKEのようにlost+found
ディレクトリが最初から存在する場合もあるので、ある程度どこでも動くようにinitContainers
でこれを取り除いています。
また、.spec.strategy.type
にRecreate
を指定している点に注意してください。Step2でも説明しましたが、これは更新時のPod
の入れ替えの挙動を指定するパラメータです。デフォルトはRollingUpdate
になっており、古いPod
を停止する前に新しいPod
を起動します。MySQLは同一のデータディレクトリを複数のサーバが参照するとエラーになるため、Recreate
を指定して二つ以上のPod
が同時に起動しないようにしています。
Redisについてもほぼ同様に、redis-pvc.yaml
を追加してredis-deploy.yaml
を更新しています。特筆すべき点はないので説明は割愛します。詳細はサンプルコードを参照してください。
StorageClass
は、永続ストレージの設定をまとめたオブジェクトです。設定とはボリュームタイプやPV
の削除時にデータオブジェクトを残すかどうかといった項目です。minikubeの場合、クラスタの作成時に自動的にstandard
という名前のStorageClass
が作られるのでそれを指定しています。このオブジェクトの定義は、minikube dashboard
上または下記のコマンドで確認できます。
$ kubectl get storageclass standard -o yaml
重要な部分を抜粋すると下記のような内容です。
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: annotations: storageclass.beta.kubernetes.io/is-default-class: "true" name: standardprovisioner: k8s.io/minikube-hostpathreclaimPolicy: DeletevolumeBindingMode: Immediate
.provisioner
がボリュームプラグインの種類を示しています。minikube-hostpath
はシングルノードでの検証用のもので、minikube VM上のローカルストレージをデータの保存先として使用します。
GCPのGKEでk8sクラスタを作った場合も、自動的にstandard
という名前のStorageClass
が作られますが、下記の通り.provisioner
はGCEのPersistentDiskが指定されているため、マルチノードクラスタでの運用に耐えるものとなっています。
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: annotations: storageclass.beta.kubernetes.io/is-default-class: "true" name: standardprovisioner: kubernetes.io/gce-pdparameters: type: pd-standardreclaimPolicy: Delete
重要なのは、PVC
オブジェクトでは単にstandard
という名前だけを指定しており、ボリュームプラグインのようなクラスタ環境に依存するパラメータを一切記述していないという点です。これにより、この例のようにminikubeとGKEのような異なるプラットフォームであっても、それぞれの環境に応じて適切に設定されたStorageClass
が同じ名前で用意されていれば、このマニフェストはいずれのクラスタにもデプロイ可能になります。
k8sにはStatefulSet
というAPIオブジェクトがあります。このオブジェクトはステートフルなアプリケーションを管理するためのものと位置付けられています。
そのため、一見するとMySQLやRedisのようなサービスを管理する際にはDeployment
ではなくStatefulSet
を使うべきではと思われるのですが、StatefulSet
が必要になるのは Set の名が示す通りMySQLのマスター/スレーブ構成など複数のコンテナでクラスタを組む場合です。
Step3の例のように単一のコンテナによるステートフルなアプリケーションの場合、Deployment
を使って.spec.strategy.type
をRecreate
にするだけで十分です。
ということが公式のドキュメントにも書いてあるのでリンクを貼っておきます。
Run a Single-Instance Stateful Application
Railsアプリケーションをクラウド上のk8sクラスタ上で運用する場合には、MySQLやRedisのような永続データを持つサービスはコンテナ化せず、クラウドベンダのマネージドサービスを使うという方法も考えられます。
特に可用性などの要件が厳しい場合には、簡単にマルチAZでマスタースレーブ構成を運用できるAmazon RDSのようなサービスは魅力的な選択肢です。
一方、可用性やデータロスト耐性に対する要求がそこまで厳しくない場合には、Step3で示したような構成も悪くない選択肢です。Pod
やk8sノードに障害が発生した場合には、新しいPod
が即座に作成されて永続ボリュームの内容を引き継ぎます。Amazon EBSやGCE Persistent Diskのようなストレージサービスを永続ボリュームとして使う場合、定期的なスナップショットを取るのは簡単です。
確かにマルチ構成のAmazon RDSと比較すると障害発生時のダウンタイムは長くなる可能性が高いし、永続ボリュームに障害が発生した場合には直前のスナップショットまでデータロストするリスクがありますが、それを許容できるような場合にはこの程度の構成で十分とも言えます。
サンプルコードは全て下記のディレクトリにあります。
Step3の時点では、Railsアプリ(puma
プロセス)の外部向けのインタフェースとして、NodePort
タイプのService
オブジェクトを使っていました。この方法ではk8sクラスタを構成する全てのノードにエンドポイントが用意されます。minikubeのようにシングルノード構成の検証用クラスタではこれで十分ですが、マルチノード構成の本番環境ではそのまま運用するのは難しいでしょう。少なくとも負荷分散や可用性担保のためには前段にロードバランサが必要になります。そうするとノードの追加や縮退の際にロードバランサの設定も変更する必要が生じ、管理が煩雑になります。
そのため、このような用途でService
オブジェクトを使う場合には、通常はLoadBalancer
というタイプを指定します。
LoadBalancer
を指定した場合にどのような仕組みで接続用のエンドポイントが用意されるかはk8sのデプロイ先の環境によって異なります。例えばAWS上であればELBを使うことができます。一方、minikubeはLoadBalancer
に完全には対応していません。そのため、LoadBalancer
タイプでService
を作ると下記のようにEXTERNAL-IP
がいつまでもpending
の状態のまま変わらず、ダッシュボードでも準備中のアイコンが表示され続けます。
% kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEdemoapp-mysql ClusterIP 10.99.99.47 <none> 3306/TCP 1mdemoapp-puma LoadBalancer 10.98.211.117 <pending> 3000:32320/TCP 1mdemoapp-redis ClusterIP 10.111.218.160 <none> 6379/TCP 1mkubernetes ClusterIP 10.96.0.1 <none> 443/TCP 8m
参考: docker - kubernetes service external ip pending - Stack Overflow
一方、k8sには負荷分散やSSLターミネーション、ホスト名やリクエストパスによるバーチャルホストなど、外部向けのエンドポイントを抽象化することにより特化した機能を持つIngress
というオブジェクトがあります。これらの機能の一部はLoadBalancer
タイプのService
でも実現できますが、AWSのELBなどバックエンドごとに固有のannotation
で指定する必要があり、Ingress
に比べると可搬性の面で劣ります。
そこでこのStep4では外部向けのエンドポイントにIngress
を使うように構成を変更します。LoadBalancer
タイプのService
の使い方については説明しません。詳細は下記のドキュメントを参照してください。
PersistentVolume
同様、Ingress
もまたその実装がk8sクラスタのデプロイ先に依存する機能です。GKEではGCPのネットワーク機能を使って実装されていますが、その他の環境ではIngress
コントローラをPod
としてk8sクラスタ上に動かしておく必要があります。
Ingress
コントローラの中にはGLBC(ingress-gce)のようにクラウドプロバイダ固有の機能を使ったものと、NGINX Ingress Controller(ingress-nginx)のようにどこでも動作するものがあります。
幸いにしてminikubeではNGINX Ingress Controller
をaddonとして簡単にインストールすることができます。本ドキュメントで示すマニフェストを試す場合には、minikubeインスタンスの起動後に下記のコマンドを実行してください。
$ minikube addons enable ingress
このドキュメントでは、下記のような構成を目指します。
puma
サービスへ https://demoapp-puma.$(minikube ip).nip.io/
というホスト名で接続できるようにする。Ingress
でSSL終端して、Service
にはHTTPで接続する。Step3以前と比べて少し手順が変わっています。各コマンドの詳細は次節以降で説明します。
# Railsアプリのイメージをビルド$ (eval $(minikube docker-env) && docker build . -t demoapp:0.0.1)$ cd k8s/manifests-step4# ダミーのサーバ証明書を作成COMMON_NAME=demoapp-puma.$(minikube ip).nip.ioopenssl req -new -x509 -nodes -keyout server.key -days 3650 \ -subj "/CN=${COMMON_NAME}" \ -extensions v3_req \ -config <(cat openssl.conf | sed s/\${COMMON_NAME}/$COMMON_NAME/) > server.pemunset COMMON_NAME# 証明書をSecretオブジェクトとして登録kubectl create secret tls demoapp-puma-tls --key server.key --cert server.pem# APIオブジェクトを作成export MINIKUBE_IP=$(minikube ip)cat *.yaml | sed s/\${MINIKUBE_IP}/$MINIKUBE_IP/ | kubectl apply -f -# puma deploymentの起動が完了するまで待機$ kubectl rollout status deploy demoapp-puma# puma Ingressのエンドポイントをブラウザでオープン$ open https://demoapp-puma.$(minikube ip).nip.io/# 作成したAPIオブジェクトを削除cat *.yaml | kubectl delete -f -kubectl delete secret demoapp-puma-tls
Makefileを置いてあるので下記のコマンドで代替できます。
$ cd k8s/manifests-step4# Railsアプリのイメージをビルド$ make minikube-docker-build# ダミーのサーバ証明書を作成しSecretオブジェクトとして登録$ make kubectl-create-secret-tls# APIオブジェクトを作成$ make kubectl-apply# puma deploymentの起動が完了するまで待機$ make kubectl-rollout-status# puma Ingressのエンドポイントをブラウザでオープン$ make open
単にmake
とだけ入力すると順番に全部実行します。
$ make
HTTPSで接続できるようにするためにはサーバ証明書が必要です。kube-legoというモジュールを使えばIngress
とLet’s Encryptを連携して動的に証明書を作成するようなこともできるのですが、今回はもっとシンプルにopenssl
コマンドで自己署名証明書(いわゆるオレオレ証明書)を作ります。
まず下記のような内容でopensslの設定ファイルを用意します。Chromeではバージョン58以降、subjectAltName(SAN)が設定されていない証明書はエラー扱いになりました。下記の設定はSANを設定するためのものです。
# k8s/manifests-step4/openssl.conf[req]distinguished_name=req_distinguished_namereq_extensions=v3_req[v3_req]basicConstraints=CA:FALSEkeyUsage=nonRepudiation, digitalSignature, keyEnciphermentsubjectAltName=@alt_names[req_distinguished_name][alt_names]DNS.1=${COMMON_NAME}
次に下記のようなコマンドで証明書(server.pem
)と鍵(server.key
)を作成します。
COMMON_NAME=demoapp-puma.$(minikube ip).nip.ioopenssl req -new -x509 -nodes -keyout server.key -days 3650 \ -subj "/CN=${COMMON_NAME}" \ -extensions v3_req \ -config <(cat openssl.conf | sed s/\${COMMON_NAME}/$COMMON_NAME/) > server.pemunset COMMON_NAME
ホスト名は前述の通りdemoapp-puma.$(minikube ip).nip.io/
とします。$(minikube ip)
部分はminikubeインスタンスの立ち上げごとに変わる可能性があるため、設定ファイルはテンプレートとして扱い、${COMMON_NAME}
と記述されている部分をsed
コマンドで書き換えています。
内容は下記のコマンドで確認できます。
$ openssl x509 -text < server.pem(一部抜粋)Certificate: Signature Algorithm: sha256WithRSAEncryption Issuer: CN=demoapp-puma.192.168.64.25.nip.io Subject: CN=demoapp-puma.192.168.64.25.nip.io X509v3 extensions: X509v3 Subject Alternative Name: DNS:demoapp-puma.192.168.64.25.nip.io
macOSの場合、server.pem
をキーチェーンに追加して信頼するようにしておけばブラウザの警告を抑止できます。後で忘れずに削除してください。
なお、Step4にもMakefileを置いているので、証明書の作成は下記のコマンドで代替できます。
$ cd k8s/manifests-step4$ make server.pem# minikube ipが変わったら必ず作り直すこと$ make --always-make server.pem
Secret
オブジェクトの作成Ingress
で先ほど作成した証明書と鍵を使うためには、Secret
オブジェクトに登録する必要があります。これまでに紹介したSecret
オブジェクトのようにYAML形式のマニフェストファイルを書いても良いのですが、TLS用の証明書と鍵をファイルから登録することについてはkubectl
コマンドがサポートしているため、下記のように登録します。
kubectl create secret tls demoapp-puma-tls --key server.key --cert server.pem# またはmake kubectl-create-secret-tls
Ingress
オブジェクト# k8s/manifests-step4/puma-ing.yml---apiVersion: extensions/v1beta1kind: Ingressmetadata: name: demoapp-puma annotations: kubernetes.io/ingress.allow-http: "false" # HTTPでのアクセスを禁止 # GKEで確保済みの静的IPアドレスを指定する場合は下記のようにする # kubernetes.io/ingress.global-static-ip-name: rails-k8s-demoappspec: tls: - hosts: - demoapp-puma.${MINIKUBE_IP}.nip.io # このTLS証明書で受けるホスト名のリスト secretName: demoapp-puma-tls # 証明書と鍵のSecretオブジェクトの名前 rules: - host: demoapp-puma.${MINIKUBE_IP}.nip.io # ホスト名 http: paths: - backend: serviceName: demoapp-puma # バックエンドのServiceの名前とポート servicePort: 3000
${MINIKUBE_IP}
となっている部分は、kubectl
コマンドに渡す前にsed
コマンドで置換します。
$ export MINIKUBE_IP=$(minikube ip)$ cat *.yaml | sed s/\${MINIKUBE_IP}/$MINIKUBE_IP/ | kubectl apply -f -
より実践的な課題として、GKEにデプロイした場合の設定方法については下記のドキュメントを参照してください。
Ingress での HTTP 負荷分散の設定 | Kubernetes Engine のドキュメント
nip.io
というのはワイルドカードDNSと呼ばれるサービスのドメインの一つです。Exentrique Solutionsという企業によって運営されています。
下記のようにサブドメインに相当するIPアドレスを動的に返してくれるため、プライベートアドレスを使ったテストに便利です。
$ dig +short 192.168.64.25.nip.io192.168.64.25$ dig +short www.192.168.64.25.nip.io192.168.64.25
先ごろ発表された Jenkins X でも使われていました。
nip.io
はPowerDNSとカスタムスクリプトの組みわせで実装されているとのことです。nip.io
自体はOSSではないのですが、Dockerで動作するnip.io
クローンが存在します。
https://github.com/resmo/nip.io
今回のようにTLSの上で通信する場合にはセキュリティリスクは限定的だと思いますが、DNSを外部に依存したくない場合には自前でnip.io
クローンを運用するかdnsmasqなどの利用を検討してください。
openssl.conf
やpuma-ing.yaml
では、$(minikube ip)
に相当する部分をsed
コマンドで環境変数に置換していました。このようにテキストファイルの一部を環境変数で置換する場合には envsubst
というコマンドを使うとさらに簡単です。
macOSの場合はHomebrewでgettextパッケージでインストールできますが、パスを通すためにはbrew link
に--force
オプションをつけて実行する必要があります。
$ brew install gettext$ brew link gettext --force
今回の場合は置換の対象が一つと少なくsed
での代替が容易だったので、手順全体の依存を減らすためにあえて採用しませんでしたが、マニフェストを全体をテンプレート化して多数のパラメータを注入できるようにしたいのであれば、採用を検討しても良いかもしれません。
ただし、そのような目的のためには次回紹介するHelmを使う方がおすすめです。例えばSecret
のデータエントリはBase64でエンコードする必要がありますが、envsubst
でそれを実現しようとすると煩雑な手順になりますが、Helmであれば簡単です。
下記のオブジェクトについての最低限の説明と、これらを使ってpuma
、sidekiq
、mysql
、redis
のコンテナをk8s上で管理するための実例を示しました。
Deployment
Service
ConfigMap
Secret
Job
PersistentVolumeClaim
Ingress
k8sに関しては、次のステップとして下記について調べるのが良いと思います。
ConfigMap
やSecret
の内容をボリュームとしてPod
にマウントする方法(MySQLやRedisの設定ファイルの管理など)CronJob
オブジェクトによる定期実行ジョブの定義方法(DBのバックアップなど)なお、今回はYAMLファイルとしてマニフェストを管理する方法を示しましたが、この構成においては下記の課題があります。
Secret
の定義ファイルにBase64エンコードした値を読み書きするのが面倒今回は以下のサンプルアプリケーションをminikubeにデプロイするためのマニフェストや手順について説明します。
https://github.com/kwhrtsk/rails-k8s-demoapp
まずサンプルアプリのコードをチェックアウトしてminikubeを起動してください。(前回と同じなのですでにやっている人は読み飛ばしてください)
# サンプルアプリのコードをチェックアウト$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git$ cd rails-k8s-demoapp# minikubeを起動$ minikube start --cpus=3 --memory=2048 --vm-driver=hyperkit --disk-size=12g# kubernetesのダッシュボードをオープン$ minikube dashboard
前回は下記について説明しました。
kubectl
コマンドを使ってAPIオブジェクトを管理する方法今回は実際にRailsアプリをMySQLやRedisなどのミドルウェアも含めて丸ごとk8sクラスタ上にデプロイするための手順を説明します。一度に多種のAPIオブジェクトを使うと全体を理解するのが難しくなるので、4つのステップに分けて進めます。
Step1では、第2回Docker Compose/Dockerfile編のdocker-compose-preview.yml
に相当する構成をできるだけ少ないAPIオブジェクトで簡潔に記述します。この時点では簡潔さと引き換えにいくつかの制約がありますが、それらをStep2からStep4で解消していきます。
今回のKubernetes基礎編ではStep1を説明し、次回のKubernetes応用編でStep2以降を説明します。
マニフェストの説明に入る前に、ビルドしたRailsアプリのイメージをminikube上にデプロイできるようにする方法を説明します。
いくつかの方法があります。
今回は一番簡単な4の方法を使います。
準備 の節を参照して、サンプルコードをチェックアウトしminikubeを起動したら、下記のコマンドを実行してください。
$ (eval $(minikube docker-env) && docker build . -t demoapp:0.0.1)
minikube docker-env
コマンドは、minikube VM上のDockerプロセスに接続できるようにするための環境変数を出力します。上記のようにすることでminikube VM上のDockerプロセス上で直接イメージをビルドします。これにより、マニフェストではイメージ名をdemoapp:0.0.1
と指定すればこのイメージを使えるようになります。
この方法を使う場合、タグにはlatest
以外の値を指定してください。k8sの仕様で、タグがlatest
になっているイメージを指定するとコンテナの起動前に必ずレジストリからイメージをpullしようとするため、4の方法だと必ず失敗するようになってしまいます。
これは4の方法を使う場合の制約ではあるのですが、インフラの構成管理という観点ではそもそもイメージのタグにlatest
を使うとデプロイされるバージョンがタイミング依存になるため、4以外の方法でもlatest
タグの使用は避けたほうが良いと思います。
Step1では、第2回Docker Compose/Dockerfile編のComposeファイルdocker-compose-preview.yml
に相当する構成を(いくつかの制約と引き換えに)できるだけ簡潔に記述します。
# docker-compose-preview.ymlversion: "3"services: puma: image: demoapp build: context: . env_file: - .dockerenv/rails ports: - 3000:3000 depends_on: - mysql - redis command: ./bin/setup-db-and-start-puma sidekiq: image: demoapp env_file: - .dockerenv/rails depends_on: - puma command: ./bin/start-sidekiq mysql: image: mysql:5.7.21 env_file: - .dockerenv/mysql redis: image: redis:4.0.9 command: redis-server --appendonly yes
このComposeファイルの要点は下記の通りです。
.dockerenv/rails
にまとめて記述.dockerenv/mysql
にまとめて記述rails db:setup
)はpumaコンテナの起動時に実行./bin/setup-db-and-start-puma
の解説を参照これと同等の構成をk8sで実現するために、Deployment
、Service
、ConfigMap
、Secret
の4種類のAPIオブジェクトを使います。
Deployment
は、Pod
(≒コンテナ)の起動管理を行うオブジェクトです。Service
は、Pod
へアクセスするためのI/Fを提供します。この二つは前回のKubernetes入門編で解説したので詳細はそちらを参照してください。
Deployment
とService
の組み合わせで、Docker Composeにおけるservice
に相当する機能になります。今回の例だと、mysql
, redis
, puma
のサービスには外部のコンテナまたはクラスタの外部(ブラウザ)からアクセスする必要があるため、Deployment
とService
を一組ずつ定義します。sidekiq
は外から参照する必要がないため、Deployment
のみ定義します。
ConfigMap
とSecret
は、Deployment
に設定する環境変数の管理に使います。今回の使い方ではComposeファイルにおける.dockerenv/mysql
と.dockerenv/rails
に相当します。
使用するマニフェストファイルは以下の11個で、全てk8s/manifests-step1/に置いてあります。一つのYAMLファイルに一つのAPIオブジェクトの定義を書いています。
これから順に内容を説明していきますが、まずはデプロイを実行してみましょう。
準備 の節を参照して、サンプルコードをチェックアウトしminikubeを起動したら、下記のコマンドを実行してください。デプロイが済んだ後、ブラウザでサンプルアプリを開くことができます。
# Railsアプリのイメージをビルド$ (eval $(minikube docker-env) && docker build . -t demoapp:0.0.1)$ cd k8s/manifests-step1# APIオブジェクトを作成$ cat *.yaml | kubectl apply -f -# pumaとsidekiqの起動が完了するまで待機$ kubectl rollout status deploy demoapp-puma$ kubectl rollout status deploy demoapp-sidekiq# puma serviceのエンドポイントをブラウザでオープン$ minikube service demoapp-puma
上記の手順と等価なタスクをMakefileに定義してあるので、代わりにmake
コマンドで実行することもできます。
$ cd k8s/manifests-step1$ make kubectl-apply$ make kubectl-rollout-status$ make minikube-service# またはパラメータなしの make で全て順に実行
APIオブジェクトを削除するには次のようにします。
$ cd k8s/manifests-step1$ cat *.yaml | kubectl delete -f -# または make clean
Makefileは下記のような内容です。GNU Makeで動作を確認しています。
# k8s/manifests-step1/MakefileSHELL = /bin/bashall: $(MAKE) minikube-docker-build $(MAKE) kubectl-apply $(MAKE) kubectl-rollout-status $(MAKE) minikube-serviceclean: kubectl-deleteminikube-docker-build: eval $$(minikube docker-env) && docker build ../../ -t demoapp:0.0.1kubectl-apply: cat *.yaml | kubectl apply -f -kubectl-rollout-status: kubectl rollout status deploy demoapp-puma kubectl rollout status deploy demoapp-sidekiqminikube-service: minikube service demoapp-pumakubectl-delete: cat *.yaml | kubectl delete -f -
スクリプト化するまでもないような短いコマンドであっても、定型的な処理はこのように形として残しておくのがおすすめです。こういった小さな積み重ねがチーム内に暗黙知が生まれることを防ぎます。
次に、各マニフェストファイルの内容を説明します。
はじめにMySQLに関連するマニフェストの内容を説明します。
まずは Deployment
から。
# k8s/manifests-step1/mysql-deploy.yaml---apiVersion: apps/v1kind: Deploymentmetadata: name: demoapp-mysql labels: app: demoapp component: mysqlspec: replicas: 1 selector: matchLabels: app: demoapp component: mysql template: metadata: labels: app: demoapp component: mysql spec: restartPolicy: Always containers: - name: mysql image: mysql:5.7.21 livenessProbe: tcpSocket: port: 3306 readinessProbe: tcpSocket: port: 3306 envFrom: - configMapRef: name: demoapp-mysql-env - secretRef: name: demoapp-mysql-env
最初なので全般事項についてもここで説明します。
.metadata.name
にはdemoapp-mysql
のようにdemoapp-
というプレフィクスをつけます。例えば、Service
オブジェクトの名前をredis
にしてしまうと、それ以降に起動する全てのコンテナにk8sによってREDIS_PORT
のような環境変数が設定されるのですが、sidekiqなど一部のgemはこういった環境変数によって動作が変わってしまう場合があります。名前のプレフィクスはこのような環境変数とgemなどで一般的に使用される環境変数が競合するのを避けるのが目的です。自動設定される環境変数の詳細についてはドキュメントを参照してください。app
ラベルにdemoapp
を、コンポーネントごとにcomponent
ラベルを設定します。コンポーネントはpuma
, sidekiq
, mysql
, redis
などです。mysql
用のDeployment
、Service
には.metadata.name
に共通のdemoapp-mysql
という名前をつけます。また、.metadata.labels
にはapp: demoapp
, component: mysql
の二つを設定します。mysql
の Deployment
であれば mysql-deploy.yaml
となります。略称については 前回のkubectl get
の節を参照してください。次に、mysql
のための固有の設定について説明します。
livenessProbe
とreadinessProbe
は、コンテナの死活監視のための設定項目です。この例では3306番ポートにTCP接続できるかどうかで判定を行なっています。また、上記では使用していませんが、それぞれinitialDelaySeconds
(最初に検査を行うまでの待機秒数)やperiodSeconds
(検査の間隔: デフォルト10秒)などの項目を設定することができます(詳細)。また、コマンドを実行したりHTTPリクエストを投げることもできます。
livenessProbe
で死亡判定されると、ReplicaSet
はrestartPolicy
に基づいてそのPod
の再起動か再作成を試みます。条件が厳しすぎるとプロセスの起動処理中に強制的に再起動されていつまで経っても起動しなくなります。その場合はinitialDelaySeconds
などの値を緩めるなどして調整する必要があります。
readinessProbe
はService
オブジェクトがそのPod
に接続リクエストを転送するかどうかの判定に使います。こちらは起動直後のPod
にリクエストを転送するのを防ぐことなどを目的に使用します。
envFrom
は環境変数を設定するための項目です。この例では ConfigMap
と Secret
という別のAPIオブジェクトを参照して環境変数を設定しています。ConfigMap
とSecret
については後述します。
env
を使うと直接Deployment
の定義の内部に環境変数を定義することもできます。詳細は下記を参照してください。
Define Environment Variables for a Container | Kubernetes
続いてmysql-service.yaml
の内容を説明します。
# k8s/manifests-step1/mysql-svc.yaml---apiVersion: v1kind: Servicemetadata: name: demoapp-mysql labels: app: demoapp component: mysqlspec: ports: - protocol: TCP port: 3306 selector: app: demoapp component: mysql
.spec.selector
にapp
とcomponent
という二つのラベルを指定しています。これはmysql-deploy.yaml
の.spec.template.metadata.labels
と一致させる必要があります。
前回のKubernetes入門編ではapp
というラベル一つでDeployment
とService
を接続していましたが、今回はDeployment
がmysql
の他にredis
やpuma
など複数存在するため、app: demoapp
というラベルだけだとmysql
のService
がredis
やpuma
のPod
を参照して混線することになります。これを避けるためにapp
とcomponent
という二つのラベルをセレクタに指定しています。
次にmysql-env-cm.yaml
を説明します。
# k8s/manifests-step1/mysql-env-cm.yaml---apiVersion: v1kind: ConfigMapmetadata: name: demoapp-mysql-envdata: MYSQL_USER: demoapp MYSQL_DATABASE: demoapp_production
ConfigMap
は環境変数や設定ファイルなどアプリケーションのパラメータを管理するためのAPIオブジェクトです。使用形態には大まかに3つの方法があります。
envFrom
で丸ごと環境変数に設定する。env
でキーを指定して個別に環境変数に設定する。Pod
上のファイルにマウントする。mysql-deploy.yaml
では1の方法を使っています。関係ある部分だけ抜き出すと下記の通りです。
# k8s/manifests-step1/mysql-deploy.yaml---spec: template: spec: containers: - name: mysql image: mysql:5.7.21 envFrom: - configMapRef: name: demoapp-mysql-env # ConfigMapの `.metadata.name` を指定
このように参照すると、ConfigMapの.data
の各エントリが全て環境変数として設定されます。
2の方法で同じことをしようとすると下記のようになります。
# k8s/manifests-step1/mysql-deploy.yaml---spec: template: spec: containers: - name: mysql image: mysql:5.7.21 env: - name: MYSQL_USER # 環境変数名を指定 valueFrom: configMapKeyRef: name: demoapp-mysql-env # ConfigMapの`.metadata.name`を指定 key: MYSQL_USER # ConfigMapの`.data`エントリのキーを指定 - name: MYSQL_DATABASE valueFrom: configMapKeyRef: name: demoapp-mysql-env key: DATABASE
だいぶ冗長になりました。envFrom
は比較的最近追加された機能なので以前はこの方法で書いていましたが大変でした。ConfigMap
はできるだけ用途に応じた単位で分割して、envFrom
を使えるようにした方が良いと思います。
3の方法は、例えば/etc/mysql/my.cnf
のような設定ファイルをConfigMap
で管理する方法です。.data
エントリに定義した値をファイルとしてPod
上の任意のパスにマウントすることができます。この方法についてはこのドキュメントでは扱いませんので、詳細は下記の資料を参照してください。
Configure a Pod to Use a ConfigMap | Kubernetes
ConfigMap
とSecret
については、APIオブジェクトの名前(.metadata.name
)を${プレフィクス}-${コンポーネント}
の形式ではなく、さらに-env
を付け加えてdemoapp-mysql-env
のような形にしています。これは将来的に環境変数に加えて3の方法で設定ファイルを追加する場合に名前が競合するのを防ぐ意図があります。(設定ファイル用のConfigMap
を追加する場合はdemoapp-mysql-conf
のような名前にするのが良いと思います)
最後にmysql-env-secret.yaml
を説明します。
# k8s/manifests-step1/mysql-env-secret.yaml---apiVersion: v1kind: Secretmetadata: name: demoapp-mysql-envdata: MYSQL_PASSWORD: c2VjcmV0 # echo -n "secret" | base64 MYSQL_ROOT_PASSWORD: dG9wc2VjcmV0 # echo -n "topsecret" | base64
Secret
はConfigMap
同様キー・バリュー形式で設定値を管理できるAPIオブジェクトですが、パスワードやクレデンシャルなど機密性の高い情報を管理することを前提としています。
Secret
は、ConfigMap
と比較すると表面的には下記の点が異なります。
.data
エントリに値を書き込むときはBASE64エンコードして書く必要がある。また、Secret
の中身はPod
のtmpfs上に展開されディスクには書き込まれないなど、内部的にはConfigMap
よりも安全性に配慮された運用が行われます。セキュリティ上の制限については下記のドキュメントを参照してください。
Encrypting Secret Data at Rest | Kubernetes
Deployment
からの参照方法はほとんどConfigMap
と同じなので説明を割愛します。configMapRef
の代わりにsecretRef
を使う必要がある点にだけ注意してください。
なお、Secret
オブジェクトのマニフェストファイルにはBASE64エンコードしただけの値を書いているので平文で機密情報を書いているのと同じです。通常はこのままgitリポジトリにコミットすることはしません。実際の運用の際には sopsやyaml_vault とGCP/AWSのKMSで暗号化するのがおすすめですが、それについてはHelm編で紹介したいと思います。
次にRedisに関連するマニフェストの内容を説明します。
まずは Deployment
から。
# k8s/manifests-step1/redis-deploy.yaml---apiVersion: apps/v1kind: Deploymentmetadata: name: demoapp-redis labels: app: demoapp component: redisspec: replicas: 1 selector: matchLabels: app: demoapp component: redis template: metadata: labels: app: demoapp component: redis spec: restartPolicy: Always containers: - name: redis image: redis:4.0.9 livenessProbe: tcpSocket: port: 6379 readinessProbe: tcpSocket: port: 6379 command: - redis-server - --appendonly - "yes"
構造としては mysql-deploy.yaml
とほとんど同じです。
command
に関しては、docker-compose-preview.yaml
では下記のように文字列のエントリとして定義していましたが、k8sのマニフェストでは上記の通り配列として定義する必要がある点に注意してください。
services: redis: image: redis:4.0.9 command: redis-server --appendonly yes
続いて Service
の内容を確認します。
# k8s/manifests-step1/redis-svc.yaml---apiVersion: v1kind: Servicemetadata: name: demoapp-redis labels: app: demoapp component: redisspec: ports: - protocol: TCP port: 6379 selector: app: demoapp component: redis
component
とポート番号が違うだけでmysql
のものとほとんど同じです。特筆すべき点はありません。
いよいよRailsアプリ本体のためのマニフェストの説明に移ります。
まずはpuma
用のDeployment
から説明します。
# k8s/manifests-step1/puma-deploy.yaml---apiVersion: apps/v1kind: Deploymentmetadata: name: demoapp-puma labels: app: demoapp component: pumaspec: replicas: 1 selector: matchLabels: app: demoapp component: puma template: metadata: labels: app: demoapp component: puma spec: restartPolicy: Always containers: - name: puma image: demoapp:0.0.1 imagePullPolicy: IfNotPresent command: - ./bin/setup-db-and-start-puma livenessProbe: httpGet: path: /health_check/full port: 3000 initialDelaySeconds: 30 readinessProbe: httpGet: path: /health_check/full port: 3000 initialDelaySeconds: 30 envFrom: - configMapRef: name: demoapp-rails-env - secretRef: name: demoapp-rails-env
image
にはdocker buildコマンドで指定したイメージ名を指定します。imagePullPolicy
はIfNotPresent
を指定します。前述の通りこのイメージはレジストリに登録していないためです。command
にはdocker-compose-preview.yml
と同じく./bin/setup-db-and-start-puma
を指定します。これはpumaの起動前にrails db:setup
を試みるスクリプトです。詳細は第2回の記事で説明しました。livenessProbe
とreadinessProbe
ではそれぞれ/health_check/full
というパスを指定しています。このサンプルアプリではhealth_checkというgemで死活監視用のエンドポイントを実装しています。このパスを叩くだけでMySQLやRedisとの接続検証を行うことができるようになります。envFrom
では、demoapp-rails-env
という名前のConfigMap
とSecret
を参照しています。これらのオブジェクトはdocker-compose-preview.yml
におけるenv_file
に相当する環境変数の設定を保持しています。次にpuma用のService
の定義を確認します。
# k8s/manifests-step1/puma-svc.yaml---apiVersion: v1kind: Servicemetadata: name: demoapp-puma labels: app: demoapp component: pumaspec: type: NodePort ports: - protocol: TCP port: 3000 selector: app: demoapp component: puma
.spec.type
にNodePort
を指定しています。mysql
やredis
と異なり、puma
の接続相手は他のコンテナではなくクラスタの外にあるブラウザです。そのため、クラスタ内での通信のためのエンドポイントを生成するClusterIP
の代わりにNodePort
を指定しています。
NodePort
を指定すると、k8sクラスタを構成する全てのノードに、このサービスへ接続するためのエンドポイントが作成されます。.ports[*].nodePort
で公開するポートを指定することもできますが、使用中のポートを指定するとエラーになります。無指定の場合は、30000-32767
のレンジから自動的に割り当てられます。いずれの場合も、ポート番号は全てのノードで同じになります。
minikubeの場合、このサービスへ接続するためのURLはminikube service list
で確認することができます。
% minikube service list|-------------|----------------------|----------------------------|| NAMESPACE | NAME | URL ||-------------|----------------------|----------------------------|| default | demoapp-mysql | No node port || default | demoapp-puma | http://192.168.64.25:32320 || default | demoapp-redis | No node port || default | kubernetes | No node port || kube-system | default-http-backend | http://192.168.64.25:30001 || kube-system | kube-dns | No node port || kube-system | kubernetes-dashboard | http://192.168.64.25:30000 ||-------------|----------------------|----------------------------|
また、minikube service demoapp-puma
のようにService
の名前をパラメータとして渡せば、そのURLをブラウザでオープンすることができます。典型的には、http://$(minikube ip):32320
のようにminikube ip
にランダムなポート番号を与えた形式のURLでアクセスできるようになります。
さて、前述のようにNodePort
を指定するとk8sクラスタを構成する全てのノードにエンドポイントが用意されます。minikubeのようにシングルノード構成の検証用クラスタではこれで十分ですが、マルチノード構成の本番環境では不十分です。少なくとも負荷分散や可用性担保のためには前段にロードバランサが必要になるし、k8sの外側でロードバランサの管理をするのは面倒です。
外向けのエンドポイントにロードバランサを組み合わせる場合、通常はLoadBalancer
タイプのService
かIngress
というオブジェクトを使います。次回Kubernetes応用編のStep4ではIngress
を使ってこの問題に対処する方法を示します。
下記のような内容です。MySQLやRedisのホスト名として、それぞれmysql-svc.yaml
とredis-svc.yaml
で定義したService
オブジェクトの名前を指定しています。
# k8s/manifests-step1/rails-env-cm.yaml---apiVersion: v1kind: ConfigMapmetadata: name: demoapp-rails-envdata: RAILS_SERVE_STATIC_FILES: "true" RAILS_LOG_TO_STDOUT: "true" SIDEKIQ_TIMEOUT: "60" MYSQL_HOST: demoapp-mysql MYSQL_USER: demoapp MYSQL_DATABASE: demoapp_production REDIS_HOST: demoapp-redis REDIS_URL: redis://demoapp-redis:6379/1
特筆すべき点は特にありません。
mysql-env-secret.yaml
の説明の際にも述べたとおり、環境変数の値はこの時点では暗号化されていないため、実際の運用においては sopsやyaml_vault で暗号化するなどしてからgitリポジトリに含める必要があります。
# k8s/manifests-step1/rails-env-secret.yaml---apiVersion: v1kind: Secretmetadata: name: demoapp-rails-envdata: SECRET_KEY_BASE: MTIz # echo -n "123" | base64 MYSQL_PASSWORD: c2VjcmV0 # echo -n "secret" | base64
最後にsidekiq用のDeployment
の定義を見ます。
# k8s/manifests-step1/sidekiq-deploy.yaml---apiVersion: apps/v1kind: Deploymentmetadata: name: demoapp-sidekiq labels: app: demoapp component: sidekiqspec: replicas: 1 selector: matchLabels: app: demoapp component: sidekiq template: metadata: labels: app: demoapp component: sidekiq spec: restartPolicy: Always terminationGracePeriodSeconds: 65 containers: - name: sidekiq image: demoapp:0.0.1 imagePullPolicy: IfNotPresent command: - ./bin/start-sidekiq livenessProbe: exec: command: - ./bin/health-check-sidekiq initialDelaySeconds: 30 envFrom: - configMapRef: name: demoapp-rails-env - secretRef: name: demoapp-rails-env
内容としてはほぼpuma-deploy.yaml
と同じですが、下記の点に注目してください。
terminationGracePeriodSeconds
はPod
にTERMシグナルを送ってからプロセスの終了を待つ時間です。この時間を超えるとプロセスはKILLシグナルで強制的に停止されます。デフォルト値は30秒です(参考)。./bin/start-sidekiq
では-t $SIDEKIQ_TIMEOUT
オプションをつけてsidekiq
を起動しており、$SIDEKIQ_TIMEOUT
にはrails-env-cm.yaml
で60秒を指定しているので、terminationGracePeriodSeconds
には65秒を設定しています。ローリングアップデートやコンテナ終了時の動作の詳細についてはStep2で説明します。livenessProbe
はpuma
と異なりexec
タイプで独自実装のコマンドを呼び出します。内容については後述します。Service
と接続しないためreadinessProbe
は定義しません。envFrom
は完全にpuma
と同じです。厳密にいうと環境変数SECRET_KEY_BASE
は無くても動作するのですが、設定を簡素化するためにpuma
と共通化しています。sidekiq
のワーカープロセスには、puma
のようなヘルスチェックのためのインタフェースがありません。sidekiq
のオプションでPIDをファイルに書き出すようにすればプロセスの存在確認くらいはできるのですが、そもそもプロセスが停止しているようなケースではコンテナ自体が終了してしまうためk8s側で死亡判定できます。問題は何らかの原因でsidekiq
プロセスが生きたまま応答しなくなっているようなケースをいかに検出するかです。
sidekiq
のワーカープロセスは定期的に自身のホスト名やプロセスIDなどの情報をRedisに書き込んでいます。この処理はheartbeatと呼ばれています。(詳細はこの素敵な絵文字メソッドを参照)
heartbeatは5秒間隔で実行されますが、60秒間更新が無いと自動的にRedisから削除されるようになっています。heartbeatによってRedisに書き込まれたワーカーのリストはsidekiq
のWeb UI上でも確認することができます。
そしてこのワーカーのリストは、SidekiqのAPIでも取得することができます。
https://github.com/mperham/sidekiq/wiki/API#processes
k8sではPod
のホスト名はPod
の名前と同じになるのでユニークであることが保証されています。livenessProbe
で実行しているスクリプト./bin/health-check-sidekiq
では、このAPIを利用してワーカーのリストに自身のホスト名が含まれているかどうかを確認しています。
#!/bin/bash -ecd $(dirname $0)/.../bin/rake sidekiq:status
# lib/tasks/sidekiq.rakenamespace :sidekiq do desc "Health check for sidekiq worker process." task status: [:environment] do require "socket" hostname = Socket.gethostname if Sidekiq::ProcessSet.new.any? { |ps| ps["hostname"] == hostname } exit 0 else exit 1 end endend
死活監視が意図したように動作するかどうかは、sidekiq
プロセスにSIGSTOP
シグナルを送ってサスペンドすれば確認できます。SIGKILL
やSIGTERM
などでは即座にコンテナが終了するため、livenessProbe
を待たずPod
が再起動される点に注意してください。
$ kubectl get podsNAME READY STATUS RESTARTS AGEdemoapp-mysql-57d56b47cd-ztc5k 1/1 Running 0 2mdemoapp-puma-749c456c87-6vhrl 1/1 Running 0 2mdemoapp-redis-58f795f487-v2x4j 1/1 Running 0 2mdemoapp-sidekiq-669cd7cb6c-bfhpf 1/1 Running 0 2m$ kubectl exec demoapp-sidekiq-669cd7cb6c-bfhpf -- pgrep -l sidekiq31 sidekiq 5.1.3 app [0 of 25 busy]$ kubectl exec demoapp-sidekiq-669cd7cb6c-bfhpf -- pkill -STOP sidekiq
この後、60秒ほど待つとSidekiqのWeb UI上でワーカープロセスが消えるのを確認できます。さらにその後、だいたい10秒に一度の頻度でヘルスチェックが失敗したというログがsidekiq
のPod
のイベントログに表示されます。3回失敗したところでコンテナがKillされて再起動し、再びSidekiqのWeb UI上に新しいワーカーが現れるのを確認できます。
$ kubectl get pods --watchNAME READY STATUS RESTARTS AGEdemoapp-mysql-57d56b47cd-ztc5k 1/1 Running 0 3mdemoapp-puma-749c456c87-6vhrl 1/1 Running 0 3mdemoapp-redis-58f795f487-v2x4j 1/1 Running 0 3mdemoapp-sidekiq-669cd7cb6c-bfhpf 1/1 Running 0 3mdemoapp-sidekiq-669cd7cb6c-bfhpf 1/1 Running 1 5m$ kubectl describe pod demoapp-sidekiq-669cd7cb6c-bfhpf(省略)Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 5m default-scheduler Successfully assigned demoapp-sidekiq-669cd7cb6c-bfhpf to minikube Normal SuccessfulMountVolume 5m kubelet, minikube MountVolume.SetUp succeeded for volume "default-token-dzvgp" Warning Unhealthy 42s (x3 over 1m) kubelet, minikube Liveness probe failed: Normal Pulled 11s (x2 over 5m) kubelet, minikube Container image "demoapp:0.0.1" already present on machine Normal Created 11s (x2 over 5m) kubelet, minikube Created container Normal Started 11s (x2 over 5m) kubelet, minikube Started container Normal Killing 11s kubelet, minikube Killing container with id docker://sidekiq:Container failed liveness probe.. Container will be killed and recreated.
Step1では、第2回Docker Compose/Dockerfile編のdocker-compose-preview.yml
に相当する構成をDeployment
, Service
, ConfigMap
, Secret
の4種のAPIオブジェクトで記述しました。
この時点では下記に挙げる三つの制約があります。
rails db:setup
が並列実行されてエラーになる。これらの制約を解消する方法は次回Kubernetes応用編で取り上げたいと思います。
今回は下記について書きます。
kubectl
コマンドのチュートリアルKubernetesには膨大な機能があるので、最初から汎用的な使い方を学ぼうとすると挫折しがちです。このドキュメントでは、紹介する機能や概念を「初心者がRailsアプリを動かすために必要な機能」という観点で限定し、かつ段階的に説明していきます。
まずは minikubeを使ってmacOS上の仮想マシンにスタンドアローンのk8sクラスタを作り、そこに前回 Docker Compose/Dockerfile編 で使用したサンプルアプリをデプロイする具体的な手順を示します。
紹介するサンプルコードは全て下記のリポジトリにあります。
https://github.com/kwhrtsk/rails-k8s-demoapp
下記のツールを使います。
minikube
k8sクラスタをローカルVM上に構築するためのツールです。hyperkitのドライバ
minikubeがmacOS上でVMを操作するために必要です。kubectl
k8sクラスタを操作するためのCLIツールです。helm
k8sのマニフェストをパッケージとして管理するためのツールです。第六回 Helm編で使用します。stern
k8s上のコンテナのログを見るためのツールです。必須ではありませんがあると便利です。GNU Make
XCode付属のものを使います。無くても大丈夫です。minikubeとkubectlとsternはHomebrew(cask)でインストールできます。
$ brew cask install minikube$ brew install kubernetes-cli kubernetes-helm$ brew install stern
hyperkitのドライバは次の手順でインストールしてください。参考
$ curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \&& chmod +x docker-machine-driver-hyperkit \&& sudo mv docker-machine-driver-hyperkit /usr/local/bin/ \&& sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit \&& sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit
macOS版のminikubeでは、hyperkitの代わりにVirtualBoxを使うこともできます。全ての手順は両方で動作を確認していますが、hyperkitの方が高速です。筆者の環境ではRailsアプリやMySQLなど一式k8sにデプロイするのにかかる時間はVirtualBoxだとおおよそ2分20秒前後、hyperkitだと50秒前後でした。
k8sはDocker Composeと同じく複数のコンテナを管理するためのツールです。
Docker Composeでは一つのホスト上に複数のコンテナを起動することができましたが、k8sでは複数のDockerサーバで一つのクラスタを構成し、その上に複数のコンテナを分散配置することができます。
また、Docker Composeは単にコンテナの起動や停止だけでなく、コンテナ間通信やボリュームの管理を行うことができましたが、k8sはさらにロードバランサや定期実行ジョブなど実環境で必要になる多様な機能を抽象化し管理することができます。
k8sをセットアップして使えるようにするためには色々な方法があります。代表的なものをいくつか紹介します。
本番環境でk8sを運用するためには上記のようなものを使う必要がありますが、k8sを試してみるだけなら手元の環境で動作するminikubeを使うのが一番簡単です。
minikubeは端末上のVMへk8sをインストールして簡単に使えるようにするためのツールです。VMドライバとしてはVirtualBox、hyperkit、KVMなどがサポートされています。
macOS上で試すのであれば VirtualBox か hyperkit が簡単です。デフォルトでは VirtualBox が選択されるようですが、hyperkitの方が高速なのでこちらを使うのがおすすめです。
hyperkitはmacOS組み込みの仮想化フレームワーク「Hypervisor Framework」を使うためのツールで、Docker for Macでも使われています。
次節以降ではminikubeの基本的な使い方を説明します。
minikube start
: k8sクラスタ用VMの作成と起動下記のコマンドを実行すると、CPUコア3つ、メモリ2GBを割り当ててminikube専用のVMインスタンスを起動することができます。
# hyperkitを使う場合$ minikube start --cpus=3 --memory=2048 --vm-driver=hyperkit --disk-size=12g# virtualboxを使う場合$ minikube start --cpus=3 --memory=2048 --vm-driver=virtualbox
このドキュメントで紹介する手順は全て上記の構成で動作を確認しています。
hyperkitを使う場合、--disk-size
を指定しないと無条件に20GBのファイルが~/.minikube/machines/minikube/minikube.rawdisk
に作成されます。ホスト側のストレージが足りない場合はこれが原因で起動に失敗することがあるので注意してください。上記の例では12GB割り当てています。
VirtualBoxを使う場合、ストレージファイルは必要に応じて拡張されていくのでサイズ指定は省略しています。ストレージの消費量は起動直後で2GB程度です。
minikube dashboard
: k8sダッシュボード下記のコマンドを実行すると、ブラウザでk8sのダッシュボードが開かれます。
$ minikube dashboard
このダッシュボード上では、k8sクラスタ上の各種オブジェクトの状態の確認や編集を行うことができます。
minikube docker-env
: VM上のdockerdに接続するための環境変数の表示minikubeが動作しているVM上でもDockerプロセスが動作しています。
普段 docker image ls
などのコマンドを実行するとDocker for MacのDockerプロセスに接続していますが、下記のようにminikube docker-env
で出力される環境変数をロードするとminikube VM上のDockerプロセスに接続することができます。
$ eval $(minikube docker-env)$ docker image lsREPOSITORY TAG IMAGE ID CREATED SIZEk8s.gcr.io/kube-proxy-amd64 v1.10.0 bfc21aadc7d3 5 weeks ago 97MBk8s.gcr.io/kube-apiserver-amd64 v1.10.0 af20925d51a3 5 weeks ago 225MBk8s.gcr.io/kube-controller-manager-amd64 v1.10.0 ad86dbed1555 5 weeks ago 148MBk8s.gcr.io/kube-scheduler-amd64 v1.10.0 704ba848e69a 5 weeks ago 50.4MB(省略)
通常、自分でビルドしたイメージをk8s上でコンテナとして起動するためには、何らかのレジストリサービスにそのイメージを登録する必要があるのですが、このドキュメントでは手順を簡単にするためにminikube VM上のDockerプロセスに直接接続してRailsアプリのイメージをビルドする方法を紹介します。これについての詳細は次回Kubernetes応用編で説明します。
minikube delete
: k8sクラスタ用VMの停止と削除下記のコマンドを実行すると、k8s用に作成したVMを丸ごと削除します。気軽にk8sクラスタの初期化を行えるのがminikubeの良いところです。
$ minikube delete
k8sではコンテナを直接操作することはありません。Deployment
やService
など抽象化されたリソースを介してコンテナの起動や停止を行います。これらのリソースをk8sではAPIオブジェクト
と呼びます(単にオブジェクトと呼ばれることもあります)。
k8sクラスタにAPIオブジェクトを作成したり更新したりする方法には大まかに3つの方法があります。
kubectl apply -f
で指定して実行する。kubectl create deployment
やkubectl run
、kubectl expose
などのコマンドを実行する。インフラ構成を管理する上で一番望ましいのは、構成をコード化してバージョン管理できる1の方法です。このドキュメントでは、一部の例外をのぞいて全て1の方法で説明します。
一部の例外というのは以下の二つです。詳細は後述します。
Secret
オブジェクトの作成まずサンプルアプリのコードをチェックアウトしてminikubeを起動してください。
# サンプルアプリのコードをチェックアウト$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git$ cd rails-k8s-demoapp# minikubeを起動$ minikube start --cpus=3 --memory=2048 --vm-driver=hyperkit --disk-size=12g# kubernetesのダッシュボードをオープン$ minikube dashboard
この節ではAPIオブジェクトの例としてDeployment
とService
を取り上げ、kubectl
コマンドを使用してオブジェクトの作成などを行う方法を説明します。
Deployment
の概要k8sの最も重要なAPIオブジェクトの一つはDeployment
です。状態を持たないアプリケーションのコンテナはDeployment
オブジェクトによって管理します。
Deployment
オブジェクトを作成すると、その配下へ自動的にReplicaSet
とPod
というオブジェクトが作成されます。
Pod
はk8sにおいてコンテナを管理するための最小単位です。多くの場合、コンテナと一対一に対応しています。(一つのPod
に複数のコンテナを入れることもできますが、それは高度なユースケースであるとドキュメントにも書いてありますし、ここでは取り上げません)
ReplicaSet
は、同じ設定で作られたPod
がreplicas
パラメータで指定された個数だけ維持されるように調整します。
+--------------+ | Deployment | +------+-------+ | +------+-------+ | ReplicaSet | replicas: 2 +-+----------+-+ | | +---+---+ +---+---+ | Pod | | Pod | +-------+ +-------+
Deployment
は、ReplicaSet
を管理します。コンテナイメージの更新などを行う際には新しい設定のReplicaSet
を作成して、古いReplicaSet
からPod
を削除しながら新しいReplicaSet
にPod
を作成します。
+--------------+ | Deployment | +------------+-+ : | +------------+-+ +-+----------+-+ | ReplicaSet | -> | ReplicaSet | +-+----------+-+ +-+----------+-+ | | | | +---+---+ +---+---+ +---+---+ +---+---+ | Pod | | Pod | | Pod | | Pod | +-------+ +-------+ +-------+ +-------+
Deployment
のマニフェストファイルの例以下のYAMLデータはmysql:5.7.21
のコンテナを一つ起動するだけのDeployment
のマニフェストファイルです。
# k8s/manifests-step0/mysql-deploy.yaml---apiVersion: apps/v1 # このYAMLデータのスキーマバージョンkind: Deployment # オブジェクトの種類metadata: name: mysql # オブジェクトの名前 labels: # オブジェクトを選択するために使うラベル(後述) app: mysql # 任意のキー・値を設定して良いspec: replicas: 1 # 起動するPodの数 selector: matchLabels: # .spec.template.metadata.labelsと一致させる必要がある app: mysql template: metadata: labels: # ReplicaSetとPodに付けられるラベル app: mysql spec: restartPolicy: Always # コンテナが死亡した際の対応 containers: - name: mysql # コンテナのNAME属性に使用 image: mysql:5.7.21 # コンテナイメージ env: # 環境変数を指定 - name: MYSQL_ALLOW_EMPTY_PASSWORD value: "yes"
簡単な注釈を加えていますが、わかりにくい部分をいくつか補足します。
.apiVersion
apps/v1
の他に、v1
、extensions/v1beta1
などの値を取る場合があり、kind
とapiVersion
の組み合わせによってYAMLデータの構造が決まります。
各オブジェクトのapiVersion
の推奨値はk8sのAPIバージョンによって決まっていて、例えば2018年5月30日時点で最新のk8sのバージョン1.10では、Deployment
のapiVersion
の推奨値はapps/v1
になります。
参考: Workloads API changes in versions 1.8 and 1.9 | Kubernetes
.metadata.labels
ラベルです。他のAPIオブジェクトの設定からこのDeployment
を参照する際に使用する場合があります。また、kubectl get
など一部のCLIツールでオブジェクトをフィルタリングする際にも使用できます。
.spec.selector.matchLabels
以前のバージョンでは省略できたのですが、apiVersion
がapps/v1
になって必須パラメータになりました。必ず .spec.template.metadata.labels
にマッチしている必要があります。書き方によっては、別のDeployment
で定義したオブジェクトを参照してしまう場合があるので注意が必要です。
.spec.template.spec.restartPolicy
コンテナが停止した時に再起動するかどうかをAlways, OnFailure, Neverのいずれかで指定します。参考
このファイルはサンプルコードのk8s/manifests-step0/mysql-deploy.yamlに置いてあります。
以降、このマニフェストファイルを使ってkubectl
コマンドの基本的な使い方を説明します。
kubectl apply
: APIオブジェクトの作成と更新前述のYAMLファイルを使ってk8sクラスタにDeployment
オブジェクトを作るには下記のようにします。
$ kubectl apply -f k8s/manifests-step0/mysql-deploy.yamldeployment.apps "mysql" created
複数のYAMLファイルを指定する場合は下記のように標準入力で指定します。
$ cat k8s/manifests-step0/*.yaml | kubectl apply -f -
この際、各オブジェクトの定義が ---
で区切られている必要がある点に注意してください。上記のようにcat
で結合しやすいように、あらかじめ各YAMLファイルの先頭に ---
を入れておくのが良いと思います。
作成したオブジェクトの設定を更新したい場合は、マニフェストファイルを編集してもう一度同じコマンドを実行します。
以前のバージョンでは、作成はkubectl create -f
、更新はkubectl apply -f
と使い分けていましたが、現在は両方ともkubectl apply -f
を使います。
kubectl get
: APIオブジェクトの状態の確認Deployment
、ReplicaSet
、Pod
オブジェクトの状態を確認するには、下記のようにします。
$ kubectl get deployments,replicaset,podsNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGEdeployment.extensions/mysql 1 1 1 1 1sNAME DESIRED CURRENT READY AGEreplicaset.extensions/mysql-55fb9dc7db 1 1 1 1sNAME READY STATUS RESTARTS AGEpod/mysql-55fb9dc7db-5mh8v 1/1 Running 0 1s
kubectl get
のパラメータにはAPIオブジェクトの種類を示す文字列を指定しますが、略称がある場合はそれを使えます。
# kubectl get deploymentsと同じ$ kubectl get deploy
このドキュメントで扱う主要なAPIオブジェクトの略称は下記の通りです。
名称 | 略称 |
---|---|
deployments | deploy |
configmaps | cm |
deployments | deploy |
ingresses | ing |
jobs | 無し |
namespaces | ns |
persistentvolumeclaims | pvc |
persistentvolumes | pv |
pods | po |
replicasets | rs |
secrets | 無し |
services | svc |
storageclasses | sc |
完全なリストはパラメータ無しで kubectl get
を実行すると表示されます。
kubectl logs
(またはstern): コンテナのログの確認コンテナのログを確認するには、オブジェクトの種類とNAMEを指定して下記のようにします。NAMEは前述のkubectl get
で確認できます。
# Deploymentを指定する場合$ kubectl logs deployments/mysql# ReplicaSetを指定する場合$ kubectl logs replicasets/mysql-55fb9dc7db# Podを指定する場合$ kubectl logs pods/mysql-55fb9dc7db-5mh8v
これはdocker container logs
相当の機能です。コンテナの標準出力と標準エラー出力の履歴を確認できます。
Pod
を指定する場合、マニフェストファイルで設定したラベル(.spec.template.metadata.labels
)の値を使って、下記のように指定できます。
$ kubectl logs --selector "app=mysql"# ラベルが複数ある場合はカンマ区切りでAND指定$ kubectl logs --selector "app=demoapp,component=mysql"
ログの閲覧は stern を使うともっと簡単です。Pod
の名前に対して正規表現でマッチさせることができ、複数のPod
のログを一度にtailすることができます。
$ stern "mysql.*"
kubectl port-forward
: コンテナのポートをホスト側にポートフォワードDocker Composeではports
エントリをYAMLファイルに書いておくだけでコンテナのポートをホスト側にマッピングすることができましたが、k8sではクラスタの外側からコンテナに接続するためにはService
やIngress
など別のAPIオブジェクトを設定する必要があります。
ただし、kubectl port-forward
コマンドを使うと、k8sクラスタのノードにsshで接続して特定のコンテナのポートをホストOSにポートフォワードすることができます。このコマンドではDeployment
またはPod
の名前をパラメータに指定する必要があります。
# Deploymentを指定する場合$ kubectl port-forward deployment/mysql 3306:3306# TYPEを省略するとPodを指定したことになる$ kubectl port-forward mysql-55fb9dc7db-5mh8v 3306:3306
ポート番号は左側がホスト側のポートです。コンテナとホストのポートが同じ場合は3306
のように番号を一つだけ指定することもできます。
別のターミナルで下記のコマンドを実行すると、k8s上のmysqlコンテナに接続することができます。オブジェクトの起動直後はエラーになる場合があるので、数秒待ってから試してみてください。
$ mysql -u root --host 0.0.0.0 --port 3306
Pod
の名前を指定する場合は、下記のようにするとラベルでオブジェクトを選択することができます。
$ kubectl port-forward $(kubectl get pods --selector "app=mysql" --output "jsonpath={.items..metadata.name}") 3306
kubectl apply -f
で指定したファイルを指定してオブジェクトを削除することができます。
$ kubectl delete -f k8s/manifests-step0/mysql-deploy.yaml
Service
の概要k8sの最も重要なAPIオブジェクトのもう一つはService
です。
前節ではmysql
コンテナを管理するDeployment
の例を示しましたが、別のPod
からこのMySQLサーバにアクセスするためにはPod
のIPアドレスとポートを指定する必要があります。Pod
のIPアドレスは起動するたびに変わってしまうので、アプリケーションの設定に記述することはできません。
Service
オブジェクトを作ると、ラベルで指定したPod
群へアクセスできるIPアドレスとDNS名がk8sによって用意されます。
前節のmysql
のDeployment
に対応するService
定義の例を示します。
# k8s/manifests-step0/mysql-svc.yaml---apiVersion: v1kind: Servicemetadata: name: mysql labels: app: mysqlspec: type: ClusterIP ports: - protocol: TCP port: 3306 # 接続するPod側のポートを指定 selector: # 接続するPodのラベルを指定 app: mysql
上記のマニフェストをkubectl apply
で実行してService
オブジェクトを作ると、k8sクラスタ上のコンテナ上ではService
オブジェクトの名前である"mysql"
という文字列をホスト名として指定することでmysql
のDeployment
が管理しているPod
にアクセスできるようになります。
.spec.type
にはClusterIP
、NodePort
、LoadBalancer
、ExternalName
のいずれかを指定します。ClusterIP
は、このService
オブジェクトが作るエンドポイントがk8sクラスタの内部向けであることを意味します。k8sクラスタの外側からアクセスできるようなエンドポイントを作る場合にはNodePort
またはLoadBalancer
を指定します。このドキュメントでは、最終的にはIngress
オブジェクトを使用して外部向けのエンドポイントを用意するため、NodePort
やLoadBalancer
の詳細については割愛します。また、ExternalName
を使うとk8sの外側のサービスのエイリアスを作ることができますが、このドキュメントでは扱いません。
kubectl run
: コマンドによるDeployment
の作成インフラ構成管理の観点からは全てのDeployment
はマニフェストファイルで管理するのが望ましいのですが、k8sクラスタ上に一時的なコンテナを作成して簡単なオペレーションを実行したい時もあります。
そのような場合にはkubectl run
コマンドを使うとYAMLファイル無しでDeployment
を作成することができ便利です。フォアグラウンドで対話的なコマンドを実行することもできるため、トラブルシュートにも最適です。
利用例として、前節までに紹介したmysql
用のDeployment
とService
を作成した後、kubectl run
コマンドでシェルを起動し、Service
によって用意されたエンドポイント経由でMySQLサーバに接続する方法を示します。
まずmysql
のDeployment
とService
を作成します。
$ kubectl apply -f k8s/manifests-step0/mysql-deploy.yamldeployment.apps "mysql" created$ kubectl apply -f k8s/manifests-step0/mysql-svc.yamlservice "mysql" created$ kubectl get deployments,replicasets,pods,servicesNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGEdeployment.extensions/mysql 1 1 1 1 9sNAME DESIRED CURRENT READY AGEreplicaset.extensions/mysql-b59b886c9 1 1 1 9sNAME READY STATUS RESTARTS AGEpod/mysql-b59b886c9-955tg 1/1 Running 0 9sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 1dservice/mysql ClusterIP 10.110.107.218 <none> 3306/TCP 4s
service/kubernetes
はシステム標準のService
です。service/mysql
というService
オブジェクトが作成されているのを確認できます。
次に下記のコマンドで新たにmysql:5.7.21
イメージのDeployment
を作成します。コンテナの中ではmysqld
コマンドの代わりに対話モードでbash
を起動して、プロンプトからMySQLサーバに接続します。
$ kubectl run mysql-client --image=mysql:5.7.21 -it --rm --restart=Never /bin/bashIf you don't see a command prompt, try pressing enter.root@mysql-client:/#
上記のようにシェルのプロンプトが表示されたらmysql
コマンドを入力してください。
root@mysql-client:/# mysql -u root --host mysqlWelcome to the MySQL monitor. Commands end with ; or \g.Your MySQL connection id is 2Server version: 5.7.21 MySQL Community Server (GPL)Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql>
MySQLサーバに接続できました。kubectl run
コマンドのパラメータの意味は次の通りです。
mysql-client
はDeployment
の名前です。--image
はコンテナのイメージ名です。-it
オプションはdocker container run(旧 docker run)
コマンドと同じ意味です。--rm
オプションを指定すると、コマンドの終了時に自動的にDeployment
削除されます。--restart=Never
をつけておかないとDeployment
が削除されるまでのわずかな間にPod
が再起動されるケースがあり、動作がやや不安定になります。kubectl explain
: ドキュメントの表示すでに見てきたように、マニフェストファイルは結構複雑な構造のYAMLデータです。慣れないうちはリファレンスを見ながら書くことになると思いますが、公式サイトのドキュメントは個々の機能の解説に注力しているため、今ひとつマニフェスト定義自体の全貌をつかむことができません。
マニフェストの構造を確認するためにはAPIのリファレンスを参照する方が確実です。
また、kubectl
には指定したオブジェクトのAPIリファレンスを参照する機能があります。Webよりもこちらの方が便利かもしれません。
$ kubectl explain --api-version=apps/v1 deploymentKIND: DeploymentVERSION: apps/v1DESCRIPTION: Deployment enables declarative updates for Pods and ReplicaSets.FIELDS: apiVersion <string> APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources kind <string> Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds metadata <Object> Standard object metadata. spec <Object> Specification of the desired behavior of the Deployment. status <Object> Most recently observed status of the Deployment.
オブジェクトの名前に続けて.
区切りでフィールドを指定するとネストした要素のドキュメントを参照できます。また、オブジェクトの名前はkubectl get
と同じ略称を使うことができます。
$ kubectl explain --api-version=apps/v1 deployment.spec.template.spec.containers.resourcesKIND: DeploymentVERSION: apps/v1RESOURCE: resources <Object>DESCRIPTION: Compute Resources required by this container. Cannot be updated. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources ResourceRequirements describes the compute resource requirements.FIELDS: limits <map[string]string> Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ requests <map[string]string> Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
Kubernetesのより詳細な情報は下記のドキュメントを参照してください。
kubectl
コマンドのチートシートです。1度目を通しておくと引き出しが増えるのでおすすめです。おすすめの書籍はこの2冊です。
一冊めの「プログラマのためのDocker教科書」は前回の記事でも紹介した書籍です。おおよそ2章分と比較的大きく紙面を割いてKubernetesの概要と基礎的なAPIオブジェクトの説明がわかりやすく書いてあります。また、GCP上で開発を行う方法がコードの管理からイメージのビルドまで丁寧に説明してあり、付録でGCPの使い方まで説明されていて手厚いです。
一方、Pod
の死活監視の設定やService
によるサービスディスカバリの詳細など、少し踏み込んだ内容は大胆に省略してあります。もう少し詳しい仕組みを知りたい場合は二冊目の「入門Kubernetes」で補うのが良いと思います。これ一冊で入門するのは正直しんどいと思うのですが、ある程度知識を持った状態で読むのであればおすすめです。
次回は Kubernetes基礎編 です。
]]>今回は下記について書きます。
簡単なRailsアプリを例に、Docker Composeの使い方やDockerfileの書き方を説明します。このサンプルアプリrails-k8s-demoapp
のコードは下記のリポジトリに置いています。
https://github.com/kwhrtsk/rails-k8s-demoapp
rails-k8s-demoapp
は「フォームでメッセージを送信すると画面上のリストに追加して表示する」だけの小さなアプリです。
仕様は極小ですが、できるだけ一般的なRailsアプリの構成に近くなるように下記の要件を満たすものにしています。
下記のコマンドで動作を確認することができます。RubyやRailsの開発環境は必要ありません。gitとDockerがインストールされていれば動きます。
$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git$ cd rails-k8s-demoapp$ docker-compose -f docker-compose-preview.yml up -d$ open http://localhost:3000/
docker-compose
コマンドの使い方やサンプルアプリケーションで使っているdocker-compose.yml
とDockerfile
については順に説明します。
複数のコンテナをYAML形式の構成ファイルで一括管理するツールです。
docker-compose.yml
です。Railsアプリ開発において、Docker Composeには大きく二つの用途があります。
以降、それぞれの詳細について説明します。
一般的なRailsアプリケーションでは、PostgreSQLやMySQLなどのRDBMSに加えて、しばしばRedisやElasticsearchなどのミドルウェアを使用します。従来、macOS上でRailsアプリの開発を行う場合にはこれらのミドルウェアのmacOS版をインストールし、サービスとして起動しておく必要があったため、READMEには長々とセットアップの手順を書いたりしていました。
一方、先に挙げたようなメジャーなプロダクトはいずれも公式のDockerイメージが存在するため、適切に書かれた docker-compose.yml
ファイルさえあれば、docker-compose up
コマンドを一つ実行するだけで必要なミドルウェアのイメージの取得からコンテナの起動まですべて自動で行うことができます。
また、コンテナとしてミドルウェアを起動する際には個別にバージョンやデータの保存場所を指定できるため、導入の手順が簡単になるだけでなく下記のようなメリットもあります。
前述のサンプルアプリケーションではMySQLとRedisを使います。この2つを起動するためのdocker-compose.yml
は下記のような内容です。通常、このファイルはプロジェクトのルートディレクトリに置きます。
version: "3"services: mysql: image: mysql:5.7.21 environment: - MYSQL_ROOT_PASSWORD=$MYSQL_PASSWORD ports: - 3306:3306 volumes: - ./tmp/mysql:/var/lib/mysql redis: image: redis:4.0.9 ports: - 6379:6379 volumes: - ./tmp/redis:/data command: redis-server --appendonly yes
services
の下にコンテナ(Docker Comoposeの文脈ではサービス)の定義を書いていきます。
mysql
については、前回Docker編のdocker container run(mysql編) に出てきた docker container run
コマンドとやっていることはほぼ同じです。
environment
: 環境変数を追加してコンテナを起動します。.envrc
に書かれている環境変数 MYSQL_PASSWORD
をrootユーザのパスワードとして設定するため、MYSQL_ROOT_PASSWORD
の値として設定しています。.envrc
の内容をシェルに設定するという想定です。MYSQL_ALLOW_EMPTY_PASSWORD
にyes
を設定するとroot
のパスワードを空にすることもできます。ports
: コンテナのポート(右側)をホスト側のポート(左側)にマッピングします。volumes
: ホスト側のパス(左側)をコンテナ上のパス(右側)にマッピングします。docker container run
の-v
オプションとは異なり、ホスト側の相対パスで指定できます。redis
のcommand
には、データをストレージに永続化するためのオプションを指定しています。
Composeファイルには多数の機能があります。詳細については下記のリファレンスを参照してください。
mysql
とredis
のイメージに指定できる環境変数やコマンドのオプションについては、公式リポジトリのドキュメントを参照してください。
まず、.envrc
に書かれた環境変数を設定してください。
$ source .envrc# direnvを使っている場合は下記でも可$ direnv allow
下記のコマンドを実行すると、Docker Hubからmysql
とredis
のイメージを取得してコンテナが起動します。(-d
はバックグラウンドで起動するという意味です)
-f
で任意のComposeファイルを指定できますが、省略するとカレントディレクトリのdocker-compose.yml
が参照されます。
$ docker-compose up -dCreating network "railsk8sdemoapp_default" with the default driverPulling mysql (mysql:5.7.21)...5.7.21: Pulling from library/mysql2a72cbf407d6: Pull complete38680a9b47a8: Pull complete4c732aa0eb1b: Pull completec5317a34eddd: Pull completef92be680366c: Pull completee8ecd8bec5ab: Pull complete2a650284a6a8: Pull complete5b5108d08c6d: Pull completebeaff1261757: Pull completec1a55c6375b5: Pull complete8181cde51c65: Pull completeDigest: sha256:691c55aabb3c4e3b89b953dd2f022f7ea845e5443954767d321d5f5fa394e28cStatus: Downloaded newer image for mysql:5.7.21Pulling redis (redis:4.0.9)...4.0.9: Pulling from library/redisb0568b191983: Pull complete6637dc5b29fe: Pull complete7b4314315f15: Pull complete2fd86759b5ff: Pull complete0f04862b5a3b: Pull complete2db0056aa977: Pull completeDigest: sha256:6b9f935e89af002225c0dcdadf1fd74245b4cc1e3e91222f7e4769c236cf80d4Status: Downloaded newer image for redis:4.0.9Creating railsk8sdemoapp_redis_1 ... doneCreating railsk8sdemoapp_mysql_1 ... done
2回目以降は取得済みのイメージを使用するため、起動は少し早くなります。
MySQLとRedisが起動した後であれば、下記のように開発環境用の各種プロセスを起動できます。
$ gem install foreman$ bundle install --path=vendor/bundle$ yarn install$ ./bin/rails db:setup$ foreman start -f Procfile
foremanでは、puma
、sidekiq
、webpack-dev-server
の三つを起動します。(ruby, node, yarnが必要です)
# Procfileweb: ./bin/rails s -p 3000worker: ./bin/sidekiqclient: ./bin/webpack-dev-server
docker-compose.yml
に定義されたサービスのコンテナ一覧を表示します。
$ docker-compose psName Command State Ports-----------------------------------------------------------------------------------------railsk8sdemoapp_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcprailsk8sdemoapp_redis_1 docker-entrypoint.sh redis ... Up 0.0.0.0:6379->6379/tcp
コンテナの名前は、${プレフィクス}_${サービス名}_${連番}
になります。プレフィクスはカレントディレクトリから-
や_
を除いた文字列になります。連番がついているのは一つのサービスに複数のコンテナが起動し得るためです。(今回は紹介しませんが、docker-compose scale
コマンドでサービスごとのコンテナの数を増減させることができます)
下記のコマンドを実行すると、docker-compose.yml
に書かれたすべてのサービスのコンテナを停止した後、削除します。-v
オプションは関連するデータボリュームも削除するという意味です。(ホスト側のボリュームをマウントしている場合は残ります)
$ docker-compose down -vStopping railsk8sdemoapp_mysql_1 ... doneStopping railsk8sdemoapp_redis_1 ... doneRemoving railsk8sdemoapp_mysql_1 ... doneRemoving railsk8sdemoapp_redis_1 ... doneRemoving network railsk8sdemoapp_default
RailsアプリのDockerイメージを作る方法を説明します。
Dockerイメージを作るには、まずDockerfile
にイメージの作り方を記述します。
サンプルアプリ rails-k8s-demoapp に同梱している下記のようなDockerfile
を例に説明します。
### image for buildFROM ruby:2.5.1-alpine AS build-envARG RAILS_ROOT=/appARG BUILD_PACKAGES="build-base curl-dev git"ARG DEV_PACKAGES="libxml2-dev libxslt-dev mysql-dev yaml-dev zlib-dev nodejs yarn"ARG RUBY_PACKAGES="tzdata yaml"ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"WORKDIR $RAILS_ROOT# install packagesRUN apk update \ && apk upgrade \ && apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGES# install rubygemCOPY Gemfile Gemfile.lock $RAILS_ROOT/RUN bundle install -j4 --path=vendor/bundle# install npmCOPY package.json yarn.lock $RAILS_ROOT/RUN yarn install# build assetsCOPY . $RAILS_ROOTRUN bundle exec rake webpacker:compileRUN bundle exec rake assets:precompile### image for executionFROM ruby:2.5.1-alpineLABEL maintainer 'Kawahara Taisuke <kwhrtsk@gmail.com>'ARG RAILS_ROOT=/appARG PACKAGES="tzdata yaml mariadb-client-libs bash"ENV RAILS_ENV=productionENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"WORKDIR $RAILS_ROOT# install packagesRUN apk update \ && apk upgrade \ && apk add --update --no-cache $PACKAGESCOPY --from=build-env $RAILS_ROOT $RAILS_ROOT
Dockerfile
では、上記の例のようにFROM
やRUN
などのコマンドを一行に一つ書きます。各コマンドの詳細は下記のリファレンスを参照してください。docker image build
コマンドでイメージの作成を行うと、Dockerfile
に書かれたコマンドが上から順に実行されます。
このDockerfile
でrails-k8s-demoapp
のイメージをビルドするには下記のようにします。
$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git$ cd rails-k8s-demoapp$ docker image build . -t demoapp:latest
.
はビルドコンテキストです。通常、Dockerfile
が置いてあるパスを指定します。-t
はイメージの名前です。
docker build
です。ドキュメントはこちらの方が詳しいです。これでRailsアプリのイメージができました。下記のコマンドでコンテナを起動できます。
$ docker container run -it --rm demoapp:latest lsGemfile libGemfile.lock logProcfile node_modulesREADME.md package.jsonRakefile publicapp specbin storageconfig testconfig.ru tmpdb tsconfig.jsondocker-compose-preview.yml vendordocker-compose.yml yarn.lockk8s
この例ではls
コマンドを実行してコンテナのファイルを表示しています。上記のようにアプリケーションルートディレクトリの中身が表示されるはずです。
次にDockerfile
の中身を順に解説していきます。
FROM
コマンドではベースイメージを指定します。サンプルのDockerfile
では2回出てきますが、このケースでは2つイメージを作っています。
FROM ruby:2.5.1-alpine AS build-env# ...FROM ruby:2.5.1-alpine# ...COPY --from=build-env $RAILS_ROOT $RAILS_ROOT
前半がgemやnpmのC拡張やjsやcssなどのアセットの ビルド用イメージ で、後半がアプリケーションとして運用する 実行用イメージ です。後半では前半のビルドの結果を単にコピーしています。このように2段階に分けてイメージを作成している理由は、イメージのサイズを小さくするためです。詳細は レイヤについて で説明します。
最終的なイメージの成果物は後者の 実行用イメージ です。前者は削除しても構いませんが、残しておくと2回目以降のビルドが差分で実行されるため速くなります(これについても後述)。
次にベースイメージのruby:2.5.1-alpine
について説明します。Docker Hubのrubyリポジトリには大まかに3系統のタグがあります。
ruby:<version>
: Debian stretchベースruby:slim
: Debian stretchベースだがインストールされたパッケージが少ないruby:alpine
: Alpine Linuxベース今回はイメージサイズが最も小さいruby:2.5.1-alpine
を使います。
% docker imagesruby 2.5.1-alpine b620ae34414c 9 days ago 55.5MBruby 2.5.1-slim 85b814a932e6 9 days ago 172MBruby 2.5.1 1624ebb80e3e 9 days ago 863MB
また、Alpine Linuxのパッケージマネージャであるapk
はDebianのapt
と比べてNode.jsやYARNのインストールやキャッシュの制御がより簡単というメリットもあります。
ENV
とARG
はどちらもコンテナに環境変数を設定するコマンドですが、ARG
で指定した環境変数はイメージのビルド時にだけ設定され、作成済みのイメージをコンテナ化した際には残っていないという特徴があります。また、docker image build
コマンドの--build-arg
やdocker-compose.yml
のargs
オプションで上書きすることができます。例えば、プロキシ環境下でイメージをビルドする際にHTTP_PROXY
のような環境変数を指定する際には、ENV
ではなくARG
を使うのが望ましいです。
このサンプルではインストールするパッケージの名前などをARG
で設定しています。また、ENV
では環境変数 RAILS_ENV
を production
に設定しています。
### image for build# ...# アプリケーションのインストール先ARG RAILS_ROOT=/app# gemやnpmのC拡張やjs, cssなどのアセットのビルドに必要なパッケージARG BUILD_PACKAGES="build-base curl-dev git"ARG DEV_PACKAGES="libxml2-dev libxslt-dev mysql-dev yaml-dev zlib-dev nodejs yarn"ARG RUBY_PACKAGES="tzdata yaml"ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"### image for execution# ...# Railsアプリの実行に必要なパッケージARG PACKAGES="tzdata yaml mariadb-client-libs bash"ENV RAILS_ENV=productionENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
BUNDLE_APP_CONFIG
はrubyイメージがもともと持っている環境変数です。このDockerfile
では、ビルド済みのbundleディレクトリを丸ごと実行イメージにコピーしたいので、gemのインストール先を${RAILS_ROOT}/vendor/bundle
に指定しています。このようにインストール先を変更した場合には BUNDLE_APP_CONFIG
を上記のように上書きしないとbundle exec
が正常に動作しません。
ただし、この挙動は紛らわしいという指摘もあるため、将来の更新で修正されるかもしれません。
WORKDIR
はこのイメージから作成したコンテナ上でコマンドを実行するときのカレントディレクトリです。この例では /app
をRailsアプリのルートディレクトリとして指定しているので、WORKDIR
も同じパスにしています。
ARG RAILS_ROOT=/appWORKDIR $RAILS_ROOT
また、RUN
やCOPY
などのDockerfile
のコマンドもWORKDIR
で指定したパスで実行されます。
RUN
はコマンドを実行します。主にパッケージのインストールやアプリケーションのビルドなどを行います。
COPY
はホスト側のファイルをコンテナ側に複製します。この時、.dockerignore
ファイルで指定されたファイルは複製されません。パスワードやクレデンシャルのような秘匿値を書いたファイルは忘れずに.dockerignore
に追加してください。また、.dockerignore
に一致したファイルはイメージのビルド時にdockerd
へ転送されなくなるため、.git
やnode_modules
などビルド時に不要でかつサイズやファイル数が大きいディレクトリも指定するのがセオリーです。
今回ベースイメージにしているのはruby:2.5.1-alpine
というイメージですが、これはAlpine Linuxというディストリビューションをベースにしています。Alpine Linuxではapk
というパッケージマネージャを使います。使用可能なパッケージを下記のサイトで検索できます。Alpine Linux packages
RUN apk update \ && apk upgrade \ && apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGES
Dockerイメージのデータはレイヤと呼ばれる単位で記録されており、Dockerfile
のRUN
やCOPY
などのコマンドは実行するたびに新しいレイヤに結果が記録されます。またイメージのビルドや送受信はレイヤ単位でキャッシュされて差分実行されるため、適切な単位でレイヤを分割しなければビルドやデプロイに無駄な時間がかかるようになります。特に、一度追加したファイルは別のレイヤで削除したとしても以前のレイヤに残り続けるため、イメージ全体のファイルサイズは減らない点に注意が必要です。
一般的に、Dockerfile
ではイメージのファイルサイズを減らしたりビルドやデプロイの速度を上げるために次のような工夫をします。
Gemfile
、Gemfile.lock
のコピーとbundle install
の実行はアプリケーションコードのCOPY
より前で個別に行う。gemの追加は比較的頻度の少ない作業なので、こうしておくとbundle install
の頻度を減らしてビルドを高速化できます。package.json
とyarn.lock
のコピー、yarn
の実行についても同様です。
このようにすることでビルドに必要なパッケージが含まれるレイヤを丸ごと削除できます。
「1つのRUN
コマンドでパッケージのインストール、ビルド、パッケージやキャッシュの削除を行う」ことでも同じことを実現できますが、パッケージのインストールとビルドのレイヤが同じになるので、アプリのコードを更新しただけでもビルド時にはパッケージのインストールからやり直しになり、余分に時間がかかるようになります。COPY
コマンドの--from
オプションは比較的最近追加された機能なので、古いドキュメントにはよくこのようなやり方が書いてあります(古いランタイムに配慮して止むを得ずこのような実装になっているケースもあるかもしれません)。
RailsアプリのDockerイメージを作れるようになったので、次は「Ruby/Railsの開発環境がなくても、DockerさえあればRailsアプリのイメージを作成して起動できるようにする」ためのComposeファイルを用意します。
ローカルでの開発用に docker-compose.yml
がすでにあるので、docker-compose-preview.yml
というファイル名で用意します。
version: "3"services: puma: image: demoapp build: context: . env_file: - .dockerenv/rails ports: - 3000:3000 depends_on: - mysql - redis command: ./bin/setup-db-and-start-puma sidekiq: image: demoapp env_file: - .dockerenv/rails depends_on: - puma command: ./bin/start-sidekiq mysql: image: mysql:5.7.21 env_file: - .dockerenv/mysql redis: image: redis:4.0.9 command: redis-server --appendonly yes
コードをチェックアウトした後、下記のコマンドを実行するだけでRailsアプリが http://localhost:3000/ に起動するようになります。
$ docker-compose -f docker-compose-preview.yml up -d
ログを見たい場合は-d
オプションを外すか、下記のようにしてください。
$ docker-compose -f docker-compose-preview.yml logs -f
後始末は下記のようにします。-v
を外すとデータボリュームが残ってしまうので注意してください。
$ docker-compose -f docker-compose-preview.yml down -v
このYAMLファイルのポイントは下記の通り。
mysql
とredis
にはpuma
とsidekiq
のコンテナから接続できれば良いので、ports
(ホスト側へのポートマッピング)エントリを書きません。puma
にはbuild
エントリを追加して、docker-compose up
実行時にイメージをビルドするようにしています。--build
オプションをつける必要があります。puma
はdepends_on
でmysql
を指定して、mysql
コンテナの起動後にrake db:setup
が実行されるようにしています。nc
でmysql
の3306番ポートを確認して起動するまでループします。後述。puma
、sidekiq
、mysql
にはenv_file
を指定し、共通の環境変数をファイルで一括指定します。後述。pumaとsidekiqのcommand
では、./bin/setup-db-start-puma
, ./bin/start-sidekiq
というコマンドをそれぞれ指定しています。これは下記のような内容です。
# ./bin/setup-db-and-start-pumacd $(dirname $0)/..trap "pkill -P $$" EXIT./bin/wait-for $MYSQL_HOST 3306./bin/wait-for $REDIS_HOST 6379./bin/rails db:setup_if_not_yet./bin/pumactl start
# ./bin/start-sidekiqcd $(dirname $0)/..trap "pkill -P $$" EXIT./bin/wait-for $MYSQL_HOST 3306./bin/wait-for $REDIS_HOST 6379./bin/sidekiq -t ${SIDEKIQ_TIMEOUT:-8}
pumaやsidekiqの起動をシェルスクリプトでラップする場合、ラッパ側のシェルがTERMやINTなどのシグナルで停止すると子プロセスのpumaやsidekiqが起動したまま残ってしまいます。上記のtrap
はそれを防ぐための記述で、スクリプトの停止時に子プロセスへTERMシグナルを送るように指定しています。
sidekiqの-t
オプションは実行中のジョブを強制的に停止するまでのタイムアウト値で、オプションを指定しない場合のデフォルト値は8秒です。このスクリプトでは環境変数SIDEKIQ_TIMEOUT
でこの値を変更できるようにしています。環境変数が存在しない場合はデフォルト値と同じ8秒が指定されます。
./bin/wait-for
は下記のような内容で、パラメータで指定したホストのポートをnc
コマンドでチェックして接続可能になるまで待機するだけのスクリプトです。
# ./bin/wait-forHOST=$1PORT=$2while :do nc -w 1 -z $HOST $PORT if [[ $? = 0 ]]; then break; fi sleep 1done
db:setup_if_not_yet
は下記のようなRakeタスクで、まだrake db:setup
を実行したことがなさそうな時だけ実行します。
# lib/tasks/db.rakenamespace :db do task setup_if_not_yet: [:environment] do begin ActiveRecord::Base.connection rescue ActiveRecord::NoDatabaseError # database not exists Rake::Task["db:setup"].invoke exit 0 else if !ActiveRecord::SchemaMigration.table_exists? # database exists but tables not exists Rake::Task["db:setup"].invoke exit 0 end end endend
.dockerenv/rails
はpuma
とsidekiq
に共通の環境変数の設定ファイルです。
RAILS_SERVE_STATIC_FILES=trueRAILS_LOG_TO_STDOUT=trueSIDEKIQ_TIMEOUT=60SECRET_KEY_BASE=123MYSQL_HOST=mysqlMYSQL_USER=demoappMYSQL_PASSWORD=secretMYSQL_DATABASE=demoapp_productionREDIS_HOST=redisREDIS_URL=redis://redis:6379/1
.dockerenv/mysql
はmysql
用の環境変数の設定ファイルです。デーベース名、ユーザ名、パスワードを書いています。
MYSQL_USER=demoappMYSQL_PASSWORD=secretMYSQL_DATABASE=demoapp_productionMYSQL_ROOT_PASSWORD=topsecret
公式のmysqlイメージでは、rootユーザのパスワードの他、コンテナの起動時に作成するデータベースとそのデータベースにアクセスできるユーザとパスワードも環境変数で指定できます。.dockerenv/mysql
で設定している4つの環境変数はそのためのものです。
また、Railsアプリ側ではMySQLやRedisの接続先など実行環境に依存するようなパラメータを全て環境変数で受け取れるようにしておく必要があります。
まずデータベースの設定ファイルでは、MYSQL_HOST
, MYSQL_DATABASE
, MYSQL_USER
, MYSQL_PASSWORD
でそれぞれDBのホスト名、データベース名、ユーザ名、パスワードを受け取れるようにしておきます。(話を簡単にするため、mysql
コンテナと環境変数の名前を合わせています)
# config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch("MYSQL_USER") { "root" } %> password: <%= ENV.fetch("MYSQL_PASSWORD") { "" } %> host: <%= ENV.fetch("MYSQL_HOST") { "0.0.0.0" } %>development: <<: *default database: demoapp_developmenttest: <<: *default database: demoapp_testproduction: <<: *default database: <%= ENV.fetch("MYSQL_DATABASE") { "demoapp_production" } %>
また、ActionCableの設定ファイルでは環境変数 REDIS_URL
でRedisの接続先を指定できるようにしておきます。
# config/cable.ymldevelopment: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>test: adapter: asyncproduction: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: demoapp_production
Sidekiqについてはもともと環境変数 REDIS_URL
で接続先を切り替えられる仕様なので特に何もしなくて良いです。別の方法で接続先を指定したい場合は config/initializers/sidekiq.rb
というファイルを作って設定を追加します。詳細は下記のドキュメントを参照してください。
Using Redis mperham/sidekiq Wiki
REDIS_HOST
は前述のbin/wait-for
に渡すパラメータとして使っています。
SECRET_KEY_BASE
はCookieに改ざん検知のためのHMACダイジェストをつけるときの秘密鍵を指定するための環境変数ですが、Rails 5.2.0のデフォルトの動作ではCredentialsで暗号化されたファイル config/credentials.yml.enc
に書かれた値を鍵として使うようになっています。下記のコマンドでこのファイルの中身を確認できます。
% ./bin/rails credentials:show# aws:# access_key_id: 123# secret_access_key: 345# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.secret_key_base: f758dcb894cf0fee1b68f7989ebd65f5fd0dbb3c366e61789c112836789dfda22def856b6419ae96447352bbdd8873b90ae0096cd61d9532775caca8c04e0783
ただ、この値は本番環境でも使う値になるので、今回のような不特定多数に配布するのが前提のプレビュー環境には使えません。HMAC用の鍵はCredentials の値よりも環境変数SECRET_KEY_BASE
の値が優先される実装になっているので、今回は環境変数を設定します。また、Credentialsの秘密鍵(環境変数RAILS_MASTER_KEY
またはconfig/master.key
)は設定しません。(デフォルトでは設定しなくてもエラーにはなりません。)
RAILS_SERVE_STATIC_FILES
は、jsやcssなどのファイルをpumaが応答するかどうかを指定するための設定値です。これはRails標準の仕組みで、実装は config/environments/production.rb
を参照してください。本番環境ではこのような静的なアセットファイルはnginxやCDNで配信するのが望ましいのですが、今回は構成を簡単にするためにtrueにしておきます。
RAILS_LOG_TO_STDOUT
は、production
環境でRailsのログ出力先を標準入力に切り替えるための環境変数です。これもRails標準の仕組みで、実装は config/environments/production.rb
を参照してください。
こういった環境変数設定用のファイルに本番環境の値を書く場合には、GitリポジトリやDockerイメージに含めないように注意してください。.gitignore
と .dockerignore
にそれらのファイル名を書いておくと意図せず混入させることを防ぐことができます。
本稿では、docker-compose-preview.yml
で構築するのはあくまでプレビュー用の環境であり、本番環境はKubernetesで構築するという前提です。Kubernetes上のアプリに環境変数を設定する際には全く別の方法を用いるため、Composeファイルなどに書いた設定値は全て本番環境とは異なる値という想定なので、リポジトリにコミットしています。
また、env_file
で指定するファイルは、direnv
で使う.envrc
などシェルに環境変数を設定するスクリプトとは根本的に異なるものなので下記の点に注意してください。
export
はつけない。'
や"
を含む形で設定されてしまいます)コンテナにはそれぞれ固有のIPアドレスが割り当てられます。docker container run
コマンドで起動したコンテナ同士が互いのIPアドレスを知るためにはオプション指定が必要ですが、docker-compose
で起動したコンテナ同士は、互いのサービス名をホスト名として通信できるように自動的に設定されます。また、ホスト側からコンテナに接続する際にはports
エントリでポートマッピングの設定を書く必要がありましたが、docker-compose
で起動したコンテナ同士はこういった設定なしで互いの全てのポートにアクセスできます。
MySQLの接続設定をもう一度掲載します。下記のようにMYSQL_HOST
環境変数でホスト名を指定するようになっています。
# config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch("MYSQL_USER") { "root" } %> password: <%= ENV.fetch("MYSQL_PASSWORD") { "" } %> host: <%= ENV.fetch("MYSQL_HOST") { "0.0.0.0" } %># 省略production: <<: *default database: <%= ENV.fetch("MYSQL_DATABASE") { "demoapp_production" } %>
.dockerenv/rails
では下記のようにmysql
という文字列をMYSQL_HOST
環境変数に設定していました。
MYSQL_HOST=mysql
このmysql
というのは、docker-compose-preview.yml
のサービス名を指しています。
version: "3"services: # 省略 mysql: # <- これがサービス名 image: mysql:5.7.21 # 省略
docker container run
コマンドに--link
オプションをつけるとdocker-compose
が自動的に準備してくれていたようなことを手動で行うこともできますが、Railsアプリ開発という主題においては必要になる場面はほぼ無いと思いますので、詳細は割愛します。
少なくともホストOSがmacOSなのであれば、ローカルの開発環境においてはRailsアプリまでDockerコンテナの上で動かす必要はないと筆者は考えています。
./bin/rails g
や./bin/rails c
などちょっとしたコマンドが全てdocker
コマンド経由になるのは煩雑すぎる。以上の理由で、私の主観ではメリットをデメリットが上回っていると感じるので、開発環境においては下記のような構成をとっています。
docker-compsoe
で起動する。Docker Composeのより詳細な使い方は下記のドキュメントを参照してください。
docker-compose
コマンドについてdocker-compose.yml
)についてDockerfileのより詳細な仕様については下記のドキュメントを参照してください。
おすすめの書籍は「プログラマのためのDocker教科書」です。2018/4/11に第2版が出ました。
前回のDocker編で扱った範囲も含めて、本ドキュメントでは紹介しなかったコマンドやDockerfile/Composeファイルの機能が一通り紹介されています。また、コンテナ技術の概要や使いどころ、プライベートレジストリやイメージの公開方法などDocker周辺を広く浅く解説してあり、入門には良い書籍だと思います。後半ではKubernetesにも軽く触れてあります。
次回は Kubernetes入門編 です。
]]>この記事はシリーズ連載記事の第一回です。シリーズ全体の概要は次の通りです。
はじめに、DockerやKubernetesの概要について簡単に説明しつつ、docker
コマンドやkubectl
コマンドの基本的な使い方をチュートリアル風味で紹介します。
次に、ごく小さな仕様の具体的なRailsアプリを題材として、Docker Composeの構成ファイルやKubernetesのマニフェストの具体的なサンプルと使い方を示し、最終的にHelmを使ってKubernetesクラスタにデプロイする方法を説明します。
DockerとKubernetesの全機能を網羅的に学ぼうとすると膨大な時間が必要になるので、この記事では「RailsアプリをKubernetesで運用すること」にフォーカスして一つ一つの概念や機能の詳細は必要最小限に絞り、より詳細な資料へのポインタを置くに留めます。
シリーズ全体の構成は次の通りです。
docker
コマンドのチュートリアルkubectl
コマンドのチュートリアルkubectl
コマンドとYAML形式のマニフェストファイルでRailsアプリをk8sにデプロイする方法全てのサンプルコードはここにあります。
https://github.com/kwhrtsk/rails-k8s-demoapp
brew tap caskroom/cask
それぞれ下記のバージョンで動作を確認しています。
名称 | バージョン |
---|---|
macOS | High Sierra(10.13.4) |
Docker for Mac | 18.0.3.1-ce-mac65 |
Ruby | 2.5.1 |
Rails | 5.2.0 |
minikube | 0.27.0 (k8s 1.10.0) |
kubectl | 1.10.2 |
helm | 2.9.1 |
hyperkit | v0.20171204-60-g0e5b6b |
virtualbox | 5.2.8 |
Docker初心者がRailsアプリをk8sで動かすまでに最低限必要なDockerについての知識とdocker
コマンドの操作方法を書きます。Rails固有のトピックを知りたい人は Docker Compose/Dockerfile編 までスキップしてください。
LXC(Linux Containers)と呼ばれる技術を使ってアプリケーションの開発やデプロイを行うためのツール及びプラットフォームです。LXCやDockerについて解説されたドキュメントは山ほどあるのでここでは詳細は割愛します。
2017年1月にリリースされたDocker 1.13で docker
コマンドの構成が整理されました。アナウンスは Introducing Docker 1.13 - Docker BlogのCLI restructured
の部分です。
「旧コマンドは引き続きサポートするが、新コマンドの使用を勧める」とのことなのでこのドキュメントでは新しいコマンド体系で説明します。ただ、古いドキュメントには旧コマンド体系で書かれているものも多いため、対応する旧コマンドについても紹介します。
このドキュメントで紹介するコマンドの新旧対応は下記の通りです。基本的にはimageかcontainerのサブコマンドへの移動ですが、一部違うものがあるので太字にしています。
旧コマンド | 新コマンド | 説明 |
---|---|---|
docker pull | docker image pull | イメージを取得する |
docker images | docker image ls | イメージの一覧を表示する |
docker rmi | docker image rm | イメージを削除する |
docker run | docker container run | コンテナを起動する |
docker ps | docker container ls | コンテナの一覧を表示する |
docker exec | docker container exec | 起動中のコンテナで新しいコマンドを実行する |
docker logs | docker container logs | コンテナのログを表示する |
docker rm | docker container rm | コンテナを削除する |
以降の節で順にこれらのコマンドの使い方を説明します。見出しの括弧は旧コマンドです。
また、各節の終わりにリファレンスへのリンクを置いていますが、少なくとも本稿の執筆時点においては旧コマンドの方が詳細に書かれているので、必要に応じてそちらも参照してください。なお日本語版のドキュメントにはまだ新コマンド版のリファレンスはありません。
docker
のサブコマンドは、 861162a44
のようなハッシュ値や、romantic_neumann
のようにランダムな英単語の組み合わせで自動生成されたコンテナ名をパラメータとして受け取ります。
Docker for Macにはbashやzshでコマンドの補完を行うためのスクリプトが同梱されていますが、ただインストールするだけでは有効になりません。補完のための設定については別に記事を書いたのでこちらを参照してください。
bash/zshとfzfでDocker関連コマンドの補完を行う方法
イメージとはコンテナの雛形です。docker image pull
コマンドでリポジトリとタグを指定し、イメージを取得します。MySQLの公式リポジトリから5.7.21
のタグが付いたイメージを取得する場合は下記のようにします。
$ docker image pull mysql:5.7.215.7.21: Pulling from library/mysql2a72cbf407d6: Pull complete38680a9b47a8: Pull complete4c732aa0eb1b: Pull completec5317a34eddd: Pull completef92be680366c: Pull completee8ecd8bec5ab: Pull complete2a650284a6a8: Pull complete5b5108d08c6d: Pull completebeaff1261757: Pull completec1a55c6375b5: Pull complete8181cde51c65: Pull completeDigest: sha256:691c55aabb3c4e3b89b953dd2f022f7ea845e5443954767d321d5f5fa394e28cStatus: Downloaded newer image for mysql:5.7.21
タグは、多くの場合バージョンを指します。ただし、ここでいうバージョンはイメージのバージョンではなく、アプリケーションのバージョンであることが多いため、同じタグを指定してもタイミングによって違うイメージを取得するケースがある点に注意が必要です。
例えばmysql:5.7
は現時点ではmysql:5.7.21
と同じイメージを指していますが、以前はmysql:5.7.20
を指していました。タグを指定しないとlatest
というタグを指定したとみなされます。
リポジトリを管理しているサービスをレジストリと呼びます。MySQLやRedisなどの主要なプロダクトの公式イメージは Docker Hub というレジストリで配布されており、上記の例ではDocker Hub上のリポジトリからイメージを取得しています。Docker Hub上のリポジトリは下記のページで探すことができます。
https://hub.docker.com/explore/
Docker Hub以外のレジストリを使う場合は下記のようにリポジトリの前にレジストリのホスト名とポート番号を書きます。例えばローカルレジストリ myregistry.local:5000
から testing/test-image
を取得する場合は下記のようになります。
$ docker image pull myregistry.local:5000/testing/test-image
docker pull
です。ドキュメントはこちらの方が詳しいです。持っているイメージの一覧を表示します。
$ docker image lsREPOSITORY TAG IMAGE ID CREATED SIZEmysql 5.7.21 5195076672a7 3 weeks ago 371MB
docker images
です。ドキュメントはこちらの方が詳しいです。docker image ls
で表示された IMAGE ID
を指定します。
$ docker image rm 5195076672a7Untagged: mysql:5.7.21Untagged: mysql@sha256:691c55aabb3c4e3b89b953dd2f022f7ea845e5443954767d321d5f5fa394e28cDeleted: sha256:5195076672a7e30525705a18f7d352c920bbd07a5ae72b30e374081fe660a011Deleted: sha256:bc52f6d08bc65c22baab4384ae534d4c5ba8c988197de49975e0a0f78310dd89Deleted: sha256:b2590548a0917767b420cf20d0cef3aae8912314de216f624c0840f3ad827aa7Deleted: sha256:756d63a7d5896b52d445ea84ee392cb08a7c119322cfcdfed6303de1ed0d0eabDeleted: sha256:8e4736576db75536185beba95c5877deeb3915740688cbbc17fe04aed3632282Deleted: sha256:e6e6e1bb8a16eadbe6628770767615fbc8d67bf11dde69a902116efe847baa7eDeleted: sha256:080b6c4ec1d55d91a7087e12ae3bd4df252148d94f9911209e0a83d50dc63784Deleted: sha256:58b97da9f98f75af01ae59c3cb1fdd07a07297015459f3f9f88b140699b29147Deleted: sha256:3918448e7fe95f36f67a55c938559bab787249b8fa5c7e9914afd46994d045b0Deleted: sha256:fac8373d1ec4f5bb6c13f12170f558edc3cfbfe8215ae3d1c869940401bc14cfDeleted: sha256:130f3e567e288fdbbc3ae7cd7aa6c8b3d952bebd3eae58f0a7da93acbb22a258Deleted: sha256:3358360aedad76edf49d0022818228d959d20a4cccc55d01c32f8b62e226e2c2
docker rmi
です。ドキュメントはこちらの方が詳しいです。docker container run
コマンドを使います。下記のようなフォーマットです。
$ docker container run [OPTIONS] IMAGE [COMMAND] [ARGS...]
指定したIMAGEで新しいコンテナを起動し、COMMANDを実行します。ARGSはCOMMANDに対する引数です。
COMMANDを省略した場合は、そのイメージに定義されたデフォルトのコマンドが実行されます。
mysql:5.7.21
イメージでbashを実行する場合は下記のようにします。
$ docker container run -it --rm mysql:5.7.21 /bin/bash
オプションはそれぞれ下記のような意味です。
-it
: シェルのような対話型のCLIコマンドを起動する場合に指定します。--rm
: プロセス終了時にコンテナを削除したい場合に指定します。(指定しないとコンテナの残骸が残ります)-i
と-t
は別々のオプションですが、セットで指定することが多いオプションです。詳細はリファレンスで確認してください。
なお、指定したイメージがない場合は自動的にレジストリからダウンロードされるため、事前にdocker image pull
を実行する必要はありません。
docker run
です。ドキュメントはこちらの方が詳しいです。mysql:5.7.21
イメージでコンテナを起動し、ローカルから接続してみます。
$ docker container run --rm -d -p 3306:3306 -v $(pwd)/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysql --name db1 mysql:5.7.2140a551e33878d12c0c2fff330e6af0ad487be2235919a49ebc29dd4c084ddcf1
表示されたのはCONTAINER ID
です。後述するコンテナへの操作を行う際には、このIDか--name
で指定した名前を指定します。今回はCOMMANDを指定していないので、自動的にmysqld
コマンドが実行されます。mysql
イメージの内容については Docker HubのmysqlのページにあるDockerfileへのリンクで確認できます。
オプションはそれぞれ下記のような意味です。
--rm
: プロセス終了時にコンテナを削除したい場合に指定します。(指定しないとコンテナの残骸が残ります)-d
: コンテナをバックグラウンドで起動します。-p 3306:3306
: コンテナのポートをホスト側のポートにマッピングします。左側がホスト側のポートです。-v $(pwd)/data:/var/lib/mysql
: ホスト側のパスをコンテナ上のパスにマッピングします。左側がホスト側のパスで、絶対パスで指定する必要があります。-e MYSQL_ROOT_PASSWORD=mysql
: 環境変数を追加してコンテナを起動します。--name db1
: コンテナの名前です。省略するとランダムな値が自動的に与えられます。MYSQL_ROOT_PASSWORD
のように、イメージによっては環境変数を指定することでコンテナの動作を変えることができる場合があります。mysql
イメージにおける環境変数の仕様は このページのEnvironment Variables
以下に書かれています。大体の場合はDocker Hubのリポジトリのページに説明があります。
-v
オプションは、Data Volume
の指定に使います。コンテナを削除するとコンテナに書き込まれたデータは全て削除されますが、データベースのようなプロダクトではそれだと困るので、永続化データをコンテナの外部に持つ仕組み(Data Volume)があります。今回のケースではホスト側のカレントディレクトリ以下のdata/
というディレクトリをコンテナ上の/var/lib/mysql/
にマウントしています。mysql
イメージのデフォルト設定では/var/lib/mysql/
はMySQLのデータディレクトリなので、ホスト側でdata/
ディレクトリを削除しない限り、コンテナを削除しても同じパスにマウントしてまたコンテナを起動すればデータベースの内容を維持できます。Data Volumeの詳細については下記のドキュメントを参照してください。
なお、起動したコンテナ上のmysqldには下記のコマンドで接続できます。(mysqlコマンドがない場合、brew install mysql
でインストールできます)
$ mysql --host 127.0.0.1 -uroot -pmysql
docker run
です。ドキュメントはこちらの方が詳しいです。$ docker container ls -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES40a551e33878 mysql:5.7.21 "docker-entrypoint.s…" Less than a second ago Up 2 seconds 0.0.0.0:3306->3306/tcp db1
-a
オプションは停止中のコンテナも表示するという意味です。
docker ps
です。ドキュメントはこちらの方が詳しいです。CONTAINER ID
またはdocker container run
でコンテナを起動するときに--name
オプションで指定した名前でコンテナを指定します。以下の例では、先ほどバックグラウンドで起動したmysqlコンテナでbashを起動し、ターミナルからmysqladminコマンドを実行しています。オプションの -it
の意味は docker container run
と同じです。
% docker container exec -it db1 /bin/bashroot@40a551e33878:/# mysqladmin status -pmysqlmysqladmin: [Warning] Using a password on the command line interface can be insecure.Uptime: 43 Threads: 1 Questions: 4 Slow queries: 0 Opens: 105 Flush tables: 1 Open tables: 98 Queries per second avg: 0.093root@40a551e33878:/# exit
docker exec
です。ドキュメントはこちらの方が詳しいです。docker container logs
コマンドを使うと、BG実行したコンテナのログを端末に表示できます。CONTAINER ID
またはdocker container run
でコンテナを起動するときに--name
オプションで指定した名前でコンテナを指定します。
$ docker container logs -f db1(省略)2018-04-08T05:04:39.040525Z 0 [Note] Event Scheduler: Loaded 0 events2018-04-08T05:04:39.041069Z 0 [Note] mysqld: ready for connections.Version: '5.7.21' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
-f(--follow)
オプションを指定すると、コンテナのSTDOUTとSTDERRから新しい出力があれば表示し続けます。
docker logs
です。ドキュメントはこちらの方が詳しいです。CONTAINER ID
またはdocker container run
でコンテナを起動するときに--name
オプションで指定した名前でコンテナを指定します。
# コンテナの停止$ docker container stop db1# コンテナの削除$ docker container rm db1
または
# コンテナを停止して削除$ docker container rm -f db1
docker stop
です。ドキュメントはこちらの方が詳しいです。docker rm
です。ドキュメントはこちらの方が詳しいです。より詳細な使い方は下記のドキュメントを参照してください。
また、対応しているバージョンが少し古いですが、日本語化されたドキュメントもあります。
次回は Docker Compose/Dockerfile編 です。おすすめの書籍もこちらで紹介しています。
]]>また、fzfを使って候補のフィルタリングや複数選択を楽に行えるようにする方法についても説明します。
最終的にはこんな感じになります。
Docker for Macにはbashとzsh用の補完スクリプトが同梱されています。
% ls -1 /Applications/Docker.app/Contents/Resources/etc/docker-compose.bash-completiondocker-compose.zsh-completiondocker-machine.bash-completiondocker-machine.zsh-completiondocker.bash-completiondocker.fish-completiondocker.zsh-completion
適切に設定すれば下記のようにサブコマンドやイメージIDなどを補完できます。
下記のように補完スクリプトのリンクを作って ~/.zshrc
で読み込むようにすると、docker
コマンドとdocker-compose
コマンドに補完が効くようになります。
$ mkdir ~/.zsh/completions$ ln -s /Applications/Docker.app/Contents/Resources/etc/docker.zsh-completion ~/.zsh/completions/_docker$ ln -s /Applications/Docker.app/Contents/Resources/etc/docker-compose.zsh-completion ~/.zsh/completions/_docker-compose
# ~/.zshrcif [ -e ~/.zsh/completions ]; then fpath=(~/.zsh/completions $fpath)fiautoload -U compinitcompinit
bash-completion
が必要です。Homebrewでインストールできます。
$ brew install bash-completion
下記のように補完スクリプトのリンクを作って ~/.bash_profile
で読み込むようにすると、docker
コマンドとdocker-compose
コマンドに補完が効くようになります。
$ ln -s /Applications/Docker.app/Contents/Resources/etc/docker-compose.bash-completion /usr/local/etc/bash_completion.d/$ ln -s /Applications/Docker.app/Contents/Resources/etc/docker.bash-completion /usr/local/etc/bash_completion.d/
# ~/.bash_profileif [ -f $(brew --prefix)/etc/bash_completion ]; then . $(brew --prefix)/etc/bash_completionfi
fzfと組み合わせると、docker rm
やdocker rmi
のようなコマンドで、パラメータのコンテナやイメージをフィルタしたり複数選択したりできます。
Homebrewでインストールできます。
$ brew install fzf
fzfのインストールスクリプトを実行します。
$ /usr/local/opt/fzf/install
質問に答えると ~/.fzf.zsh
と~/.fzf.bash
が作成されます。~/.zshrc
に下記を追加してこれを有効にします。
# ~/.zshrc[ -f ~/.fzf.zsh ] && source ~/.fzf.zshexport FZF_COMPLETION_TRIGGER="," # default: '**'
FZF_COMPLETION_TRIGGERは、fzfによる補完を開始するトリガーです。ここで指定した文字列の後にタブを入力するとfzfによる補完が始まります。デフォルトでは **
ですが、zshのグロブ補完と重複していて気持ち悪いので、上記の例では,
に変更しています。この記事のスクリーンキャストでもこの設定を有効にしていて、docker rmi ,
まで入力してからタブを押して補完を開始しています。
下記のリポジトリから docker-fzf.zsh
を取得して、~/.zshrc
で読み込みます。
https://github.com/kwhrtsk/docker-fzf-completion
git cloneでもコピペでもなんでもいいので、ローカルマシンのどこかに置いてsource
で読み込んでください。bashの場合も docker-fzf.bash
を取得して ~/.bash_profile
で source
すれば使えると思います。
自分の場合はzshで下記のようにしています。
$ ghq get https://github.com/kwhrtsk/docker-fzf-completion# これと同じ# mkdir -p mkdir ~/.ghq/github.com/kwhrtsk/# cd ~/.ghq/github.com/kwhrtsk/# git clone https://github.com/kwhrtsk/docker-fzf-completion
# ~/.zshrcsource ~/.ghq/github.com/kwhrtsk/docker-fzf-completion/docker-fzf.zsh
上記のスクリプトは下記のリポジトリをフォークして作った二次著作物です。(MITライセンス)
https://github.com/Mike-Now/docker-fzf-completion
オリジナルに対して下記の修正を行っています。
attach
, kill
, logs
, stats
, history
などコンテナとイメージに関するものほぼ全部container
とimage
のサブコマンド群ほぼ全部start
には停止中のコンテナしか補完しないetcリポジトリ名:タグ
が空(<none>:<none>
)の場合は代わりにIMAGE_ID
を使うように修正docker run
やdocker rm
のようなコマンドの後、FZF_COMPLETION_TRIGGER
で指定した文字列(下記の例では,
)を入力してタブを押すと補完が始まります。
Ctrl+P
、Ctrl+N
でカーソルを上下に移動します。Enter
を押すとカーソル位置のコンテナ名またはイメージIDがコマンドのパラメータとして入力されます。Ctrl+A
で全選択、Ctrl+D
で全選択解除、Ctrl+T
で全選択をトグルします。いつものことですが、2018-04-11現在、Rails 5.2.0だと正常に動作しないgemがあります。とりあえず見つけたものをメモしておきます。
Gemfileでsimple_formのバージョンを指定していない場合、railsのバージョンを5.2.0に上げてbundle update
したらsimple_formのバージョンが3.5.1から1.4.1まで下がるという現象が起きていました。上記はそのために引き起こされるエラーです。
単にgemspecの要求バージョンが厳しかったみたいで、このブログを書いている間に 4.0.0 がリリースされて解決しました。
https://github.com/plataformatec/simple_form/blob/master/CHANGELOG.md#400
3.5系でバージョンをロックしている人は4.0.0を試してみましょう。
# Gemfile# gem "simple_form", "~> 3.5.1"gem "simple_form", "~> 4.0.0"
Annotate no longer works with Rails 5.2 models Issue #538 · ctran/annotate_models
./bin/rails annotate_models
または ./bin/rails db:migrate
など、モデルのアノテーションを更新する際に下記のようなエラーが起きる場合があります。
Unable to annotate app/models/author.rb: can't modify frozen StringUnable to annotate app/models/author.rb: no implicit conversion of nil into Array
暫定的な措置として、設定でインデックスに関する情報の記入を無効化すればエラーを回避できます。
diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rakeindex 9088128..9c2f5d5 100644--- a/lib/tasks/auto_annotate_models.rake+++ b/lib/tasks/auto_annotate_models.rake@@ -16,7 +16,7 @@ if Rails.env.development? 'position_in_serializer' => 'before', 'show_foreign_keys' => 'true', 'show_complete_foreign_keys' => 'false',- 'show_indexes' => 'true',+ 'show_indexes' => 'false', 'simple_indexes' => 'false', 'model_dir' => 'app/models', 'root_dir' => '',
他にも何か見つけたら更新していきます。
]]># VagrantfileVagrant.configure(2) do |config| # こんなの config.vm.box = "chef/centos-7.1"end
8/27か28あたりに配布されなくなったようです。
跡地でBentoというorganizationへ誘導しています。
https://atlas.hashicorp.com/bento/
BentoというのもChef社にメンテナンスされているプロジェクトで、PackerテンプレートでVagrantのbase boxイメージを作ることができます。
彼ら自身がchefをテストするためのboxイメージを作るためにも使っているとのこと。
今後はこんな風にBento boxを使うことにします。
# VagrantfileVagrant.configure(2) do |config| config.vm.box = "bento/centos-7.1"end
(おそらく公式の)BentoのTwitterアイコンがすごく弁当箱です。集中線がジワジワくる。
]]>追記:記事の一部を更新しました。詳細は末尾の更新履歴でご確認ください。
以下、Vagrantを開発環境で利用することを想定しています。
下記のドキュメントを参考にしました。
プライベートネットワークモードで立ち上げた2つのVMに、knife-zeroでレシピを適用する具体的な手順を説明します。
基本方針は下記の通り。
適切に設定ファイルを書けば、下記のような一連のコマンドでレシピの適用まで実行することができます。
% cd knife_zero_example% direnv allow% vagrant up% bundle install --path=vendor/bundle --binstubs% ./bin/berks vendor cookbooks% ./bin/knife zero bootstrap $VAGRANT_HOST001% ./bin/knife zero bootstrap $VAGRANT_HOST002% ./bin/knife node run_list set host001.example build-essential% ./bin/knife node run_list set host002.example build-essential% ./bin/knife zero converge 'name:*.example' -a knife_zero.host
VMのホスト名とIPアドレスは下記とします。
hostname | ip address |
---|---|
host001.example | 192.168.33.10 |
host002.example | 192.168.33.11 |
ネットワークアドレスは、VBoxManage list hostonlyifs
コマンドでVirtualBoxのホストオンリーネットワークのアドレスを確認して、そのレンジのアドレスを設定するようにしてください。普通にVirtualBoxをインストールすると下記のような出力になると思います。
% VBoxManage list hostonlyifsName: vboxnet0GUID: 786f6276-656e-4074-8000-0a0027000000DHCP: DisabledIPAddress: 192.168.33.1NetworkMask: 255.255.255.0IPV6Address:IPV6NetworkMaskPrefixLength: 0HardwareAddress: 0a:00:27:00:00:00MediumType: EthernetStatus: UpVBoxNetworkName: HostInterfaceNetworking-vboxnet0
この場合、192.168.33.0/24
のレンジでアドレスを設定する必要があります。
VMのIPアドレスを環境変数に設定します。これらの環境変数はdirenvで自動的にロード・アンロードするようにします。
% mkdir knife_zero_example % cd knife_zero_example% cat <<EOF > .envrcexport VAGRANT_HOST001=192.168.33.10export VAGRANT_HOST002=192.168.33.11EOF% direnv allow
direnvがない場合は直接読み込んでもよいです。
% source ~/.envrc
% cat <<EOF> VagrantfileVagrant.configure(2) do |config| config.vm.box = "bento/centos-7.1" config.vm.define :host001 do |host| host.vm.hostname = "host001.example" host.vm.network "private_network", ip: ENV['VAGRANT_HOST001'] end config.vm.define :host002 do |host| host.vm.hostname = "host002.example" host.vm.network "private_network", ip: ENV['VAGRANT_HOST002'] end # VMにログインする鍵を固定 config.ssh.insert_key = falseendEOF
~/.ssh/config
に下記のエントリを追加します。
% cat <<EOF>> ~/.ssh/config# Vagrant private network ipHost 192.168.33.* UserKnownHostsFile /dev/null StrictHostKeyChecking no LogLevel FATAL User vagrant IdentityFile ~/.vagrant.d/insecure_private_keyEOF
vagrantで使うプライベートネットワークでのデフォルトのログインユーザをvagrantにし、鍵をデフォルトのものに固定します。
IdentityFileは、もしもVAGRANT_HOME環境変数を設定している場合には、その下のinsecure_private_keyファイルのパスに変更してください。
VMを立ち上げて、ログインできるか確認して下さい。
% vagrant up% ssh $VAGRANT_HOST001 hostnamehost001.example% ssh $VAGRANT_HOST002 hostnamehost002.example
knife-zeroとberkshelfをインストールします。
% cat <<EOF> Gemfilesource "https://rubygems.org"gem "knife-zero"gem "berkshelf"EOF% bundle install --path=vendor/bundle --binstubs
knifeコマンドのオプション引数を省略するために、.chef/knife.rb
を作成します。
% mkdir -p .chef% cat <<EOF> .chef/knife.rb# -z, --local-modelocal_mode true# -x vagrant, --ssh-user vagrantknife[:ssh_user] = "vagrant"# --sudoknife[:use_sudo] = trueEOF
knife.rb
のその他の設定は下記のドキュメントを参照してください。
knife.rb Optional Settings — Chef Docs
今回はknife-zeroの利用例として、コミュニティクックブックのbuild-essentialを2つのVMにインストールします。
このコミュニティクックブックを使うための設定をします。
% cat <<EOF> Berksfilesource "https://supermarket.chef.io"cookbook "build-essential"EOF% ./bin/berks vendor cookbooks
knife-zeroを使って、VMにchefをインストールするとともに、ノードとしてchef-zeroサーバに登録します。
% ./bin/knife zero bootstrap $VAGRANT_HOST001% ./bin/knife zero bootstrap $VAGRANT_HOST002
nodes/
ディレクトリとclients/
ディレクトリ以下にそれぞれjsonファイルができていることを確認して下さい。
また、knife node list
コマンドを実行して、登録したVMのホスト名が表示されることを確認して下さい。
% ./bin/knife node listhost001.examplehost002.example
各ノードのrun_listにbuild-essentialレシピを追加します。
% ./bin/knife node run_list set host001.example build-essentialhost001.example: run_list: recipe[build-essential]% ./bin/knife node run_list set host002.example build-essentialhost002.example: run_list: recipe[build-essential]
次に各ノードへレシピを適用します。
knife-soloではノードにレシピを適用するとき、knife solo cook FQDN
でホストを一つ指定します。
knife-zeroでは、knife solo cook FQDN
に相当するコマンドはknife zero converge QUERY
です。
QUERYで指定した条件にマッチするすべてのホストに対して並列にsshログインし、chef-client
を実行してレシピを適用します。
knife zero converge QUERY
では、対象のノードにsshでログインするためのIPアドレスをノード情報のAttribute名で指定する必要があります(-a
オプション)。このAttributeは通常ipaddress
で良く、その場合は省略できます。ただし、Vagrant + VirtualBoxのprivate_networkでIPアドレスを固定したVMにknife zero bootstrap
した場合、ノードのAttributeとして記録されるipaddress
は、Vagrantfileで指定したアドレス192.168.33.*
ではなく、10.0.2.*
のように異なるネットワークのアドレスになっているはずです。(さらにおそらくアドレス自体が同じ)
% ./bin/knife search node -a ipaddress2 items foundhost001.example: ipaddress: 10.0.2.15host002.example: ipaddress: 10.0.2.15
knife-zeroのバージョン1.8.0以降であれば、bootstrap時にknife_zero.host
というAttributeに接続時のアドレスが記録されます。
% ./bin/knife search node -a knife_zero.host2 items foundhost001.example: knife_zero.host: 192.168.33.10host002.example: knife_zero.host: 192.168.33.11
なので下記のようにknife_zero.host
を指定すればレシピを適用できます。
% ./bin/knife zero converge 'name:*' -a knife_zero.host
'name:*'
はクエリ文字列で、この例ではすべてのノードを対象にしています。
クエリの構造は knife search
と同様です。
# host001だけを指定% ./bin/knife search node 'name:host001.example'# exampleドメインに一致するホストだけを指定% ./bin/knife search node 'name:*.example'
また、クエリ指定でsshログインして任意のコマンドを実行することもできます。
% ./bin/knife ssh 'name:*' -a knife_zero.host 'sudo yum update -y'
knife exec
やknife node edit
で変更した情報は nodes/*.json
に保存されています。
これは本質的にはknife-solo(chef-solo)で書くものと同じなのですが、knife-zero(chef-zero)では非常に分量が多いです。
これについては、.chef/knife.rb
にAttributeのホワイトリストを書くことで制御できます。
chef-zero - Knife-Zeroで管理するnodeオブジェクトを任意のattributesに限定する - Qiita
knife exec
でnormalを指定していますが、chefのAttributeには他にもdefaultやoverrideなどの種類があります。defaultで記録してしまうと、次にconvergeしたタイミングで消えるので注意してください。normalであればconvergeでは消えません。優先順位は複雑なので公式のドキュメントを参照してください。
設定ファイル一式まとめたものを下記に置きました。
https://github.com/kwhrtsk/knife_zero_example
knife_zero.host
にbootstrapで接続した時のIPアドレスが記録されるようになったのはknife-zero 1.8.0からです。
それ以前は knife node edit
でjsonファイルを編集するか、下記のようにknife exec
で接続用のアドレスを追加する必要がありました。
% ./bin/knife exec -E "search(:node, 'hostname:host001'){|n| n.normal['chef_ip'] = ENV['VAGRANT_HOST001']; n.save}"% ./bin/knife exec -E "search(:node, 'hostname:host002'){|n| n.normal['chef_ip'] = ENV['VAGRANT_HOST002']; n.save}"# 設定されているか確認% ./bin/knife search node 'hostname:*' -a chef_ip2 items foundhost001.example: chef_ip: 192.168.33.10host002.example: chef_ip: 192.168.33.11
knife exec
でどのようなことができるかは、下記のドキュメントが参考になります。
chef/centos-7.1 => bento/centos-7.1
しかしどれもしっくりこなかったので、デフォルトのテーマ Landscape をベースにして自分好みのものを作りました。
変更がそれなりの分量になったので、Ingenuousと名前をつけてリリースすることにしました。
欲しい機能を追加したり、スタイルを部分的に自分好みに修正したりしています。基本方針は「自分の技術ブログに必要十分な機能とスタイル」です。
主な追加機能・変更箇所は下記の通り。
toc: true
を指定)あとは過去記事のページネーションなど細かい修正をいくつか。
オリジナルのLandscapeはMIT License、IngenuousもMIT Licenseです。
]]>この記事では自分の場合の移行の手順についてまとめます。
記事や画像などのコンテンツデータの移行は、自分の場合はほぼこれだけでOKでした。
# 新しいhexoのルートディレクトリを作成mkdir hexocd hexohexo init# 投稿記事をコピーcp -pr ../octopress/source/_posts/*.markdown source/_posts/# 拡張子をmdに変更for f in *.markdown; do echo mv $f ${f:r}.md; done | sh -x# 投稿の先頭行(`---`)を削除for f in source/_posts/*.md; do sed -i "" -e '1,1d' $f; done# 画像をコピーcp -pr ../octopress/source/images source/# その他のファイルをコピー(CNAMEは独自ドメインの場合のみ)cp -p ../octopress/public/{CNAME,favicon.ico} source/
ただし、Octopressの include
に相当する機能はHexoには無いので、その記事についてはベタッとinclude先をコピーしました。
_config.yml
)従来の投稿のパーマリンク、および今後のパーマリンクの構造をOctopressと同じにするため、_config.yml
で下記のように設定を変更しました。
permalink: blog/:year/:month/:day/:title/new_post_name: :year-:month-:day-:title.md
デプロイ先はGitHubなので、下記の設定を追加しました。
deploy: type: git repo: git@github.com:kwhrtsk/kwhrtsk.github.io.git
テーマはしっくり来るものがなかったので、Landscapeをベースに自作したものを使っています。見た目はほとんどLandscapeのままですが、特にモバイルでのコードの表示量や、サイドバーにAbout追加したり細かい修正を加えています。
これについては別の記事で。
技術ブログのためのHexoのテーマ Ingenuous をリリースしました
下記のプラグインを追加しました。
hexo-generator-feed
hexo-generator-sitemap
hexo-generator-robotstxt
hexo-deployer-git
npm install --save \ hexo-generator-feed \ hexo-generator-sitemap \ hexo-generator-robotstxt \ hexo-deployer-git
ここまでやれば、次のコマンドでローカルにサーバを立ち上げて、http://localhost:4000/ でプレビューを確認できます。記事や_config.yml
の変更は随時反映されます。
hexo server
デプロイは下記のコマンドです。
hexo deploy
.deploy_git/
ディレクトリ以下にデプロイ用のリポジトリが作られて、_config.yml
で指定したリポジトリにpushされます。(octopressのコミットはすべて消えます)
自分の発表資料をちょっと加筆修正したものを以下に公開しました。
テーマはRailsのエラーモニタリングです。
通知先はSlackという前提で、exception_notificationというgemで直接Slackに通知する方法と、New RelicやAirbrakeといったWebサービス経由で通知する方法、Errbit というOSSのAirbrakクローン経由でSlackに通知する方法を紹介しました。
ErrbitはHerokuの無料枠で運用することが可能で、詳細なセットアップの情報が日本語でも見つかります。
Errbit - Railsアプリの本番エラーをherokuで管理、メール通知する【無料枠】 - 酒と泪とRubyとRailsと
今回はHerokuを使わずにオンプレやAmazon EC2のようなIaaSに自分でセットアップする方法を検討しました。
まずErrbitをセットアップするChefのレシピを探してみたのですが、見つかったものは対応しているErrbitのバージョンが古く、サポートもUbuntuのみでした。
errbit Cookbook - Chef Supermarket
自分はCentOSも好きなので、2015-04-17時点の最新のErrbitに対応していて、かつUbuntuとCentOSのどちらもサポートしているものを作成して公開しました。
errbit-server Cookbook - Chef Supermarket
今回はコミュニティクックブックを多めに使ってやってみました。大まかな構成は下記のとおりです。
rake errbit:bootstrap
)Vagrantで動作を確認するための手順をsampleディレクトリに置いています。下記のバージョンの組み合わせで動作を確認しています。
CentOSで試すのであれば、ターミナルで下記のようなコマンドを入力すると、最終的に http://192.168.33.10:3000/
でローカルVM上のErrbitにログインできます。
git clone https://github.com/kwhrtsk/chef-errbit-servercd chef-errbit-server/sample# VMのIPアドレスを環境変数 VAGRANT_PRIVATE_NETWORK_IP に設定source .envrc# chefなど必要なgemをインストールbundle install --path=vendor/bundlebundle binstubs berkshelf chef# errbit-serverと依存クックブックをダウンロード./bin/berks vendor cookbooks/# VM起動vagrant up
レシピの適用はknife-zeroかknife-soloのいずれかを好みで。
# knife-zeroの場合./bin/knife zero bootstrap --sudo -x vagrant -i .vagrant/machines/default/virtualbox/private_key $VAGRANT_PRIVATE_NETWORK_IP -N $VAGRANT_PRIVATE_NETWORK_IP./bin/knife node from file node.json./bin/knife zero chef_client "name:*" -a chef_ip -x vagrant --identity-file ./.vagrant/machines/default/virtualbox/private_key --sudo# knife-soloの場合./bin/knife solo bootstrap $VAGRANT_PRIVATE_NETWORK_IP --identity-file .vagrant/machines/default/virtualbox/private_key -x vagrant./bin/knife node run_list add $VAGRANT_PRIVATE_NETWORK_IP errbit-server./bin/knife solo cook -i .vagrant/machines/default/virtualbox/private_key vagrant@$VAGRANT_PRIVATE_NETWORK_IP
Ubuntuで試したい場合はVagrantfileの下記の部分を書き換えてください。
config.vm.box = "chef/centos-6.6" #config.vm.box = "chef/ubuntu-14.10"
Errbitの組み込みの初期化処理は errbit:bootstrap
というrakeタスクです。このタスクを実行するとErrbitの初期ユーザが作成されるのですが、パスワードは毎回ランダムに生成され、標準出力に出力されます。このレシピではその出力を /opt/errbit/bootstrap.out
に記録しています。
レシピの適用が終わったら、下記のコマンドを実行して初期ユーザの認証情報を確認して下さい。
vagrant ssh -- cat /opt/errbit/bootstrap.out
こんな内容です。
Seeding database-------------------------------Creating an initial admin user:-- email: errbit@errbit.example.com-- password: Lwc_FBV81Z2zBe sure to note down these credentials now!
SlackのWebhookはSlackの管理画面で作成することができます。
ここで作ったURLをErrbitのアプリ登録画面で入力します。
登録すると下記のように自分のRailsアプリに置く設定ファイル(config/initializers/errbit.rb)が表示されます。
Gemfileに gem 'airbrake'
を追加してRAILS_ENV=productionで例外を発生させれば、Errbitにエラーが登録されると同時に、slackにも通知が届くはずです。
同一のエラーが複数回届いた場合、何度目のエラーで通知するかを指定できます。chef-errbit-serverはデフォルトでは1回目、10回目、100回目に通知を行います。(メールでの通知は行わないようにしています)
このあたりの設定はattributeで変更できるようにしています。詳細はドキュメントを参照してください。
]]>少し前にRails4.2のActiveJobのバックエンドについて調べました。
Resque、SidekiqからSucker PunchまでActiveJobのバックエンドについてひと通り調べてみた
このときSidekiqのコードを読んで、Celluloidによるアクターモデルで並行処理を実装していることを知りました。ちょうどScalaやErlangでアクターを使う方法を調べていたところだったので興味がわき、Celluloidを利用したアプリケーションのサンプルとして読んでみることにしました。
その時見つけた愉快なメソッドを紹介します。
今回読んだSidekiqのバージョンは3.3.3です。
Sidekiq::Queue#💣
SidekiqはRedisをバックエンドとしてジョブを管理・実行するプロダクトです。
Sidekiq::Queue
はそのジョブキューを扱うためのクラスで、💣はキューをクリアするためのメソッドです。
もしも使うのであれば次のような字面になります。
require 'sidekiq/api'queue = Sidekiq::Queue.newqueue.💣
このメソッドはSidekiq::Queue#clearのエイリアスとして、以下で定義されています。
https://github.com/mperham/sidekiq/blob/v3.3.3/lib/sidekiq/api.rb#L246-L254
なかなかハジけたメソッド名ですよね。
なおSidekiq::SortedSetにも同様のメソッドがあります。
Sidekiq::CLI#☠
exitのエイリアスとして定義されています。
https://github.com/mperham/sidekiq/blob/v3.3.3/lib/sidekiq/cli.rb#L205
exitはSidekiq::CLIには定義されていないので、この場合Kernel.exitが実行されます。
exitは由緒正しいメソッド名ですが、実行すればこのプロセスは死ぬわけですから、dieの方がより率直に動作を表現していると言えます。ただ、☠
まで行くと毒薬かもしれないし海賊かもしれないため意図が曖昧になっており、好ましくないと思います。
Sidekiq::Manager#❤
なんとSidekiq::Manager#heatbeatの中で実際に使われています。
https://github.com/mperham/sidekiq/blob/v3.3.3/lib/sidekiq/manager.rb#L137-L148
サービスインしたあとでこんな実装が動いているのを知ってしまったら、胸がドキドキするかもしれませんね。
Sidekiq.❨╯°□°❩╯︵┻━┻
さて、このメソッドは何のためのメソッドなのでしょうか。実行すると下記のメッセージが表示されます。
"Calm down, bro"
意訳すると、まあまあ兄さん落ち着いて、というところでしょうか。
このメソッドにはRSpecで書かれた単体テストがあります。
https://github.com/mperham/sidekiq/blob/v3.3.3/test/test_sidekiq.rb#L28-L36
※iOSで見ると一部文字化けすると思います。
どうやらこのメソッドは、怒れるプログラマが感情的な仕草を表現できるようにしつつ、その気持ちを宥めるために作られたメソッドのようです。
4つの愉快なメソッドを紹介しました。
Sidekiq::Queue#💣
Sidekiq::CLI#☠
Sidekiq::Manager#❤
Sidekiq.❨╯°□°❩╯︵┻━┻
これらのうち、実際に内部で使われているのはSidekiq::Manager#❤
だけです。
SidekiqはCOVERALLSというサービスでコミットごとにテストを実行してカバレッジ計測をしており、今日時点のカバレッジは85.67%です。Sidekiq::Manager#❤
についてもテストで最低一度は実行していることが確認されています。
https://coveralls.io/builds/2264006/source?filename=lib%2Fsidekiq%2Fmanager.rb#L137
Sidekiq::Queue#💣
と Sidekiq::CLI#☠
はただのエイリアスで、内部では一度も使用されていません。Sidekiq.❨╯°□°❩╯︵┻━┻
はジョークメソッドで内部でも使われていませんが、テストはちゃんと書かれていました。
💣
、❤
はUnicode6.0で追加された携帯電話の絵文字で、いわゆる機種依存文字です。海外でもemojiと呼ぶようです。
☠
も機種依存文字ですが、上の2つに比べると古い文字のようですね。
常識的に考えると絵文字でコード書くなんて論外という方がほとんどだと思います(それでもSidekiqは素晴らしいソフトウェアだと思います)。
ただ、このたびあえて先入観を捨ててその可能性を検討してみた結果、下記の気づきがありました。
💣
はヒットするけど ❤
はヒットしない。💣
も ❤
もヒットしない。❤
とか ♥
とか❥
とか。少なくとも検索性がかなり悪くなるということがわかりましたので、個人的には絵文字でコード書くのは避けようと思います。
アクターの話はまた今度気が向いたら書きます。
なお私はどちらかというとResqueの方が好きです。
]]>この記事を読んでいて、Rails PanelというChrome拡張を知りました。
デベロッパーツールにRailsというパネルが追加されます。このパネルでは、ページリクエスト単位で下記のような情報を表示することができます。
要するに rails server
の標準出力(あるいは log/${RAILS_ENV}.log
)の情報を見やすく表示してくれます。
meta_request
というgemを追加します。group :development do gem "meta_request"end
meta_requestをbundleすると、いくつかのRackミドルウェアが自動的に組み込まれます。
この中のMetaRequest::Middlewares::Headersによって X-Meta-Request-Version
が、MetaRequest::Middlewares::RequestIdによって X-Request-Id
がRailsアプリケーションが返すすべてのレスポンスのヘッダに追加されます。
Rails Panelを有効にしていると、 X-Meta-Request-Version
がレスポンスヘッダに存在する際に、Chromeは /__meta_request/${X-Request-Id}.json
というパスにリクエストを投げます。
rails_panel/requests.js at master · dejan/rails_panel
このリクエストもmeta_requestのRackミドルウェアで処理され、Railsアプリ層の手前の段階で、必要な情報がjsonでレスポンスされます。
ただし、このリクエストのログもRackミドルウェア層によってRails.loggerに出力するため、Railsのログには下記のような痕跡が残ります。
Started GET "/__meta_request/abcd92a2-3859-4972-9708-8303fe238e9e.json" for ::1 at 2015-03-06 23:45:37 +0900
なお、 /__meta_request/${X-Request-Id}.json
のレスポンスで返されるjsonファイルは、tmp/data/meta_request/
以下に最大10個まで保存されます。
該当のコードはこのあたり。
RackミドルウェアとChrome拡張でモニタリング用の機能を実装するというアプローチは他のことにも応用ができそうですね。
]]>Rails 4.2で、ActiveJobというクラスが導入されました。
これ以前より、Railsで非同期処理を行う際にはResqueやSidekiq、Delayed Jobなどが広く使われていましたが、ActiveJobはジョブを記述するためのインタフェースを抽象化して、ジョブの実装を変えること無くジョブランナーを切り替えることを可能にするものです。なおActiveJobにおいては、バックエンドを指定しなければジョブは非同期実行しようとしても即座に実行されます。
Rails 4.2.0時点では、ActiveJobのバックエンドとして以下のページにある9つのいずれかを使用できます。
ちなみにRuby ToolboxのBackground Jobsというカテゴリの上位3つは、Resque、Sidekiq、Delayed Jobでした。この3つがよく使われている印象です。
特にResque、Sidekiqは、ジョブの情報を永続化する先としてRedisを必須としています。
情報の保存先ならActiveRecord(RDBMS)でも良さそうなものですが、あえてRedisを必須としている理由は何だろうか考えてみました。
ぱっと思いつくのは、「キューにジョブが積まれた」というイベントをワーカーへ伝えるための手段として、RedisのPub/SubやBLPOP/BRPOPのようなメッセージング機構を使うのが簡単で都合が良かったのではないか?ということです。
バックエンドにMySQLのようなRDBMSを使う場合、普通に考えるとワーカー側はジョブを保存したテーブルをロックしつつポーリングしないといけないのでスケールしなさそうだし、RabbitMQのようなメッセージングサービスを用いるのと比べて、RedisであればRailsエンジニアは元々better memcachedとして使用しているケースが多いので導入のハードルが低いです。
というわけで、このエントリではResque、Sidekiqの2つについてはRedisのメッセージング機構を使っているかどうかを確認しつつ、ActiveJobのページに挙げられている9つのプロダクトについてジョブ情報の保存先を調べてまとめてみようと思います。
9つの中で特にSucker Punchはジョブの保存先もワーカープロセスの起動も必要とせずに非同期処理を行えるので、簡単に作りたいならおすすめです。詳細は後述。
Rails 4.2.0時点においてActiveJobが対応しているのはRedisのバージョン1系列です。
ワーカーを起動する場合はrakeタスクを実行するので、そのあたりからコードを読んでいきます。
lib/tasks/resque.taskこれはresque/tasksを読み込んでいるだけ。
resque/tasks.rbResque::Worker#work
がメインループのようです。
resque/worker.rbジョブが無い場合はインターバル分だけスリープする実装になっていました。ジョブの取得はさらに読んでいくとRedisのLPOP
を使っていて、キューが空でもブロックせずにnilが帰ってきます。
Resque::Worker#work Resque#reserve Resque::Job.reserve Resque.pop Redis#lpop(redis-3.2.1)
Resqueのバージョン1系はRedisのメッセージング機構は使わずにポーリングしてました。
ただ、2015-02-26時点のmasterブランチのHEADでは、インターバルに設定した値をタイムアウト値としてBLPOP
を使う実装になっていました。(インターバルが1より小さい場合はnon-blockで実質ポーリング)
現時点で最新の3.3.2を読んでいきます。
Sidekiqのエントリポイントはbin/sidekiqです。中身はSidekiq::CLI#runなのでそこを見てみます。
Sidekiq::Launcher#runがメインループのようです。
Celluloidを使ったアクターモデルでFetcherとManagerとPollerという3つのインスタンスが強調しつつ並行処理を行っているように見えます。Pollerがスケジュールされたジョブをワークキューに載せ替える、ということをやっていました。Managerの方では同時実行数の数だけProcessorのインスタンスを作って、ProcessorごとにFetcher#fetchしています。Fetcherの中身をざっくり追いかけると、sidekiq/fetch.rb
Fetcher#fetch Fetcher.strategy Fetcher::BasicFetch#retrive_work Redis#brpop
という流れで最終的にRedisのBRPOP
でジョブ情報を読みだしていました。
Sidekiqはポーリングせずに、RedisのBRPOP
を使ってジョブが積まれたイベントをワーカーに通知していました。
beanstalkd というジョブキューサーバをバックエンドに使うようです。
バックエンドとして複数のプロダクトを指定できます。(DJはDelayed Jobのこと)
RedisかMongo。
PostgreSQLのAdvisory Lockを使います。
Advisory Lockを用いてMessage Queueを実現する方法については下記のサイトが詳しいです。
PostgreSQL で簡易に MQ - Mi manca qualche giovedi`?
ちなみにMySQLでMessage Queueやる場合はQ4MというストレージエンジンがありますがActiveJobで使う方法は見つかりませんでした。
PostgreSQL。listen / notify & row lockingで実装。PostgreSQLはPub/Sub機能があるんですね。
RabbitMQ。サンプルコードでRedisを使っていますが、デモのためのカウントアップをRedisでやっているだけです。
ジョブ情報の保存先やワーカープロセスの立ち上げが必要ありません。Celluloidで即座に非同期処理を開始するモデルのようですね。RailsやSinatraのようなアプリケーションプロセス一つでジョブの実行が(非同期に)完結するというのがウリみたいです。
そのため、RedisやRabbitMQを用意する必要はないし、ワーカープロセスを起動する必要すらありません。
Herokuで安価にサービス運用したいのでRedisやRabbitMQは使いたくないけどビューのレスポンスは高速化したいような場合に使うと良さそうです。個人的にはこれがActiveJobのデフォルトのバックエンドでも良いくらいだと思いました。
development環境のWebrickで試してみましたが、コントローラのアクションメソッドでTestJob.perform_later
しても、アクションそのものはすぐにレスポンスが返ってきました。また、ジョブが完了していなくても次のリクエストを処理することができていました。Celluloid使っているらしいのでジョブはThreadで実行されていると思われます。
以下動作を確認したサンプルコードです。
# config/routes.rbRails.application.routes.draw do get 'top/welcome' get 'top/job' root 'top#welcome'end# config/initializers/sucker_punch.rbRails.application.configure do config.active_job.queue_adapter = :sucker_punchend# app/jobs/test_job.rbclass TestJob < ActiveJob::Base queue_as :default def perform(*args) logger.info "job started" sleep 5 logger.info "job finished" endend# app/controllers/top_controller.rbclass TopController < ApplicationController def welcome end def job TestJob.perform_later flash[:notice] = "done!" redirect_to top_welcome_path endend# app/views/welcome.view.erb<% if flash[:notice] %> <%= flash[:notice] %><% end %><%= link_to "Do job", top_job_path %>
追記(2016-03-11): El Capitanの場合の手順を追加(コミュニティ版のみ確認)
% sudo port install mysql56-server
# bundle コマンドを使う場合% bundle config build.mysql2 --with-mysql-config=/opt/local/lib/mysql56/bin/mysql_config% bundle install# gem コマンドを使う場合% gem install mysql2 -- --with-mysql-config=/opt/local/lib/mysql56/bin/mysql_config
http://dev.mysql.com/downloads/mysql/
ここからOSX 10.9用のdmgをダウンロードしてインストール。
システム環境設定パネルでMySQL Serverを起動します。(MySQLのインストール後にOSXの再起動が必要な場合があるようです)
gemをインストールする前にシンボリックリンクを貼る必要があります。
# Yosemiteの場合sudo ln -s /usr/local/mysql/lib/libmysqlclient.18.dylib /usr/lib/libmysqlclient.18.dylib# El Capitanの場合sudo ln -s /usr/local/mysql/lib/libmysqlclient.18.dylib /usr/local/lib/libmysqlclient.18.dylib
これを行わないと、gemのインストールは成功しますが、実行時に下記のようなエラーが発生します。
# test.rbrequire 'mysql2'client = Mysql2::Client.new(:host => "localhost", :username => "root")puts client.query("show databases")
% ruby test.rb/Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require': dlopen(/Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/extensions/x86_64-darwin-14/2.2.0-static/mysql2-0.3.17/mysql2/mysql2.bundle, 9): Library not loaded: libmysqlclient.18.dylib (LoadError) Referenced from: /Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/extensions/x86_64-darwin-14/2.2.0-static/mysql2-0.3.17/mysql2/mysql2.bundle Reason: image not found - /Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/extensions/x86_64-darwin-14/2.2.0-static/mysql2-0.3.17/mysql2/mysql2.bundle from /Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require' from /Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/mysql2-0.3.17/lib/mysql2.rb:8:in `<top (required)>' from /Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:128:in `require' from /Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:128:in `rescue in require' from /Users/kawahara_taisuke/.rbenv/versions/2.2.0/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:39:in `require' from test.rb:1:in `<main>'
MySQLのディストリビューションパッケージの古いバグの名残のようです。
参考:
gemのインストールでは特別なことをする必要はありません。
# bundle コマンドを使う場合% bundle install# gem コマンドを使う場合% gem install mysql2
bundleを使う場合、前述のMacPorts用の設定を行っていると~/.bundle/configに下記の行が追加されているので、 削除しておく必要があります。
# ~/.bundle/configBUNDLE_BUILD__MYSQL2: "--with-mysql-config=/opt/local/lib/mysql56/bin/mysql_config"
素のまま使っていると起動直後から400MBくらい割り当てられます。
下記のページを参考にして、 table_definition_cache
というパラメータを適切に設定すれば抑えることができます。
Macbook Air にいれた開発用 MySQL 5.6.xx の メモリ使用量が異様に多い - 知のレバレッジを最大化せよ
]]>ところで下記のニュースで気になる記述がありました。
ビジネスモデル面でも変更があり、オープンソース版(「Chef Essentials」)と商用版(「Chef Enterprise」)を単一のコードベースに統一し、新たにフリーミアムサブスクリプションモデルを導入した。新たに導入された無料の「Chef Essentials」プランでは、25ノードまで(ホステッド版では5ノードまで)の設定管理が可能となっている。
オープンソース版のライセンスが変わって、25ノードまでの台数制限が入ったようにも読めます。
マジか。
さすがにそれは無いだろうと思ったけど念の為に本家の情報を確認。
Chef | IT automation for speed and awesomeness | Chef
上記のページより、ライセンスと価格についての表示を引用します。
CHEF Essentialsというのがオープンソース版ですね。ここに下記のような記述があります。
Premium Features
・Access to premium features for up to 25 nodes・Hosting available for up to 5 nodes
つまり25ノードに制限されるのは、Essentials版そのものではなく、SubscriptionとEnterprise Subscriptionの欄に書かれているPremium Featuresだけです。
Premium Features
・Management console with reporting and role-based access control・Analytics platform・High availability・Replication・Available as hosted service
以下の機能はEssentials版でも無制限に利用できます。
✔ Chef server ✔ Manage 10,000+ Nodes with a single Chef server ✔ Maintain a searchable blueprint of your infrastructure ✔ Easy Installation with Omnibus installer✔ Chef client ✔ Easily manage Linux, Windows, Mac OS, Solaris, and FreeBSD✔ Chef development kit ✔ Everything you need to start managing applications and infrastructure with Chef ✔ Download and install in just a few clicks✔ Integration with all major cloud providers✔ Easily manage containers in a versionable, testable and repeatable way.
ありえないと思ったんだけどドキドキした。
何か勘違いがあれば @KawaharaTaisuke 宛までご指摘いただけましたら幸いです。
$ gem install knife-soloFetching: chef-zero-2.2.gem (100%)Successfully installed chef-zero-2.2Fetching: chef-12.0.0.alpha.1.gem (100%)Successfully installed chef-12.0.0.alpha.1Thanks for installing knife-solo!If you run into any issues please let us know at: https://github.com/matschaffer/knife-solo/issuesIf you are upgrading knife-solo please uninstall any old versions byrunning `gem clean knife-solo` to avoid any errors.See http://bit.ly/CHEF-3255 for more information on the knife bugthat causes this.Successfully installed knife-solo-0.4.2Parsing documentation for chef-12.0.0.alpha.1Installing ri documentation for chef-12.0.0.alpha.1Parsing documentation for chef-zero-2.2Installing ri documentation for chef-zero-2.2Parsing documentation for knife-solo-0.4.2Done installing documentation for chef, chef-zero, knife-solo after 20 seconds3 gems installed
opscodeのchefのインストーラはまだ未対応なのか、knife solo prepare
が失敗するようになってしまいました。
$ knife solo prepare centos65Bootstrapping Chef... % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed100 16519 100 16519 0 0 13977 0 0:00:01 0:00:01 --:--:-- 30761Downloading Chef 12.0.0.alpha.1 for el...downloading https://www.opscode.com/chef/metadata?v=12.0.0.alpha.1&prerelease=false&nightlies=false&p=el&pv=6&m=x86_64 to file /tmp/install.sh.5840/metadata.txttrying wget...ERROR 404Unable to retrieve a valid package!Please file a bug report at http://tickets.opscode.comProject: ChefComponent: PackagesLabel: OmnibusVersion: 12.0.0.alpha.1Please detail your operating system type, version and any other relevant detailsMetadata URL: https://www.opscode.com/chef/metadata?v=12.0.0.alpha.1&prerelease=false&nightlies=false&p=el&pv=6&m=x86_64
chefがインストールされていない状態でgem install knife-solo
した場合だけ、chefの12.0.0.alphaがインストールされるようです。
Gemfileを書いてbundle install
した場合と、gem install chef
した場合は11.16.0がインストールされることを確認。変なdependencyのがslip outしたんだろうか。