手元で試してから,
Azure へ小さく導入する

このページでは,ACME Conductor をまず 1 台のマシン上で動かして仕組みを確かめ, 次に Azure Container Apps へステージング環境としてデプロイするまでを,実際の コマンドと設定例に沿って説明します.どちらもステージング CA と 1 つのホスト名 から始めることを前提にしています.

必要なもの

手元で試す場合

  • Go のビルド環境(make build でソースからビルドする場合),または GHCR の公開イメージを取得できるネットワーク
  • bin/acme-conductor と bin/acme-runner を同じホストに置けること(Conductor のコンテナイメージには Runner が含まれません)
  • Let's Encrypt ステージング の ACME ディレクトリへの通信と,テスト用の DNS ゾーン(本ガイドの例は Azure DNS)
  • curl と jq(REST API を試すため)

Azure にデプロイする場合

  • デプロイ用の Azure サブスクリプションと,リソースグループを作成する権限
  • DNS ゾーン(Azure DNS)と RBAC 権限モデルの Key Vault — デプロイと同じサブスクリプションに置くこと(カスタムロールの割り当て可能スコープの制約)
  • そのサブスクリプションにロール定義とロール割り当てを作成する権利(Owner または User Access Administrator 相当)
  • OIDC プロバイダ(例: Microsoft Entra ID)に,API 用と GUI 用の 2 つのアプリ登録を作成できること
  • acme-conductor keygen と acme-runner keygen で作る,交換用共有の往復それぞれに 1 組ずつの Ed25519 署名鍵ペア(合計 2 組)

手元で試す

開発用の認証(localhost-dev)と,Runner を子プロセスとして起動する local-process ランチャー,ファイルシステムの Certificate Store を使い, 1 台のマシンの上に Conductor と Runner の両方を動かします.ACME は Let's Encrypt の ステージング を使うため,本物の証明書は発行されません.

手元で試すときの構成 1 台のホストの上で,Conductor プロセスが子プロセスとして Runner を起動する.Runner は lego を呼び出し,ステージング CA と DNS プロバイダに問い合わせ,証明書と秘密鍵をファイルシステムの Certificate Store に書き込む.操作者は curl で Conductor の REST API を叩く. 操作者 curl / jq 1 台のホスト acme-conductor serve localhost-dev 認証 local-process ランチャー 127.0.0.1:8080 SQLite(conductor.db) 子プロセス起動 acme-runner reconcile lego を 1 度だけ起動 --job / --result Let's Encrypt staging azuredns バインディング 証明書 + 秘密鍵 filesystem Certificate Store(/store)
Conductor プロセス Runner の子プロセス 秘密鍵が置かれる場所 操作者
この構成では Conductor の環境変数がパススルーで Runner に渡るため(passthroughEnv),単一ユーザーの開発ホスト以外では使わない構成です.

1. ビルドする

make build    # ./bin/acme-conductor と ./bin/acme-runner
./bin/acme-conductor --version
./bin/acme-runner --version

GHCR のイメージ(イメージとリリース)から始めても構いませんが,Conductor のイメージには Runner が含まれないため,local-process ランチャーを試すにはソースからビルドするか,両方のバイナリを含む独自イメージを用意します.

2. 署名鍵を作る

手元での試用では省略しても動きますが(jobSigning のないローカルランチャーは生の JobSpec を渡します),練習を兼ねて作っておくと Azure デプロイの手順に直結します.

./bin/acme-conductor keygen --private job-signing.pem --public job-signing.pub

3. 設定を書く

deploy/examples/conductor-config.example.json と deploy/examples/runner-config.example.json をコピーして,パスを手元の環境に合わせます.バインディング名(letsencrypt-staging,azure-dns-staging,filesystem-dev)は両方の設定で一致していなければなりません.Conductor はバインディング名しか知らず,実体は Runner 側の設定が持ちます.次は Conductor 側の設定の抜粋です(apiVersion,server,database,scheduler などは例のファイルを参照).

