[Kamal2] deploy.ymlでサーバーIPをコミットしたくない!
デプロイツールKamal2で、例えばVPSアドレスをコミットせず、しかも環境で分けたいとする。
環境変数にKAMAL_SERVER_IP_ADDRESSと定義して、 $KAMAL_SERVER_IP_ADDRESS
みたいに指定したい。でも、IPはシークレット項目ではないため、その書き方では変数が展開されない。
servers:
web:
hosts:
- ここに環境変数を埋め込みたい
では、どうやってシークレットではない欄に環境変数を渡すのか?GitHubでは様々な混乱が見られる。
措置として、dotenvをインラインで呼び、ENVで展開する方法が議論されている。この時、どうやって値を環境(destination)ごとに分けるか を解説する。
TL;DR
種類 | 直書きする場所 | 秘匿しつつ書く場所 | 展開する場所 |
---|---|---|---|
共通の値 | .deploy.yml |
.env.kamal.<destination> |
.kamal/secrets-common |
環境ごとに内容が違う値 | .deploy.<destination>.yml |
.env.kamal.<destination> |
.kamal/secrets-common |
環境ごとに存在しない値 | .deploy.<destination>.yml |
.env.kamal.<destination> |
.kamal/secrets.<destination> |
- 秘匿する値は
.env.kamal.<destination>
に記述しておく - 秘匿する値は
.kamal/secrets-common
と.kamal/secrets.<destination>
で展開
- 例:
VAR=$VAR
- コミットしてよい値は
deploy.yml
とdeploy.<destination>.yml
に直書きする - deploy.ymlにrubyのコードを書き、ymlパース時に
dotenv
を発火する
- 例:
require 'dotenv'; Dotenv.load(".kamal/secrets-common")
- デプロイCLIでdotenvを使う
- 例:
bundle exec dotenv -f .env.kamal.<destination> kamal サブコマンド
- CI環境ではdotenvを使わず環境変数を持たせればよい
- 環境分けができないGitHub Actions無料プランでは、
$VAR=$VAR_PRODUCTION
みたいに指定
- 上記により、任意の値を秘匿して、環境ごとに分けることができる
version
- kamal 2.5.3
- dotenv 3.1.8
設定ファイルの準備
.envの用意
例えばproduction, stagingで環境を分けるとする。前提として、Kamalではこれらを「destination」と呼ぶ。
.env.kamal.<destination>
に、ローカル開発用の.envとは別に、**「デプロイ用の環境変数」**を記述する。example以外はignore。
.env
.env.*
!.env.kamal.staging.example
!.env.kamal.production.example
!.env.kamal.testing
例えば、デプロイ先のIPを書く。ついでにコンテナレジストリの情報も秘匿。
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=...
秘匿すべき値、例えばDBの接続情報などもここに追記する。
secretファイルの用意
secrets-commonに先ほどの環境変数を展開するよう、値を列挙する。
KAMAL_REGISTRY_SERVER=$KAMAL_REGISTRY_SERVER
KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
KAMAL_SERVER_IP_ADDRESS=$KAMAL_SERVER_IP_ADDRESS
# 追記例:
DB_URL=$DB_URL
シークレットの存在自体を、destinationで分岐したい場合 は、secrets.<destination>
に記載する。
SECRET_EXISTS_ONLT_ON_PRODUCTION=$SECRET_EXISTS_ONLT_ON_PRODUCTION
deploy.yml本体で環境変数を展開する
deploy.yml本体の工夫について。
(個人的にRailsを普段使わないので、Laravelの例になっている)
# Kamal2でLaravelをデプロイするための設定
# https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288
service: kamal-laravel-example
require_destination: true
# HACK: ENVの展開を可能にする
<% require 'dotenv'; Dotenv.load(".kamal/secrets-common") %>
<% require 'dotenv'; Dotenv.load(".kamal/secrets.#{ENV['KAMAL_DESTINATION']}") %>
# 以下、ENVは全て.kamal/secretsで定義する必要がある
# GitHub Actions: env機能を使う
# ローカル実行時: dotenvを使う
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: <%= ENV['KAMAL_REGISTRY_USERNAME'] %>
password:
- KAMAL_REGISTRY_PASSWORD
builder:
arch: amd64
cache:
# GitHub Actionsを使う場合
type: gha
env:
clear:
APP_ENV: '環境ごとに上書き'
APP_DEBUG: false
APP_URL: '環境ごとに上書き'
# 以下に指定した環境変数は.kamal/secretsから読み取られる
secret:
# 追記例:
- DB_URL
- SECRET_VAR
# 明示はしていないが、デプロイ後は以下の環境変数を使える:
# KAMAL_CONTAINER_NAME=kamal-laravel-example-web-staging-<SHA>
# KAMAL_VERSION=<SHA>
# CONTAINER_ROLE=web
環境変数を展開するハック
Kamalは以下のファイルを"secret"として解釈するが、これらは直接ENV
で展開できない。
- .kamal/secrets-common
- .kamal/secrets.<環境>
そこで、rubyのインライン実行により無理やり環境変数として展開している。
# HACK: ENVの展開を可能にする
<% require 'dotenv'; Dotenv.load(".kamal/secrets-common") %>
<% require 'dotenv'; Dotenv.load(".kamal/secrets.#{ENV['KAMAL_DESTINATION']}") %>
なお、ここで #{ENV['KAMAL_DESTINATION']}
を使えるのは下記のPRマージ後からのため、Kamalが古い場合は更新すること。
secretについて
envにsecret
欄をつければ、環境変数として読み取ってくれる。DB_URLなど、センシティブな値はここで指定する。
secret:
# 追記例:
- DB_URL
- SECRET_VAR
ただし、ドメイン等はここに書かなくていい。
deploy.<destination>.ymlを書く
センシティブでない情報は、clear欄に書く。
いちいち環境変数に書くと面倒なので、ymlを環境で分けて指定する。
env:
clear:
APP_ENV: '環境ごとに上書き'
APP_DEBUG: false
APP_URL: '環境ごとに上書き'
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
デプロイしないから基本不要。なぜ用意したか理由は後述する
# testing環境の上書き部分のみ記述
proxy:
host: localhost
env:
clear:
APP_ENV: testing
APP_URL: http://localhost
LOG_LEVEL: debug
ローカルデプロイ時に環境変数を展開する
Makefileの工夫
ただ、ローカルで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
例: 初回デプロイ
setup
= Dockerのインストール等をVPSで実行
kamal-%-first-deploy:
$(call KAMAL_CMD,$*,setup)
下記のように使う。
make kamal-staging-first-deploy
例: webコンテナのログを見る
app logs -r web
= webコンテナのログを表示
kamal-%-logs-web:
$(call KAMAL_CMD,$*,app logs -r web)
下記のように使う。環境とロールで絞り込まれているのが分かる。
make kamal-staging-logs-web
→ running kamal on 'staging' : app logs -r web
bundle exec dotenv -f .env.kamal.staging kamal app logs -r web -d staging
INFO [15ccb946] Running /usr/bin/env sh -c 'docker ps --latest --quiet --filter label=service=kamal-laravel-example --filter label=destination=staging --filter label=role=web (以下略)
INFO [15ccb946] Finished in 1.748 seconds with exit status 0 (successful).
ボーナス: ローカルでビルド
kamalのDockerビルドが成功するか不安な場合、「testing」環境としてビルドを走らせるといい。
.env.kamal.testingを用意する。
KAMAL_REGISTRY_SERVER=test
KAMAL_REGISTRY_USERNAME=test
KAMAL_REGISTRY_PASSWORD=test
KAMAL_SERVER_IP_ADDRESS=test
DB_URL=test
さきほどdeploy.testing.ymlを用意したので、ローカルでビルドを試すことができる。
kamal-testing-build:
$(call KAMAL_CMD,testing,build dev)
build devコマンドを使うと、ローカルにイメージが作られる。※これをdocker composeで使うわけではないため、local環境とは呼んでいない。
GitHub Actionsで環境変数を展開する
GitHub ActionsでKamalを使う場合は、dotenvは不要。envにアクションシークレット(または変数)を展開する。
上記の環境変数なら、このような展開方法になる。
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 }}
以下はステージングの例。プロダクションも同様に設定する。
※Reusable Workflowはenvの受け渡しが不便なので、使っていない。
# 参考: https://gist.github.com/danieldraper/ea6f99f3fd4baa0e024db77851131b19#file-config_deploy-yml-L6
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 }}
# ... ここに環境変数を追加していく
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
参考
Kamalドキュメント:
マクロ:
KamalのCIについて:
Laravelの設定例について:
Discussion
この記事で書いていることは、
リサーチと実装に相当な時間を割いてしまったので、記録もかねて残している。
背景としては、Webフレームワークにおけるdotenvの透明化がある。
多くのユーザーがdotenvの挙動を当たり前に思ってしまい、勝手に.envを読んでくれるだろうと期待して、つまづいている。「secretsで展開するので、好きな方法で渡せますよ!」という、kamalの思いやりが、裏目に出てしまった形。