🐕

[Kamal2] deploy.ymlでサーバーIPをコミットしたくない!

に公開
1

デプロイツールKamal2で、例えばVPSアドレスをコミットせず、しかも環境で分けたいとする。

環境変数にKAMAL_SERVER_IP_ADDRESSと定義して、 $KAMAL_SERVER_IP_ADDRESS みたいに指定したい。でも、IPはシークレット項目ではないため、その書き方では変数が展開されない。

config/deploy.yml
servers:
  web:
    hosts:
      - ここに環境変数を埋め込みたい

では、どうやってシークレットではない欄に環境変数を渡すのか?GitHubでは様々な混乱が見られる。

https://github.com/basecamp/kamal/discussions/977

措置として、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>
  1. 秘匿する値は .env.kamal.<destination> に記述しておく
  2. 秘匿する値は .kamal/secrets-common.kamal/secrets.<destination> で展開
  • 例: VAR=$VAR
  1. コミットしてよい値は deploy.ymldeploy.<destination>.yml に直書きする
  2. deploy.ymlにrubyのコードを書き、ymlパース時に dotenv を発火する
  • 例: require 'dotenv'; Dotenv.load(".kamal/secrets-common")
  1. デプロイCLIでdotenvを使う
  • 例:bundle exec dotenv -f .env.kamal.<destination> kamal サブコマンド
  1. CI環境ではdotenvを使わず環境変数を持たせればよい
  • 環境分けができないGitHub Actions無料プランでは、$VAR=$VAR_PRODUCTION みたいに指定
  1. 上記により、任意の値を秘匿して、環境ごとに分けることができる

version

  • kamal 2.5.3
  • dotenv 3.1.8

設定ファイルの準備

.envの用意

例えばproduction, stagingで環境を分けるとする。前提として、Kamalではこれらを「destination」と呼ぶ。

.env.kamal.<destination> に、ローカル開発用の.envとは別に、**「デプロイ用の環境変数」**を記述する。example以外はignore。

.gitignore
.env
.env.*
!.env.kamal.staging.example
!.env.kamal.production.example
!.env.kamal.testing

例えば、デプロイ先のIPを書く。ついでにコンテナレジストリの情報も秘匿。

.env.kamal.staging.example
KAMAL_REGISTRY_SERVER=<ステージングのレジストリのドメイン>
KAMAL_REGISTRY_USERNAME=<ステージングのレジストリのユーザー名>
KAMAL_REGISTRY_PASSWORD=<ステージングのレジストリのパスワード>
KAMAL_SERVER_IP_ADDRESS=<ステージングのVPSのIP>
# 追記例:
DB_URL=...
.env.kamal.production.example
KAMAL_REGISTRY_SERVER=<プロダクションのレジストリのドメイン>
KAMAL_REGISTRY_USERNAME=<プロダクションのレジストリのユーザー名>
KAMAL_REGISTRY_PASSWORD=<プロダクションのレジストリのパスワード>
KAMAL_SERVER_IP_ADDRESS=<プロダクションのVPSのIP>
# 追記例:
DB_URL=...

秘匿すべき値、例えばDBの接続情報などもここに追記する。

secretファイルの用意

secrets-commonに先ほどの環境変数を展開するよう、値を列挙する。

.kamal/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> に記載する。

.kamal/secrets.production
SECRET_EXISTS_ONLT_ON_PRODUCTION=$SECRET_EXISTS_ONLT_ON_PRODUCTION

deploy.yml本体で環境変数を展開する

deploy.yml本体の工夫について。
(個人的にRailsを普段使わないので、Laravelの例になっている)

config/deploy.yml
# 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が古い場合は更新すること。

https://github.com/basecamp/kamal/pull/1019

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

config/deploy.production.yml
# production環境の上書き部分のみ記述
proxy:
  host: <本番ドメイン>

env:
  clear:
    APP_ENV: production
    APP_URL: <本番URL>
    LOG_LEVEL: error

staging

config/deploy.staging.yml
# staging環境の上書き部分のみ記述
proxy:
  host: <ステージングドメイン>

env:
  clear:
    APP_ENV: staging
    APP_URL: <ステージングURL>
    LOG_LEVEL: warning

testing

デプロイしないから基本不要。なぜ用意したか理由は後述する

config/deploy.testing.yml
# testing環境の上書き部分のみ記述
proxy:
  host: localhost

env:
  clear:
    APP_ENV: testing
    APP_URL: http://localhost
    LOG_LEVEL: debug

ローカルデプロイ時に環境変数を展開する

Makefileの工夫

ただ、ローカルでkamalを実行時には.env.kamal.<環境>を展開する必要がある。

そのため、Makefileに下記のようなマクロを定義する。(ChatGPT作)

Makefile
define KAMAL_CMD
	@echo "→ running kamal on '$(1)' : $(2)"
	bundle exec dotenv -f .env.kamal.$(1) kamal $(2) -d $(1)
endef

例: 初回デプロイ

setup = Dockerのインストール等をVPSで実行

Makefile
kamal-%-first-deploy:
	$(call KAMAL_CMD,$*,setup)

下記のように使う。

make kamal-staging-first-deploy

例: webコンテナのログを見る

app logs -r web = webコンテナのログを表示

Makefile
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を用意する。

.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を用意したので、ローカルでビルドを試すことができる。

Makefile
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 }}

https://gist.github.com/danieldraper/ea6f99f3fd4baa0e024db77851131b19#file-config_deploy-yml-L6

以下はステージングの例。プロダクションも同様に設定する。

※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ドキュメント:

https://kamal-deploy.org/docs/configuration/overview/

マクロ:

https://chatgpt.com/share/68050117-7a98-8010-b107-645c32f349b9

KamalのCIについて:

https://gist.github.com/danieldraper/ea6f99f3fd4baa0e024db77851131b19#file-config_deploy-yml-L6

Laravelの設定例について:

https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288

Discussion

uruositeuruosite

この記事で書いていることは、

  1. kamalに環境変数を渡したら、secretsで展開してください。
  2. secret以外はうまいこと展開してください、という話で済む。

リサーチと実装に相当な時間を割いてしまったので、記録もかねて残している。

背景としては、Webフレームワークにおけるdotenvの透明化がある。

多くのユーザーがdotenvの挙動を当たり前に思ってしまい、勝手に.envを読んでくれるだろうと期待して、つまづいている。「secretsで展開するので、好きな方法で渡せますよ!」という、kamalの思いやりが、裏目に出てしまった形。