{
  "executionBindings": {
    "local": {
      "type": "local-process",
      "config": {
        "runnerBinary": "/path/to/bin/acme-runner",
        "runnerConfig": "/path/to/runner-config.json",
        "workDir": "/path/to/conductor-runs"
      }
    }
  },
  "acmeBindings": ["letsencrypt-staging"],
  "dnsBindings": ["azure-dns-staging"],
  "storeBindings": ["filesystem-dev"]
}

Runner 側は storeBindings.filesystem-dev を type: filesystem でファイルシステムの directory に向け,DNS の資格情報が必要なら dnsBindings.azure-dns-staging.passthroughEnv に環境変数名を列挙し,Conductor を起動する側の環境でその変数を export します.

4. Conductor を起動する

./bin/acme-conductor serve --config /path/to/conductor-config.json

既定では 127.0.0.1:8080 で REST API と GUI(/ui/)が動きます.localhost-dev モードでは,ループバックに接続できるローカルユーザーは誰でも管理者として扱われます.

5. ポリシーと target を登録する

docs/conductor.md のセッション例 に沿って,ポリシーを作成し,それを参照する target を作成します.

C=http://127.0.0.1:8080/api/v1alpha1
P=$(curl -s -H 'Content-Type: application/json' \
  -d '{"allowedDnsSuffixes":["example.ac.jp"],"acmeBinding":"letsencrypt-staging","renewBeforeDays":30,"keyType":"ec256"}' \
  $C/policies | jq -r .id)
T=$(curl -s -H 'Content-Type: application/json' \
  -d "{\"fqdn\":\"wiki.example.ac.jp\",\"owner\":\"web-team\",\"policyRef\":\"$P\",\"executionBinding\":\"local\",\"dnsBinding\":\"azure-dns-staging\",\"storeBinding\":\"filesystem-dev\"}" \
  $C/targets | jq -r .id)

target の作成でスケジューラが即座に起こされ,証明書がまだない target は待機中の run になります.次で進み具合を見ます.

6. run を見守る

curl -s $C/targets/$T | jq .lastRun
curl -s "$C/audit?targetId=$T" | jq '.items[].action'

