🐕

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を用意したほうがよいでしょう。

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

内部にはコンテナレジストリの情報やVPSのIPアドレスを記載します。

なお、簡素化のためシークレットを併記していますが、シークレットは本来AWS/GCP等のSecret Managerから取得すべきです。詳細は後述します。

.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=...

シークレット宣言ファイル

秘匿が必要な変数はsecrets系ファイルで宣言する必要があります。 ただし、値そのものではなく参照を記載します。

https://kamal-deploy.org/docs/configuration/environment-variables/#secrets

シークレット宣言ファイルの用意(共通)

destination共通のシークレットは .kamal/secrets-common に記載します。

デプロイに必要なシークレットの記載

まずはDockerレジストリのユーザー名とパスワードを記載してください。

.kamal/secrets-common
KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

環境変数をシークレットとして使用する場合

今回は単純に環境変数を展開するため、.envファイルとの二重構造になっています。

.kamal/secrets-common
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等のコマンドが掲載されています。

https://kamal-deploy.org/docs/commands/secrets/

シークレット宣言ファイルの用意(環境ごとの存在分岐)

シークレットの存在自体を分岐したい場合は、.kamal/secrets.<destination> に記載する。

.kamal/secrets.production
SECRET_EXISTS_ONLY_ON_PRODUCTION=$(...)

環境変数とシークレットを使用する

アプリケーション内でシークレットを使用する

ENVとシークレットはVPSの .kamal ディレクトリに展開され、コンテナの環境変数として指定されるため、アプリケーションでは特に工夫しなくても読み取れます。

Dockerfileのビルド中にシークレットを使用する

組織内パッケージのインストールなど、ビルド時点でシークレットを使用する場合、Dockerfileのシークレット機能を使用します。

https://kamal-deploy.org/docs/configuration/builders/#referencing-build-secrets

--mount=type=secret,id=<シークレット名> でファイルをマウントします。その後、/run/secrets/<シークレット名> を読み取って使用してください。

RUN --mount=type=secret,id=PUSHER_APP_KEY,uid=82 \
    --mount=type=secret,id=PUSHER_APP_SECRET,uid=82 \
    --mount=type=secret,id=PUSHER_APP_ID,uid=82 \
    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 を使用しています。

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

「シークレットを指定できる箇所」が限られているため、「image」「hosts」といった箇所をハードコーディングする想定になっています。

しかし、イメージ名はともかく、VPSのIPアドレスをハードコーディングする状況は好ましくないため、環境変数から展開します。

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

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を使う場合は書き方が異なります。 ドキュメントを参照してください。

https://kamal-deploy.org/docs/configuration/docker-registry/

アプリに環境変数とシークレットを使用する

アプリの環境変数名がシークレット名と同じ

アプリのシークレットは env.secret に記載します。ここに書き忘れるとアプリケーション内で使用できないため注意してください。

  secret:
    - DB_URL
    - OPENAI_API_KEY

環境変数の想定する名前がシークレット名と異なる

例えば DB_URLDATABASE_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

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

Kamalのビルドテストコマンド用に記載します。(使い方は後述)

config/deploy.testing.yml
# 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作)

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

このマクロをターゲットで使用することで、dotenvを呼ぶ手間を軽減します。

例: 初回デプロイのターゲット

初回デプロイでは、setupサブコマンドでDocker等をセットアップします。

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

以下のコマンドで bundle exec dotenv -f .env.kamal.staging kamal setup -d staging が実行されます。

make kamal-staging-first-deploy

例: webコンテナのログを見るターゲット

Makefile
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 も用意しましょう。

.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 が実行され、ビルドを検証できます。

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

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

Kamalのワークフロー:

Laravelの設定例:

  • Deploying Laravel Apps with Kamal 2.0
    • HeyのTony Messias氏の非常に詳細なLaravelデプロイのチュートリアルです。redisをはじめたアクセサリを併用する際やアセットバンドルの注意点も開設されています。

Discussion