Kamal2の環境変数・シークレット・CI運用方法まとめ
この記事が想定している状況
ローカルとCI環境(GitHub Actions)でデプロイツールのKamalを使用し、複数の環境 (ステージング・プロダクション) に環境変数を設定します。
Kamalのシークレット周りは、ビルド・デプロイ・ランタイムの3段階を考慮して管理する必要があります。また、IaaSに依存しないシークレット管理方法としてsecretsファイルが用意されていますが、値の展開にあたってはローカルとCIの両方を考慮する必要があります。
登場人物
- .kamal
- secrets-common: シークレット宣言ファイル
- secrets.<destination>
- config
- deploy.yml: デプロイ設定
- deploy.<destination>.yml
- .env: ローカル用.envファイル
- .env.kamal.<destination>: デプロイ用.envファイル
- Dockerfile
Versions
- kamal 2.7.0
- dotenv 3.1.8
ローカルデプロイ用.envファイル
環境ごとにローカル用.envファイルを用意
環境のことをKamalでは destination と呼びます。ここでは .env.kamal.<destination>
というファイル名に「デプロイ用の環境変数」を記述します。ファイル名にルールはありませんが、destinationごとに必ず分けるようにしてください。
- production ->
.env.kamal.production
- staging ->
.env.kamal.staging
exampleを用意したほうがよいでしょう。
.env*
!.env.kamal.staging.example
!.env.kamal.production.example
!.env.kamal.testing
内部にはコンテナレジストリの情報やVPSのIPアドレスを記載します。
なお、簡素化のためシークレットを併記していますが、シークレットは本来AWS/GCP等のSecret Managerから取得すべきです。詳細は後述します。
# 以下はデプロイ用
KAMAL_REGISTRY_SERVER=<ステージングのレジストリのドメイン>
KAMAL_REGISTRY_USERNAME=<ステージングのレジストリのユーザー名>
KAMAL_REGISTRY_PASSWORD=<ステージングのレジストリのパスワード>
KAMAL_SERVER_IP_ADDRESS=<ステージングのVPSのIP>
# 以下は「シークレット」として使う予定の値
DB_URL=...
# 以下はデプロイ用
KAMAL_REGISTRY_SERVER=<プロダクションのレジストリのドメイン>
KAMAL_REGISTRY_USERNAME=<プロダクションのレジストリのユーザー名>
KAMAL_REGISTRY_PASSWORD=<プロダクションのレジストリのパスワード>
KAMAL_SERVER_IP_ADDRESS=<プロダクションのVPSのIP>
# 以下は「シークレット」として使う予定の値
DB_URL=...
シークレット宣言ファイル
秘匿が必要な変数はsecrets系ファイルで宣言する必要があります。 ただし、値そのものではなく参照を記載します。
シークレット宣言ファイルの用意(共通)
destination共通のシークレットは .kamal/secrets-common
に記載します。
デプロイに必要なシークレットの記載
まずはDockerレジストリのユーザー名とパスワードを記載してください。
KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
環境変数をシークレットとして使用する場合
今回は単純に環境変数を展開するため、.envファイルとの二重構造になっています。
DB_URL=$DB_URL
OPENAI_API_KEY=$OPENAI_API_KEY
AWS Secrets Managerを使用する場合
シークレットのprefixをfrom
オプションで指定し、シークレット名を列挙します。
SECRETS=$(kamal secrets fetch --adapter aws_secrets_manager --account default --from kamal-laravel-example/ KAMAL_REGISTRY_USERNAME KAMAL_REGISTRY_PASSWORD DB_URL OPENAI_API_KEY <その他シークレット名を列挙する>)
KAMAL_REGISTRY_USERNAME=(kamal secrets extract KAMAL_REGISTRY_USERNAME $SECRETS)
KAMAL_REGISTRY_PASSWORD=(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
DB_URL=$(kamal secrets extract DB_URL $SECRETS)
OPENAI_API_KEY=$(kamal secrets extract OPENAI_API_KEY $SECRETS)
その他のシークレットマネージャーを使用する場合
KamalのドキュメントにGCPや1Password等のコマンドが掲載されています。
シークレット宣言ファイルの用意(環境ごとの存在分岐)
シークレットの存在自体を分岐したい場合は、.kamal/secrets.<destination>
に記載する。
SECRET_EXISTS_ONLY_ON_PRODUCTION=$(...)
環境変数とシークレットを使用する
アプリケーション内でシークレットを使用する
ENVとシークレットはVPSの .kamal
ディレクトリに展開され、コンテナの環境変数として指定されるため、アプリケーションでは特に工夫しなくても読み取れます。
Dockerfileのビルド中にシークレットを使用する
組織内パッケージのインストールなど、ビルド時点でシークレットを使用する場合、Dockerfileのシークレット機能を使用します。
--mount=type=secret,id=<シークレット名>
でファイルをマウントします。その後、/run/secrets/<シークレット名>
を読み取って使用してください。
RUN \
PUSHER_APP_KEY=$(cat /run/secrets/PUSHER_APP_KEY) \
PUSHER_APP_SECRET=$(cat /run/secrets/PUSHER_APP_SECRET) \
PUSHER_APP_ID=$(cat /run/secrets/PUSHER_APP_ID) \
composer install --no-interaction --prefer-dist --optimize-autoloader
デプロイ設定でデプロイ用.envとシークレットを使用する
Kamalのドキュメントでは想定されていませんが、インラインRubyで ENV
を使用しています。
「シークレットを指定できる箇所」が限られているため、「image」「hosts」といった箇所をハードコーディングする想定になっています。
しかし、イメージ名はともかく、VPSのIPアドレスをハードコーディングする状況は好ましくないため、環境変数から展開します。
# Kamal2でLaravelをデプロイするための設定
# https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288
service: kamal-laravel-example
require_destination: true
image: <%= ENV['KAMAL_REGISTRY_USERNAME'] %>/app
servers:
web:
hosts:
- <%= ENV['KAMAL_SERVER_IP_ADDRESS'] %>
proxy:
ssl: true
host: '環境ごとに上書き'
app_port: 8080
registry:
# Docker Hub以外を使う場合はserverが必須
server: <%= ENV['KAMAL_REGISTRY_SERVER'] %>
username:
- KAMAL_REGISTRY_USERNAME
password:
- KAMAL_REGISTRY_PASSWORD
builder:
arch: amd64
cache:
# GitHub Actionsを使う場合
type: gha
env:
clear:
APP_ENV: '環境ごとに上書き'
APP_DEBUG: false
APP_URL: '環境ごとに上書き'
secret:
- DB_URL
- OPENAI_API_KEY
レジストリによる違い
Dockerレジストリと互換性のあるサービス (さくらのコンテナレジストリ等) はこれで問題ありません。
registry:
server: <%= ENV['KAMAL_REGISTRY_SERVER'] %>
username:
- KAMAL_REGISTRY_USERNAME
password:
- KAMAL_REGISTRY_PASSWORD
しかし、 AWS ECRやGCP Artifact Registryを使う場合は書き方が異なります。 ドキュメントを参照してください。
アプリに環境変数とシークレットを使用する
アプリの環境変数名がシークレット名と同じ
アプリのシークレットは env.secret
に記載します。ここに書き忘れるとアプリケーション内で使用できないため注意してください。
secret:
- DB_URL
- OPENAI_API_KEY
環境変数の想定する名前がシークレット名と異なる
例えば DB_URL
を DATABASE_URL
として与えたい場合、コロンの左にアプリ用の名前を指定します。
secret:
- DATABASE_URL:DB_URL
- OPENAI_API_KEY
センシティブでない値
センシティブでない情報は、env.clear
欄に書きます。
env:
clear:
APP_ENV: '環境ごとに上書き'
APP_DEBUG: false
APP_URL: '環境ごとに上書き'
環境ごとに設定を分ける
環境ごとにenvを分ける場合は、YAMLファイルをdestination毎に作成します。
少なくともproxyのhost(ドメイン)を分ける必要があるでしょう。
以下の例では、Laravelの環境変数を環境によって使い分けています。
production
# production環境の上書き部分のみ記述
proxy:
host: <本番ドメイン>
env:
clear:
APP_ENV: production
APP_URL: <本番URL>
LOG_LEVEL: error
staging
# staging環境の上書き部分のみ記述
proxy:
host: <ステージングドメイン>
env:
clear:
APP_ENV: staging
APP_URL: <ステージングURL>
LOG_LEVEL: warning
testing
Kamalのビルドテストコマンド用に記載します。(使い方は後述)
# testing環境の上書き部分のみ記述
proxy:
host: localhost
env:
clear:
APP_ENV: testing
APP_URL: http://localhost
LOG_LEVEL: debug
ローカルでデプロイする
ローカルでKamalを使用する際は、dotenvによって展開した環境変数とSecret Managerによるシークレットを合成します。
ローカルデプロイ時にdotenvで環境変数を展開する
ただ、ローカルでkamalを実行時には.env.kamal.<環境>を展開する必要があります。
そのため、Makefileに下記のようなマクロを定義しました。(ChatGPT作)
define KAMAL_CMD
@echo "→ running kamal on '$(1)' : $(2)"
bundle exec dotenv -f .env.kamal.$(1) kamal $(2) -d $(1)
endef
このマクロをターゲットで使用することで、dotenvを呼ぶ手間を軽減します。
例: 初回デプロイのターゲット
初回デプロイでは、setupサブコマンドでDocker等をセットアップします。
kamal-%-first-deploy:
$(call KAMAL_CMD,$*,setup)
以下のコマンドで bundle exec dotenv -f .env.kamal.staging kamal setup -d staging
が実行されます。
make kamal-staging-first-deploy
例: webコンテナのログを見るターゲット
kamal-%-logs-web:
$(call KAMAL_CMD,$*,app logs -f -r web)
下記のコマンドで bundle exec dotenv -f .env.kamal.staging kamal app logs -f -r web -d staging
が実行されます。ステージング環境のwebコンテナのログをtailすることができます。
make kamal-staging-logs-web
→ running kamal on 'staging' : app logs -r web
ローカルでビルドをテスト
ビルドテストのため、deploy.testing.yml
を用意しています。
YAMLに対応する .env.kamal.testing
も用意しましょう。
KAMAL_REGISTRY_SERVER=test
KAMAL_REGISTRY_USERNAME=test
KAMAL_REGISTRY_PASSWORD=test
KAMAL_SERVER_IP_ADDRESS=test
DB_URL=test
以下のターゲットを実行すると、 bundle exec dotenv -f .env.kamal.testing kamal build dev
が実行され、ビルドを検証できます。
kamal-testing-build:
$(call KAMAL_CMD,testing,build dev)
GitHub Actionsで環境変数を展開する
GitHub Actionsでは、リポジトリシークレット経由で展開した環境変数を、Secret Managerの値と合成して使用します。
以下はステージング環境の例を記載します。
リポジトリシークレットの設定
名前 | 値 |
---|---|
<シークレット名>_STAGING |
シークレット |
SSH_PRIVATE_KEY_STAGING |
VPSの秘密鍵 |
また、Secrets Manager等を使用する場合、そのための認証情報もリポジトリシークレットに追加する必要があります。
ワークフローの定義
env:
# ビルド用:
DOCKER_BUILDKIT: 1
# デプロイ用:
KAMAL_REGISTRY_SERVER: ${{ secrets.KAMAL_REGISTRY_SERVER_STAGING }}
KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME_STAGING }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD_STAGING }}
KAMAL_SERVER_IP_ADDRESS: ${{ secrets.KAMAL_SERVER_IP_ADDRESS_STAGING }}
# シークレット:
DB_URL: ${{ secrets.DB_URL_STAGING }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_STAGING }}
以下はステージングのワークフロー例になります。プロダクションも同様に設定します。
# 参考: https://gist.github.com/danieldraper/ea6f99f3fd4baa0e024db77851131b19#file-github_workflows_deploy-yml
name: 'stagingの更新: ステージング環境のデプロイ'
concurrency:
group: staging
cancel-in-progress: true
on:
push:
branches:
- staging
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: staging
url: 'https://<ステージングのドメイン>'
env:
# ビルド用:
DOCKER_BUILDKIT: 1
# デプロイ用:
KAMAL_REGISTRY_SERVER: ${{ secrets.KAMAL_REGISTRY_SERVER_STAGING }}
KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME_STAGING }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD_STAGING }}
KAMAL_SERVER_IP_ADDRESS: ${{ secrets.KAMAL_SERVER_IP_ADDRESS_STAGING }}
# シークレット:
DB_URL: ${{ secrets.DB_URL_STAGING }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_STAGING }}
steps:
- uses: actions/checkout@v4
- name: 'kamal CLIのためrubyを用意'
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.1
bundler-cache: true
- run: gem install kamal
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_STAGING }}
- name: 'コンテナレジストリにログイン'
uses: docker/login-action@v3
with:
registry: ${{ env.KAMAL_REGISTRY_SERVER }}
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
- name: 'キャッシュのためDocker Buildxのセットアップ'
uses: docker/setup-buildx-action@v3
- name: 'キャッシュのためACTIONS_RUNTIME_TOKEN/URLを露出する'
uses: crazy-max/ghaction-github-runtime@v3
- run: kamal version
- run: kamal registry login -d staging --verbose
# https://kamal-deploy.org/docs/commands/lock/
- run: kamal lock release -d staging --verbose
- run: kamal redeploy -d staging --verbose
最後に
上記の運用は、一人のエンジニアが所有するVPSを操作して運用する想定になっており、DHHのOne Person Framework思想を如実に反映しています。ただ、現実的にはローカルからデプロイを発火できない環境や、非rootユーザーによるデプロイも想定して設計する必要が生じるでしょう。適宜カスタマイズして運用してください。
また、Zenn上には様々なKamalチュートリアルがあります。私はRailsを使わないため、皆さんのチュートリアルを参考にはしていませんが、Railsを使う場合はぜひ他の記事もご確認お願いします。
参考
Kamalドキュメント:
Kamalのワークフロー:
-
Kamal 2.0 + GitHub Actions
- KamalをGitHub Actionsで使う場合はこのフローに従うのが正解だと思います。
Laravelの設定例:
-
Deploying Laravel Apps with Kamal 2.0
- HeyのTony Messias氏の非常に詳細なLaravelデプロイのチュートリアルです。redisをはじめたアクセサリを併用する際やアセットバンドルの注意点も開設されています。
Discussion