lastRun.status が succeeded になれば,filesystem-dev の directory の下に証明書と秘密鍵が書き込まれています.手動で今すぐ run を要求したい 場合は curl -s -X POST $C/targets/$T/runs を使います(アクティブな run がある間は 409).GUI(http://127.0.0.1:8080/ui/)でも同じ内容を確認できます.

Azure にデプロイする

同梱の Bicep テンプレート(deploy/azure)は, Container Apps 環境の上に HTTPS ingress を持つ Conductor の Container App と,毎分スケジュール実行される Runner の Container Apps Job,権限の重ならない 2 つのユーザー割り当てマネージド ID,3 つの Azure Files 共有, DNS ゾーンと Key Vault へのロール割り当て,Log Analytics を一括でプロビジョニングします.

Azure にデプロイされるリソース Container Apps 環境の中に,HTTPS ingress を持つ Conductor の Container App と,毎分スケジュール実行される Runner の Container Apps Job がある.Conductor 用と Runner 用の2つのユーザー割り当てマネージド IDがあり,それぞれ異なるカスタムロールを持つ.Conductor の ID は Runner の Job に対する Conductor Job Execution Observer ロールだけを持つ.Runner の ID は DNS ゾーンに対する Runner DNS TXT Writer ロールと,Key Vault に対する Runner Key Vault Certificate Writer ロールを持つ.ストレージアカウントは3つの Azure Files 共有を提供する.DNS ゾーンと Key Vault は別リソースグループにあってもよいが同じサブスクリプションになければならない.Log Analytics が両方のコンテナのログを受け取る. 管理者 GUI / API(OIDC) Container Apps 環境 Conductor Container App HTTPS ingress + OIDC 署名付き JobSpec を差し出す id: id-conductor Runner Container Apps Job 毎分スケジュール実行 reconcile --exchange /exchange id: id-runner Conductor Job Execution Observer read / list / stop execution のみ Runner DNS TXT Writer Runner Key Vault Certificate Writer Job の開始や変更の権限はなし ストレージアカウント(Azure Files) conductor-state / runner-state / exchange DNS ゾーン(Azure DNS) 同じサブスクリプション Key Vault RBAC 権限モデル OIDC プロバイダ Entra ID など Log Analytics 両コンテナのログ Conductor の ID は DNS・Key Vault・ストレージのデータ権限を持たない.Runner の ID は Job・ストレージアカウントの権限を持たない. どちらの ID もストレージアカウントキーを読めない(共有は環境がマウントする).
Conductor 側 Runner 側 2 つのコンテナが共有する状態 デプロイの前提として用意するもの
3 つのカスタムロールはいずれも最小権限で,サブスクリプションスコープで定義されます.そのため DNS ゾーンと Key Vault はデプロイと同じサブスクリプションになければなりません.
exchange 共有での受け渡し Conductor は署名付きジョブを staging に書き,1回のリネームで pending に移す.Runner の毎分の実行がそれを claimed にリネームして取り,処理後に result.json を書く.Conductor は execution の状態をポーリングし,終了を確認してから run ディレクトリを削除する.Conductor はどの実行も開始しない. Conductor 署名付き JobSpec exchange 共有(両コンテナがマウント) staging/ rename pending/ claim claimed/ Runner の 毎分の実行 result.json(署名付き) Conductor が 状態をポーリング Conductor は execution を決して開始しない — 開始できるのはプラットフォームのスケジュールだけ claimTimeoutSeconds 以内に誰も取らなければ,Conductor は同じ rename で差し出しを取り下げる
Conductor が書く/読む Runner が書く/読む 署名付き文書
exchange 共有は Conductor が所有するトランスポートではないため,行き来する 2 種類の文書はどちらも Ed25519 で署名され,相手の公開鍵で検証されます.staging/ から pending/ への移動も,pending/ から claimed/ への移動も 1 回のディレクトリの rename です.Conductor は pollIntervalSeconds(既定 10 秒)ごとに実行の状態を読み,終端状態になるまで待ちます.

まずはステージング CA と 1 つのホスト名から始めてください. このテンプレートは 1 つの組織のサブスクリプションでの staging デプロイでは端から端まで 確認できていますが(同時実行時のリネームの原子性や SMB 上の flock など未確認の項目が残っています.新しい環境へのデプロイは,最初から最後まで見守ってください.

手順

  1. 署名鍵ペアを 2 組作る — 交換用共有の往復それぞれに 1 組.
    acme-conductor keygen --private job-signing.pem --public job-signing.pub
    acme-runner keygen --private result-signing.pem --public result-signing.pub
    各コマンドが表示する publicKey: の 1 行を,次の手順の設定に貼り付けます.
  2. Runner の設定を書く — deploy/examples/runner-config.aca.example.json を出発点にします.lego.stateDir は /state,lego.workDir は /work のままにし,DNS バインディングはマネージド ID で認証するようにします(AZURE_AUTH_METHOD は設定しません.passthroughEnv に AZURE_CLIENT_ID,IDENTITY_ENDPOINT,IDENTITY_HEADER を列挙).jobSigning.publicKeys は Bicep テンプレートが自動で追加するため,ここには書きません.
  3. main.bicepparam をコピーして値を埋める — deploy/azure ディレクトリ内にコピーします(runnerConfigJson の loadTextContent が相対パスで解決するため).イメージのダイジェスト,バインディング名,DNS ゾーン,Key Vault,公開鍵,OIDC の値を記入します.
    cd deploy/azure
    cp main.bicepparam my.bicepparam
  4. 秘密鍵を環境変数として export してデプロイする — 秘密鍵は readEnvironmentVariable で読まれ,パラメータファイルには書きません.
    export ACME_JOB_SIGNING_PRIVATE_KEY_PEM="$(cat job-signing.pem)"
    export ACME_RESULT_SIGNING_PRIVATE_KEY_PEM="$(cat result-signing.pem)"
    az deployment group create --resource-group rg-acme \
      --template-file main.bicep --parameters my.bicepparam
    イメージはダイジェストで固定して GHCR から直接 pull されるため,ローカルでのビルドや GHCR の資格情報は不要です.
  5. 最初の実行を確認する — Runner の設定はデプロイ時には検証されず,各実行の開始時に読み込まれます.待機中のジョブがなくても実行は毎分動き,no pending job; nothing to do で Succeeded になるのが正常です.
    az containerapp job execution list -g rg-acme -n <prefix>-runner \
      --query "[0:3].{name:name,status:properties.status}" -o table
    az containerapp job logs show -g rg-acme -n <prefix>-runner \
      --execution <execution name> --container runner --format text
  6. GUI のリダイレクト URI を登録する — デプロイ後に出力される conductorGuiRedirectUri(https://<app fqdn>/ui/)を,OIDC プロバイダ側の GUI 用アプリ登録(シングルページアプリケーション)のリダイレクト URI に設定します.アプリの FQDN は初回デプロイ後にはじめて判明するため,これだけを後から登録すればよく,再デプロイは不要です.

実際に組織のサブスクリプションへ 1 つずつコマンドを実行した記録は deploy/azure/walkthrough.md にあります.命名や権限の確認,Entra ID のアプリ登録の作り方まで,順を追って書かれています. 生成 AI(Claude Code など)に作業を手伝わせたい場合は deploy/azure/ai-assisted-deploy.md に,何を AI に任せてよく何を人が判断すべきかがまとめられています.

デプロイ後の運用

項目内容
到達方法API と GUI には conductorUrl(デプロイの出力)で到達します.HTTPS のみで,/api/ 配下のすべてのリクエストと GUI の操作には admin または viewer ロールを持つベアラートークンが必要です./healthz,/readyz,GUI の静的ファイルだけが認証不要です.
バックアップ(Conductor)conductor-state 共有上の conductor.db です.共有のスナップショットを取るか,アプリをゼロにスケールした状態でファイルをコピーします.runner-state 共有(ACME アカウント鍵)もバックアップ対象です.exchange 共有には永続的なものはありません.
復元プロセス(またはアプリ)を止めてファイルを戻します.新しいバイナリは起動時にスキーマを前方に移行しますが,古いバイナリは新しいスキーマのデータベースを拒否するため,ロールバックには対応するバックアップの復元が伴います.バックアップ時点で実行中だった run は次の起動時に「結果不明」として失敗扱いになります.
ロールバック以前のテンプレートとパラメータで az deployment group create を実行するか,以前のイメージダイジェストを設定し,対応する conductor.db も復元します.
署名鍵のローテーションまず検証側に新しい公開鍵を追加し(Runner の jobSigning.publicKeys,または Conductor の resultSigningPublicKey.どちらも複数保持できます),デプロイし,次に署名側を新しい秘密鍵に切り替え,最後に古い公開鍵を取り除きます.
残留した run ディレクトリ実行の終了が確認できなかったとき,Conductor は exchange 共有上に claimed/run-<runId>/ を残し「run directory kept」とログに記録します.az containerapp job execution list で実行の終了を確認したうえで,手で削除します.

イメージとリリース

バージョンタグを打つと,両方のイメージが SBOM と provenance を添えて ghcr.io/cits-nue/acme-conductor と ghcr.io/cits-nue/acme-runner に linux/amd64 と linux/arm64 向けに公開されます.固定する前に リリースを検証し,そのダイジェストで固定して使います.

gh attestation verify oci://ghcr.io/cits-nue/acme-conductor:<version> --owner CITS-NUE
gh attestation verify oci://ghcr.io/cits-nue/acme-runner:<version> --owner CITS-NUE
docker buildx imagetools inspect ghcr.io/cits-nue/acme-conductor:<version>

latest タグも便宜上付きますが,デプロイが固定する対象にはしません.GHCR の公開 イメージにレジストリの資格情報は不要ですが,新しいパッケージ(名前を変えたイメージ,新しい バイナリ)を資格情報なしで pull できるようにするには,組織のオーナーが一度だけ公開設定にする 必要があります.