Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Kamal README: 37signalsの多機能コンテナデプロイツール(翻訳)

更新情報

コンテナデプロイツールであるKamalは、元はMRSKという名前でしたが、2023/08/23にリリースされたv0.16.0でKamalに変更され、リポジトリもリネームされました。

basecamp/kamal - GitHub

その後、コミットcfe7793でREADMEの内容がKamal公式サイト(kamal-deploy.org)に移されました。

そこで、本記事では利便性のため、可能な限りドキュメント移行直前の最新の内容に沿って更新翻訳しました。今後の最新情報についてはKamal公式サイト(kamal-deploy.org)を参照してください。

概要

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ドキュメントの改善をお手伝いください。

basecamp/kamal-site - GitHub

🔗 ライセンス

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

これにより、以下がひと通り実行されます。

  1. SSH経由でサーバーに接続する(デフォルトではrootを利用し、sshキーで認証される)
  2. Dockerとcurlがなさそうなサーバーではこれらをインストールする(apt-getを利用): これを行うにはssh経由でのrootアクセスが必要です
  3. レジストリにローカルとリモートの両方でログインする
  4. アプリケーションのルートディレクトリにある標準のDockerfileでイメージをビルドする
  5. イメージをレジストリにpushする
  6. レジストリにあるイメージをサーバーにpullする
  7. Traefikが実行中で、トラフィックをポート80で受信することを確認する
  8. アプリがGET /up200 OKを返すことを確認する(アプリのイメージにcurlをインストールしておくこと!)
  9. 現在のgitバージョンハッシュと一致するアプリのバージョンで、新しいコンテナを起動する
  10. 以前のバージョンで実行されていた古いコンテナを停止する
  11. 未使用のイメージを削除してコンテナを停止し、サーバーがあふれないようにする

できました!これで、すべてのサーバーがポート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_SECRETidを抽出して、以下の.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/limitboot/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

関連記事

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。