概要
MITライセンスに基づいて翻訳・公開いたします。
- 英語記事: kamal/README.md at main · basecamp/kamal
- 原文更新日: 2023/08/23(cfe7793)
- ライセンス: MIT
本記事更新時点のバージョン: Release v0.16.1 · basecamp/kamal
Kamal README: 37signalsの多機能コンテナデプロイツール(翻訳)
Kamalは、Dockerを利用して、WebアプリケーションをベアメタルからクラウドVMまでダウンタイムゼロでどこからでもデプロイできます。新しいアプリケーションコンテナが起動してから古いコンテナが停止されるまでの間、動的リバースプロキシであるTraefikを利用してリクエストを保持します。複数のホストに対してシームレスに動作し、SSHKitを利用してコマンドを実行します。Kamalは、当初はRailsアプリケーション向けに構築されましたが、Dockerでコンテナ化可能なあらゆる種類のWebアプリケーションで動作します。
➡️ kamal-deploy.orgでは以下のドキュメントを参照できます。
🔗 ドキュメントへの貢献方法
以下のbasecamp/kamal-siteリポジトリで、Kamalドキュメントの改善をお手伝いください。
🔗 ライセンス
Kamal is released under the MIT License.
訳注
ここから下の内容は、コミットcfe7793に沿っています。
🔗 インストール方法
Rubyが使える環境であれば、以下を実行してKamalをグローバルにインストールできます。
gem install kamal
または、Docker化バージョンのKamalをエイリアス経由で実行することも可能です(以下を自分の.bashrc
ファイルに追加しておくと再利用がシンプルになります)。
alias kamal='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/basecamp/kamal'
次に、アプリケーションディレクトリでkamal init
を実行します(Rails 7以降のアプリでbin/kamal binstub
を使いたい場合は、kamal init --bundle
を実行します)。
終わったら新しいconfig/deploy.yml
ファイルを編集します。このファイルは以下のようにシンプルです。
service: hey
image: 37s/hey
servers:
- 192.168.0.1
- 192.168.0.2
registry:
username: registry-user-name
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
次に.env
ファイルを編集して、自分が使うレジストリのパスワードをKAMAL_REGISTRY_PASSWORD
環境変数に追加します(Railsアプリをproduction環境で動かすためにRAILS_MASTER_KEY
も追加します)。
以上で、サーバーにデプロイする準備が整います。
kamal setup
これにより、以下がひと通り実行されます。
- SSH経由でサーバーに接続する(デフォルトではrootを利用し、sshキーで認証される)
- Dockerとcurlがなさそうなサーバーではこれらをインストールする(apt-getを利用): これを行うにはssh経由でのrootアクセスが必要です
- レジストリにローカルとリモートの両方でログインする
- アプリケーションのルートディレクトリにある標準のDockerfileでイメージをビルドする
- イメージをレジストリにpushする
- レジストリにあるイメージをサーバーにpullする
- Traefikが実行中で、トラフィックをポート80で受信することを確認する
- アプリが
GET /up
に200 OK
を返すことを確認する(アプリのイメージにcurlをインストールしておくこと!) - 現在のgitバージョンハッシュと一致するアプリのバージョンで、新しいコンテナを起動する
- 以前のバージョンで実行されていた古いコンテナを停止する
- 未使用のイメージを削除してコンテナを停止し、サーバーがあふれないようにする
できました!これで、すべてのサーバーがポート80でアプリを配信するようになります。実行するサーバーが1個だけの場合は、これでOKです。実行するサーバーが複数の場合は、ロードバランサーをサーバーの手前に配置する必要があります。
これで、以後のデプロイは(またはサーバーにDockerとcurlがインストール済みであれば)、kamal deploy
を実行するだけで完了します。
🔗 Rails 7未満の場合の利用法
Kamalを利用するときに、アプリケーションのGemfileに記述する必要はありません。ただし、CI/CDワークフローで特定のKamalバージョンを保証したい場合は、以下のような内容で別のGemfile(例:gemfile/kamal.gemfile
)を作成できます。
source 'https://rubygems.org'
gem 'kamal', '~> 0.14'
BUNDLE_GEMFILE=gemfiles/kamal.gemfile bundle
でbundlerを実行します。
これで、このKamalをデプロイで利用できるようになります。
BUNDLE_GEMFILE=gemfiles/kamal.gemfile bundle exec kamal deploy
🔗 ビジョン
Webアプリを手軽にデプロイする商用サービスは、この何十年の間に爆発的に増加しています。その先駆けとなったHerokuの素晴らしさは、永遠に競合他社の先を行くかと思われるほどでした。近年はFly.ioやRenderなどの優れた競合サービスも登場していますし、ホスト型KubernetesによってAWSやGCPやDigital Oceanなどあらゆる場所での作業が楽になりつつあります。しかしこれらはすべて、クラウド上のコンピュータを有料でレンタルする形になります。自社内にあるハードウェア上で実行したい場合は、たとえ将来の移行パスが明確であったとしても、そうした商用プラットフォームにどの程度ロックインされるかを慎重に見極める必要があります。できれば、ビジネスが請求書の山に飲み込まれる前に!
Kamalは、これら商用サービスが切り開いてきた先進的なエルゴノミクスを取り入れて、Webサービスをあらゆる場所にデプロイ可能にすることを目指しています。デプロイ先がマネージドサービスなし・価格上乗せなしの低価格サービス(Digital Ocean、Hetzner、OVHなど)であろうと、自社内に設置されているベアメタルマシンであろうと、Kamalにとってはまったく同じです。SSHキーを追加した以外に何も準備していない素のUbuntuサーバーのIPアドレスリストを設定ファイルに記入すれば、文字通り数分で実行できるようになるのです。
この手法によって移植性が大幅に高まります。Webアプリを複数のクラウドにデプロイするのも同じように手軽にできます。普段は独自ハードウェアで運用し、アクセスが集中する時期が近づいたらクラウドにデプロイして可用性を高めることも可能です。ツール周りが単一プロバイダにロックインされていなければ、多くの魅力的なオプションが利用できます。
Kamalが最終的に目指すのは、商用サービスに縛られていないオープンソースのツールを用いて、本番運用までの複雑さを軽減することです(ゼロではありません、念のため)。LinuxやDockerの基本的な操作がまだ難しいのであれば、これまでどおりフルマネージドサービスに乗る方がおそらくよいでしょう。しかしそうした概念に慣れてくれば、Kamalを使う準備はすぐにでも整います。
🔗 Capistrano/Kubernetes/Docker Swarmでいいのでは?
Kamalは基本的に「コンテナ用Capistrano」です。事前にサーバーを注意深くセットアップする必要もありませんし、サーバーで適切なバージョンのRubyやその他の依存関係を事前に準備しておく必要もありません。必要なものはすべてDockerイメージの中で動きます。真新しいUbuntuサーバー(でも何でも)を起動してKamalのサーバーリストに追加すれば、Dockerで自動プロビジョニングされて即座に実行されます。Dockerレイヤキャッシュ機能によってデプロイも高速化され、サーバーに煩わされることも減ります。さらに、Kamalでビルドしたイメージは、後でCIや内部調査にも転用できます。
Kubernetesは猛獣です。独自ハードウェア上での運用は気の小さい人には向いていません。他の誰かのプラットフォーム上で動かすのであれば(Renderがやっているような透過的運用か、AWSやGCP上で明示的に運用するか)、Kubernetesは良いオプションですが、クラウドと独自ハードウェアの間を自由に移動したり両者を混在させたりするなら、Kamalの方がずっとシンプルです(Kamalでは基本的なDockerコマンドが呼び出されるだけです)。
Docker SwarmはKubernetesよりずっとシンプルですが、それでもステートのreconciliation(調整)を利用する宣言的モデル上に構築される点は同じです。Kamalは、あえてCapistranoと同様の命令的コマンド中心の設計を採用しています。
最終的にWebアプリのデプロイ方法はいくらでも存在しますが、Kamalは、私たち37signalsが現代のコンテナ化ツールのメリットを失わずにHEYをクラウドから自社に取り戻すのに使っているツールキットなのです。
🔗 KamalをDockerから実行する
Kamalは、rails/dockedと同様にDockerコンテナでパッケージ化されています。これにより、Docker以外の依存関係をインストールせずに、(アプリケーションのディレクトリから)Kamalを実行可能になります。
コンテナ作業をさらに便利にするために、以下のエイリアスをbashのプロファイル設定に追加してください。
alias kamal="docker run -it --rm -v '${PWD}:/workdir' -v '${SSH_AUTH_SOCK}:/ssh-agent' -v /var/run/docker.sock:/var/run/docker.sock -e 'SSH_AUTH_SOCK=/ssh-agent' ghcr.io/basecamp/kamal:latest"
Kamalはリモート接続をsshで確立するので、sshエージェントにアクセス可能になっている必要があります。上のコマンドは、ボリュームマウントを用いてコンテナ内でボリュームを利用可能にし、コンテナ内部のsshエージェントがそれを利用できるように設定します。
🔗 設定
🔗 必要な環境変数を.envファイルで読み込む
Kamalはdotenvを用いて、アプリケーションのルートディレクトリに置かれている.envファイルの環境変数を自動的に読み込みます。この.envファイルは、KAMAL_REGISTRY_PASSWORD
やデータベースパスワードなどの変数を設定するのに利用できます。ただし、この理由によって、.envファイルは決してGitにチェックインしたりDockerfileに取り込んだりしてはいけません。形式は以下のようなキーバリューになります。
KAMAL_REGISTRY_PASSWORD=pw
DB_PASSWORD=secret123
🔗 生成された.envファイルを利用する
🔗 1Passwordを秘密情報ストアとして利用する
秘密情報ストアを1Passwordなどに集約している場合は、秘密情報を取得する.env.erb
テンプレートを作成できます。以下は.env.erb
ファイルの例です。
<% if (session_token = `op signin --account my-one-password-account --raw`.strip) != "" %># Generated by kamal envify
GITHUB_TOKEN=<%= `gh config get -h github.com oauth_token`.strip %>
KAMAL_REGISTRY_PASSWORD=<%= `op read "op://Vault/Docker Hub/password" -n --session #{session_token}` %>
RAILS_MASTER_KEY=<%= `op read "op://Vault/My App/RAILS_MASTER_SECRET" -n --session #{session_token}` %>
MYSQL_ROOT_PASSWORD=<%= `op read "op://Vault/My App/MYSQL_ROOT_PASSWORD" -n --session #{session_token}` %>
<% else raise ArgumentError, "Session token missing" end %>
このテンプレートファイルはGitにチェックインしても安全です。アプリをデプロイできるユーザーなら誰でもkamal envify
を実行して、アプリを最初にセットアップしたり、パスワードを変更して正しい.env
ファイルを取得したりできます。
デプロイ先ごとに環境変数を使い分ける必要がある場合は、.env.destination.erb
テンプレートで設定できます。kamal envify -d staging
を実行すると.env.staging
ファイルが生成されます。
原注
1Passwordで生体認証を利用している場合は、このサンプルコードからsession_token
に関連する部分を削除すれば、op read op://Vault/Docker Hub/password -n
を呼び出すだけで済みます。
🔗 Bitwardenを秘密情報ストアとして利用する
Bitwardenなどのオープンソースの秘密情報ストアを使う場合は、以下のような.env.erb
テンプレートで秘密情報を探索できます。
SOME_SECRET
はbitwardenの保管庫(vault)の秘密メモに保存できます。
$ bw list items --search SOME_SECRET | jq
? Master password: [hidden]
[
{
"object": "item",
"id": "123123123-1232-4224-222f-234234234234",
"organizationId": null,
"folderId": null,
"type": 2,
"reprompt": 0,
"name": "SOME_SECRET",
"notes": "yyy",
"favorite": false,
"secureNote": {
"type": 0
},
"collectionIds": [],
"revisionDate": "2023-02-28T23:54:47.868Z",
"creationDate": "2022-11-07T03:16:05.828Z",
"deletedDate": null
}
]
上のjson
から SOME_SECRET
のid
を抽出して、以下の.erb
で利用します。
.env.erb
サンプルファイル:
<% if (session_token=`bw unlock --raw`.strip) != "" %># Generated by kamal envify
SOME_SECRET=<%= `bw get notes 123123123-1232-4224-222f-234234234234 --session #{session_token}` %>
<% else raise ArgumentError, "session_token token missing" end %>
これで、アプリをデプロイできるユーザーなら誰でもkamal envify
を実行して.env
を生成できます。
🔗 Docker Hub以外のレジストリを使用する
デフォルトのレジストリはDocker Hubですが、registry/server
で変更可能です。
registry:
server: registry.digitalocean.com
username:
- DOCKER_REGISTRY_TOKEN
password:
- DOCKER_REGISTRY_TOKEN
秘密情報DOCKER_REGISTRY_TOKEN
への参照は、Kamalを実行中のマシン上にあるENV["DOCKER_REGISTRY_TOKEN"]
を探索します。
🔗 AWS ECRをコンテナレジストリとして使う
AWS ECRのアクセストークンの有効期限はわずか12時間です。毎回トークンを手動で再生成しなくてもよいようにするために、deploy.yml
ファイル内で以下のようにERBを用いてaws
CLIコマンドにシェルアウトすることで、トークンを取得できます。
registry:
server: <AWSアカウントID>.dkr.ecr.<your aws region id>.amazonaws.com
username: AWS
password: <%= %x(aws ecr get-login-password) %>
これを動かすには、ローカル環境にaws
CLI をインストールしておく必要があります。
🔗 root以外のSSHユーザーを使う
デフォルトのSSHユーザーはrootですが、ssh/user
で変更可能です。
ssh:
user: app
自分が非rootユーザー(上の例ではapp
)の場合、Kamalを使う前に手動でブートストラップする必要があります。Ubuntuの場合は、たとえば以下のように実行します。
sudo apt update
sudo apt upgrade -y
sudo apt install -y docker.io curl git
sudo usermod -a -G docker app
🔗 SSHプロキシホストを使う
プロキシホスト経由で接続する必要がある場合は、ssh/proxy
で指定できます。
ssh:
proxy: "192.168.0.1" # デフォルトユーザーはroot
ユーザーも指定できます。
ssh:
proxy: "app@192.168.0.1"
また、サーバーへの接続に特定のプロキシコマンドが必要な場合は、以下のように書けます。
ssh:
proxy_command: aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --region=us-east-1 ## ssh via aws ssm
🔗 SSHのログレベルを設定する
ssh:
log_level: debug
有効なログレベルは以下です。
debug
info
warn
error
fatal
(デフォルト)
🔗 環境変数を使う
以下を使うと、アプリのコンテナにenv
の環境変数を注入できます。
env:
DATABASE_URL: mysql2://db1/hey_production/
REDIS_URL: redis://redis1:6379/1
🔗 機密の環境変数を使う
機密にしておくべき環境変数がある場合は、env
ファイルでclear
ブロックとsecret
ブロックを使い分けることが可能です。
env:
clear:
DATABASE_URL: mysql2://db1/hey_production/
REDIS_URL: redis://redis1:6379/1
secret:
- DATABASE_PASSWORD
- REDIS_PASSWORD
secret
ブロックの環境変数リストは、実行時にローカル環境から展開されます。つまり、DATABASE_PASSWORD
の秘密情報への参照は、Kamalを実行している環境でENV["DATABASE_PASSWORD"]
を探索します。これは、ビルド用の秘密情報の場合と同じ要領です。
参照されたsecret
の環境変数が見つからない場合は、KeyError
例外で設定を中止します。
原注
secret
の環境変数は、Kamalの出力では値が伏せ字(redacts)になります。clear
の環境変数は、実行時に平文でコンテナに注入されます。
🔗 ボリュームを使う
volumes
でカスタムボリュームをアプリのコンテナに追加できます。
volumes:
- "/local/path:/container/path"
🔗 Kamalの環境変数
コンテナを実行すると、以下の環境変数が設定されます。
KAMAL_CONTAINER_NAME
: ここには現在のコンテナ名とバージョンが含まれます
🔗 さまざまな役割のサーバーを使い分ける
デフォルトのWeb以外に、ジョブ実行など別のロール用に別ホストを使う場合は、新しいエントリポイントコマンドでこれらのホストを以下のように指定できます。
servers:
web:
- 192.168.0.1
- 192.168.0.2
job:
hosts:
- 192.168.0.3
- 192.168.0.4
cmd: bin/jobs
注: Traefikは、デフォルトではweb
ロールにのみ(ロールが指定されていない場合すべてのサーバーに)インストールおよび実行されます。Traefikをweb
以外のロールでホストする必要がある場合は、traefik: true
を追加します。
servers:
web:
- 192.168.0.1
- 192.168.0.2
web2:
traefik: true
hosts:
- 192.168.0.3
- 192.168.0.4
🔗 コンテナにラベルを付ける
起動されるコンテナにラベルを設定することで、デフォルトのTraefikルールをカスタマイズできます。
labels:
traefik.http.routers.hey-web.rule: Host(`app.hey.com`)
Traefikのルールは「service-role-destination」形式で定義されます。
- ルールが指定されていない場合、デフォルトのロールは
web
になります。 - デプロイ先(destination)が指定されていない場合は、デプロイ先は含まれません。
たとえば、デプロイ先が「staging」の場合、上記のルールは「traefik.http.routers.hey-web-staging.rule」となります。
原注
バッククォート記号は、ルールを正しく渡し、bashでコマンド置換として扱われないようにするために必要です。
これにより、同じTraefikインスタンスとポートを共有する同一サーバーで複数のアプリケーションを実行できるようになります。利用可能なルーティングルールの完全なリストは https://doc.traefik.io/traefik/routing/routers/#ruleにあります。
ラベルはロールごとにも適用可能です。
servers:
web:
- 192.168.0.1
- 192.168.0.2
job:
hosts:
- 192.168.0.3
- 192.168.0.4
cmd: bin/jobs
labels:
my-label: "50"
🔗 シェル展開構文を使う
シェル展開構文${}
を利用することで、ホストマシンからの値をラベルや環境変数に展開できます。
{}
内の任意のコードはホストマシン上で実行され、その結果がラベルや環境変数に展開されます。
labels:
host-machine: "${cat /etc/hostname}"
env:
HOST_DEPLOYMENT_DIR: "${PWD}"
原注
シェル展開が不用意に実行されるのを防ぐため、上記以外の$
はすべてエスケープされます。
🔗 コンテナオプションを利用する
コンテナの起動に使うオプションは、options
定義で特殊化できます。
servers:
web:
- 192.168.0.1
- 192.168.0.2
job:
hosts:
- 192.168.0.3
- 192.168.0.4
cmd: bin/jobs
options:
cap-add: true
cpu-count: 4
これで、jon
コンテナがdocker run ... --cap-add --cpu-count 4 ...
で起動します。
🔗 最小バージョンを指定する
Kamalの最小バージョンは以下のように設定できます。
minimum_version: 0.13.3
原注
この設定では0.13.2
以下の値は無視されます。
🔗 ログを設定する
Dockerに渡すログ出力ドライバやオプションは、logging
で設定できます。
logging:
driver: awslogs
options:
awslogs-region: "eu-central-2"
awslogs-group: "my-app"
何も設定しない場合は、すべてのコンテナでデフォルトオプションmax-size=10m
が使われます。Dockerのデフォルトのログ出力ドライバはjson-file
です。
🔗 stop_wait_time
を変更する
新規デプロイ時には、実行中の古い各コンテナはSIGTERM
で"graceful"にシャットダウンされ、10
秒の猶予期間を経てからSIGKILL
を送信します。
この値はstop_wait_time
オプションで設定できます。
stop_wait_time: 30
🔗 ネイティブのマルチアーキテクチャ向けのリモートビルダーを使う
開発をARM64(Apple Siliconなど)で行っているがAMD64(x86 64ビット)にデプロイしたい場合は、マルチアーキテクチャのイメージを利用できます。デフォルトのKamalは、QEMUエミュレーション経由でビルドを行うローカルbuildx設定をセットアップします。ただし、特に最初のビルドはかなり遅くなる可能性があります。
ビルダーのオプションを使えば、ローカルではイメージのAMD64部分をネイティブリリースし、リモートではAMD64ホストを利用してイメージのAMD64部分をネイティブビルドする形で高速化できるようになります。
builder:
local:
arch: arm64
host: unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock
remote:
arch: amd64
host: ssh://root@192.168.0.1
注意: これを行うには、ビルダーとして使われるリモートホストでDockerが動作していなければなりません。このインスタンスの共有範囲は、同一のレジストリとcredentialを用いるビルドに限定すべきです。
🔗 単一アーキテクチャ向けのリモートビルダーを利用する
開発をARM64(Apple Siliconなど)で行っていてAMD64(x86 64ビット)にデプロイしたいが、ローカルで(または他のARM64ホストで)イメージを実行する必要がない場合は、AMD64のみを対象とするリモートビルダーを構成できます。ローカルでのビルドが不要なので、マルチアーキテクチャのビルドよりも少し速くなります。
builder:
remote:
arch: amd64
host: ssh://root@192.168.0.1
🔗 マルチアーキテクチャが不要な場合にネイティブビルダーを使う
デプロイしているアーキテクチャと同一のアーキテクチャで開発している場合は、マルチアーキテクチャとリモートビルドの両方をやめることでビルドを高速化できます。
builder:
multiarch: false
これは、デプロイ先サーバーとアーキテクチャを共有しているCIサーバーでKamalを実行している場合にも適しています。
🔗 ビルド時に別のDockerfileやコンテキストを使う
ビルドコマンドに別のDockerfileやコンテキストを渡す必要がある場合は(例: monorepoを使っている場合や別のDockerfileがある場合)、以下のようにbuilder
オプションで変更可能です。
# 別のDockerfileを使う
builder:
dockerfile: Dockerfile.xyz
# コンテキストを設定する
builder:
context: ".."
# Dockerfileとコンテキストを両方指定する
builder:
dockerfile: "../Dockerfile.xyz"
context: ".."
🔗 マルチステージビルドキャッシュを使う
Dockerのマルチステージビルドキャッシュを利用すると、ビルドを大幅に高速化できます。現在、KamalでサポートされているのはGHAキャッシュまたはレジストリキャッシュのみです。
# GHAキャッシュを使う
builder:
cache:
type: gha
# レジストリキャッシュを使う
builder:
cache:
type: registry
# レジストリキャッシュで別のキャッシュイメージを使う
builder:
cache:
type: registry
# デフォルトのイメージ名は「<image>-build-cache」
image: application-cache-image
# レジストリキャッシュで追加のcache-toオプションを使う
builder:
cache:
type: registry
options: mode=max,image-manifest=true,oci-mediatypes=true
ビルドキャッシュの最適化について詳しくは、以下のDocker公式Webサイトにあるドキュメントを参照してください。
🔗 新しいイメージでビルド用秘密情報を使う
イメージによっては、ビルド時に秘密情報を渡す必要が生じることがあります(private gemリポジトリにアクセスするGITHUB_TOKEN
など)。これは、環境変数に秘密情報を設定してビルダーのコンフィグで参照することで可能になります。
builder:
secrets:
- GITHUB_TOKEN
このビルド用秘密情報はDockerfileで参照できます。
# Gemfilesをコピーする
COPY Gemfile Gemfile.lock ./
# 依存関係をインストールする
# (アクセストークン経由でアクセスするprivateリポジトリなど)
# (終了後、GITHUB_TOKENを含むバンドルキャッシュは削除する)
RUN --mount=type=secret,id=GITHUB_TOKEN \
BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
bundle install && \
rm -rf /usr/local/bundle/cache
🔗 Traefikコマンドの引数
以下のようにtraefikコマンドをargs
でカスタマイズできます。
traefik:
args:
accesslog: true
accesslog.format: json
これで、traefikコンテナが--accesslog=true accesslog.format=json
引数で起動します。
🔗 Traefikのホスト-ポートバインディング
Traefikは、デフォルトでポート80
にバインドします。別のポートは以下のようにhost_port
で設定可能です。
traefik:
host_port: 8080
🔗 Traefikのバージョン、アップグレード、カスタムイメージ
Kamalは、Traefik 2.9.xをトラッキングするためにtraefik:v2.9
イメージを実行します。
Traefikを特定のバージョンに固定したい場合や、自分たちのレジストリで公開しているイメージに固定したい場合は、以下のようにimage
を指定します。
traefik:
image: traefik:v2.10.0-rc1
この機能は、Traefikのマイナーバージョンリリースで予期しない重大な変更があった場合にTraefikをダウングレードするときに有用です。また、今後のリリースをテストするためにTraefikをアップグレードしたり、独自のTraefik派生イメージを実行したりするのにも利用できます。
KamlはまだTraefik 3ベータでの互換性についてテストされていません。どうかテストをお願いします!
🔗 Traefikコンテナを設定する
Traefikには以下の方法でDockerオプションを追加で渡せます。
options
を使うと、Traefikコンテナ用のDocker追加設定を渡せます。
traefik:
options:
publish:
- 8080:8080
volume:
- /tmp/example.json:/tmp/example.json
memory: 512m
上の設定は、docker run
を実行するときに--volume /tmp/example.json:/tmp/example.json --publish 8080:8080 --memory 512m
引数を渡してtraefikコンテナを起動します。
🔗 Traefikコンテナのラベル
TraefikのDockerコンテナにラベルを追加できます。
traefik:
labels:
traefik.enable: true
traefik.http.routers.dashboard.rule: Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
traefik.http.routers.dashboard.service: api@internal
traefik.http.routers.dashboard.middlewares: auth
traefik.http.middlewares.auth.basicauth.users: test:$2y$05$H2o72tMaO.TwY1wNQUV1K.fhjRgLHRDWohFvUZOJHBEtUXNKrqUKi # test:password
上の設定は、Traefikコンテナに--label traefik.http.routers.dashboard.middlewares=\"auth\"
などのラベルを追加します。
🔗 Traefikに別のエントリポイントを設定する
Traefikには、以下のように複数のエントリポイントを設定できます。
service: myservice
labels:
traefik.tcp.routers.other.rule: 'HostSNI(`*`)'
traefik.tcp.routers.other.entrypoints: otherentrypoint
traefik.tcp.services.other.loadbalancer.server.port: 9000
traefik.http.routers.myservice.entrypoints: web
traefik.http.services.myservice.loadbalancer.server.port: 8080
traefik:
options:
publish:
- 9000:9000
args:
entrypoints.web.address: ':80'
entrypoints.otherentrypoint.address: ':9000'
🔗 Traefikを再起動する
Traefikの引数やラベルを変更した場合は、以下を実行して再起動する必要があります。
kamal traefik reboot
production環境では、以下のようにローリング再起動を指定することで、Traefikコンテナが1つずつ再起動されます。ローリング再起動は、時間がかかる代わりに安全性が高まります。
kamal traefik reboot --rolling
🔗 新規イメージのビルド引数を設定する
秘密情報以外のビルド引数も設定可能です。
builder:
args:
RUBY_VERSION: 3.2.0
このビルド引数はDockerfileで参照可能です。
ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION-slim as base
🔗 DB/キャッシュ/検索サービスを使う
アクセサリサービス(追加サービス)もKamal経由で管理できます。アクセサリとは、アプリが依存する長時間実行サービスのことです。アクセサリサービスはデプロイ時には自動的に更新されません。
accessories:
mysql:
image: mysql:5.7
host: 1.1.1.3
port: 3306
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
volumes:
- /var/lib/mysql:/var/lib/mysql
options:
cpus: 4
memory: "2GB"
redis:
image: redis:latest
roles:
- web
port: "36379:6379"
volumes:
- /var/lib/redis:/data
internal-example:
image: registry.digitalocean.com/user/otherservice:latest
host: 1.1.1.5
port: 44444
アクセサリーサービスが実行されるホストは、ホストまたはロールで指定できます。
# 単一ホスト
mysql:
host: 1.1.1.1
# 複数ホスト
redis:
hosts:
- 1.1.1.1
- 1.1.1.2
# ロール指定
monitoring:
roles:
- web
- jobs
これで、kamal accessory start mysql
を実行して1.1.1.3のホストでMySQLサーバーを起動します。
kamal accessory
を実行すると、利用可能な全コマンドを表示できます。
アクセサリのイメージは、publicなイメージか、自分たちのprivateレジストリでタグ付けされていなければなりません。
🔗 cronを使う
cronジョブ実行用のコンテナを利用できます。
servers:
cron:
hosts:
- 192.168.0.1
cmd:
bash -c "cat config/crontab | crontab - && cron -f"
上はcron設定がconfig/crontab
に保存されていることが前提です。
🔗 ヘルスチェック
Kamalは、Dockerのヘルスチェックを利用してデプロイ中にアプリケーションの健全性をチェックします。Traefikは、この同じヘルスチェックのステータスを利用して、コンテナがトラフィックを受信可能かどうかを判断します。
ヘルスチェックは、デフォルトでポート3000の/up
パスへのHTTPレスポンスを最大7回テストします。この振る舞いは、healthcheck
設定でカスタマイズできます。
healthcheck:
path: /healthz
port: 4000
max_attempts: 7
interval: 20s
これにより、アプリケーションの /healthz
に対するヘルスチェックのTraefikラベルが設定され、Kamalが実行するデプロイ前のヘルスチェックも、ポート4000の同じパスで実行されるようになります。
以下のようにカスタムのヘルスチェックコマンドも指定できます。これは非HTTPサービスで便利です。
healthcheck:
cmd: /bin/check_health
トップレベルのヘルスチェック設定は、デフォルトでTraefikを利用するすべてのサービスに適用されます。また、以下のように設定をロールレベルで特殊化することも可能です。
servers:
job:
hosts: ...
cmd: bin/jobs
healthcheck:
cmd: bin/check
ヘルスチェックでは、オプションのmax_attempts
設定を利用できます。これにより、デプロイが失敗する前に、指定の回数までヘルスチェックを試行できるようになります。このオプションは、起動に時間がかかるアプリケーションで便利です(デフォルト値は7)。
原注
HTTPヘルスチェックでは、コンテナ内でcurl
コマンドが利用可能であることが前提です。curl
コマンドが利用できない場合は、ヘルスチェックのcmd
オプションを利用して、コンテナがサポートする別のヘルスチェックを指定してください。
🔗 コマンド
🔗 サーバーでコマンドを実行する
以下の単発コマンドを実行できます。
# すべてのサーバーでコマンドを実行する
kamal app exec 'ruby -v'
App Host: 192.168.0.1
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
App Host: 192.168.0.2
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
# primaryサーバーでコマンドを実行する
kamal app exec --primary 'cat .ruby-version'
App Host: 192.168.0.1
3.1.3
# すべてのサーバーでコマンドを実行する
kamal app exec 'bin/rails about'
App Host: 192.168.0.1
About your application's environment
Rails version 7.1.0.alpha
Ruby version ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
RubyGems version 3.3.26
Rack version 2.2.5
Middleware ActionDispatch::HostAuthorization, Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::Callbacks, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ContentSecurityPolicy::Middleware, ActionDispatch::PermissionsPolicy::Middleware, Rack::Head, Rack::ConditionalGet, Rack::ETag, Rack::TempfileReaper
Application root /rails
Environment production
Database adapter sqlite3
Database schema version 20221231233303
App Host: 192.168.0.2
About your application's environment
Rails version 7.1.0.alpha
Ruby version ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
RubyGems version 3.3.26
Rack version 2.2.5
Middleware ActionDispatch::HostAuthorization, Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::Callbacks, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ContentSecurityPolicy::Middleware, ActionDispatch::PermissionsPolicy::Middleware, Rack::Head, Rack::ConditionalGet, Rack::ETag, Rack::TempfileReaper
Application root /rails
Environment production
Database adapter sqlite3
Database schema version 20221231233303
# primaryサーバーでRailsランナーを実行する
kamal app exec -p 'bin/rails runner "puts Rails.application.config.time_zone"'
UTC
🔗 SSH経由でインタラクティブにコマンドを実行する
サーバー上で、Railsコンソールやbashセッションなどの対話型コマンドを実行できます (デフォルトはprimary: 別のサーバーに接続するには--hosts
を使います)。
# アプリの直近のイメージから作った新規コンテナで
# 新しいbashセッションを開始する
kamal app exec -i bash
# アプリが現在実行中のコンテナでbashセッションを開始する
kamal app exec -i --reuse bash
# アプリの直近のイメージから作った新規コンテナで
# Railsコンソールを開始する
kamal app exec -i 'bin/rails console'
🔗 コンテナの詳細な実行状態を表示する
kamal details
を実行するとサーバーの状態を表示できます。
Traefik Host: 192.168.0.1
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6195b2a28c81 traefik "/entrypoint.sh --pr…" 30 minutes ago Up 19 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp traefik
Traefik Host: 192.168.0.2
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
de14a335d152 traefik "/entrypoint.sh --pr…" 30 minutes ago Up 19 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp traefik
App Host: 192.168.0.1
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
badb1aa51db3 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 13 minutes ago Up 13 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
App Host: 192.168.0.2
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1d3c91ed1f55 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 13 minutes ago Up 13 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
kamal app details
でアプリコンテナの情報だけを表示したり、kamal traefik details
でTraefikの情報だけを表示したりすることも可能です。
🔗 不適切なデプロイをロールバックで修正する
不適切なデプロイに気づいたら、一時停止している古いコンテナイメージを再度アクティベートすることで即座にロールバックできます。
kamal app containers
を実行すると、ロールバックできる古いコンテナを確認できます。
表示内容はkamal app details
に似ていますが、古いコンテナもすべて含まれます。出力は以下のようになります。
App Host: 192.168.0.1
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1d3c91ed1f51 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 19 minutes ago Up 19 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
539f26b28369 registry.digitalocean.com/user/app:e5d9d7c2b898289dfbc5f7f1334140d984eedae4 "/rails/bin/docker-e…" 31 minutes ago Exited (1) 27 minutes ago chat-e5d9d7c2b898289dfbc5f7f1334140d984eedae4
App Host: 192.168.0.2
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
badb1aa51db4 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 19 minutes ago Up 19 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
6f170d1172ae registry.digitalocean.com/user/app:e5d9d7c2b898289dfbc5f7f1334140d984eedae4 "/rails/bin/docker-e…" 31 minutes ago Exited (1) 27 minutes ago chat-e5d9d7c2b898289dfbc5f7f1334140d984eedae4
上の例ではe5d9d7c2b898289dfbc5f7f1334140d984eedae4
が直近のバージョンであることがわかるので、ここにロールバック可能です。
kamal rollback e5d9d7c2b898289dfbc5f7f1334140d984eedae4
を実行してロールバックすると、6ef8a6a84c525b123c5245345a8483f86d05a123
が停止してe5d9d7c2b898289dfbc5f7f1334140d984eedae4
が起動します。古いコンテナも利用可能な状態になっているので、非常に迅速に行われます。レジストリからのダウンロードは発生しません。
kamal deploy
を実行すると、デフォルトでは3日後に古いコンテナが削除されるのでご注意ください。
🔗 削除を実行してサーバーをクリーンアップする
Traefik、コンテナ、イメージ、レジストリセッションを含むアプリケーション全体を削除する場合はkamal remove
を実行します。これにより、サーバーをクリーンにできます。
🔗 ロック
コンカレントな実行が安全でないコマンドは、実行中にデプロイロックを取得します。このロックは、primaryサーバー上のkamal_lock-<service>
ディレクトリにあります。
ロックの状態は以下で確認できます。
kamal lock status
Locked by: AN Other at 2023-03-24 09:49:03 UTC
Version: 77f45c0686811c68989d6576748475a60bf53fc2
Message: Automatic deploy lock
以下のようにロックを手動で取得・解除することも可能です。
kamal lock acquire -m "Doing maintanence"
kamal lock release
🔗 ローリングデプロイ
多数のホストにデプロイする場合、すべてのホストでサービスを同時に再起動することは避けたいことがあります。
Kamalのデフォルト設定では、新しいコンテナをすべてのホストで同時に起動します。ただし、boot/limit
やboot/wait
のオプションを以下のように設定することで制御可能です。
service: myservice
boot:
limit: 10 # ホストの総数のパーセント指定も可能("25%"など)
wait: 2
limit
を指定すると、コンテナは同時に最大limit
個のホストで起動します。Kamalは、バッチとバッチの間にwait
秒の一時停止を行います。
これらの設定が適用されるのは、(kamal deploy
または kamal app boot
で)コンテナを起動する場合のみです。それ以外のコマンドは、引き続きすべてのホストについてパラレルにコマンドを実行し続けます。
🔗 フック
フックを使うと、カスタムスクリプトを特定のポイントで実行できます。
フックは .kamal/hooks
フォルダに保存してください。kamal init
を実行すると、このフォルダが作成され、いくつかのサンプルスクリプトも追加されます。
設定ファイルでhooks_path
を設定することで、フォルダの場所を変更することも可能です。
スクリプトがゼロ以外の終了コードを返す場合、コマンドは中止されます。
フックコマンドでは、KAMAL_*
環境変数を利用して詳細な監査レポートを出力できます。
たとえば、デプロイレポートのトリガーとしてやJSON Webフックを発火させるのに使えます。環境変数には以下のようなものが含まれます。
KAMAL_RECORDED_AT
- ISO 8601形式のUTCタイムスタンプ(例:
2023-04-14T17:07:31Z
) KAMAL_PERFORMER
- コマンドを実行しているローカルユーザー(
whoami
で取得) KAMAL_SERVICE_VERSION
- メッセージで利用する省略形のサービス名とバージョン(例:
app@150b24f
) KAMAL_VERSION
- デプロイされる完全なバージョン
KAMAL_HOSTS
- コマンドの実行対象となるホストのリスト(カンマ区切り)
KAMAL_COMMAND
- 実行中のコマンド
KAMAL_SUBCOMMAND
- (オプション)実行中のサブコマンド
KAMAL_DESTINATION
- (オプション)デプロイ先(例: "staging")
KAMAL_ROLE
- (オプション)対象となるロール(例: "web")
以下の4種類のフックがあります。
- 1. pre-connect(接続前)
- デプロイロックを取得する前に呼ばれます。リモートホストに接続する前に必要なチェック(例: DNSウォーミング)に使います。
- 2. pre-build(ビルド前)
- ビルド前のチェック(例: コミットしていない変更が残っていないか、CIがパスしたかどうか)に使います。
- 3. pre-deploy(デプロイ前)
- デプロイ前の最終チェック(例: CIが完了したかのチェック)に使います。
- 4. post-deploy(デプロイ後)
- デプロイ/再デプロイ/ロールバックの後で実行されます。
このフックには、デプロイに要した合計時間(秒)を設定したKAMAL_RUNTIME
環境変数も渡されます。
これはデプロイメッセージをブロードキャストしたり、APMに新しいバージョンを登録したりするのに利用できます。
コマンドは以下のような感じになります。
#!/usr/bin/env bash
curl -q -d content="[My App] ${KAMAL_PERFORMER} Rolled back to version ${KAMAL_VERSION}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
上のコマンドを実行すると、Basecampの事前設定済みチャットボットに以下のようなメッセージが投稿されます。
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
フックの実行を回避するには、 --skip_hooks
を設定します。
🔗 SSH接続の管理
多数のサーバーにデプロイするときに、SSH接続を同時にいくつも作成することが問題になる場合があります。Kamalは、同時に作成可能な接続数をデフォルトで30個までに制限します。
また、アイドリング状態が長時間続いた後(例: 画像のビルドやCIの完了待ち)の再接続ストームを防ぐために、接続ごとに900秒のアイドリングタイムアウトも設定します。
これらの設定は以下で行えます。
sshkit:
max_concurrent_starts: 10
pool_idle_timeout: 300
更新情報
コンテナデプロイツールであるKamalは、元はMRSKという名前でしたが、2023/08/23にリリースされたv0.16.0でKamalに変更され、リポジトリもリネームされました。
その後、コミットcfe7793でREADMEの内容がKamal公式サイト(kamal-deploy.org)に移されました。
そこで、本記事では利便性のため、可能な限りドキュメント移行直前の最新の内容に沿って更新翻訳しました。今後の最新情報についてはKamal公式サイト(kamal-deploy.org)を参照してください。
追記(2024/10/21): その後Kamal 2がリリースされました。