1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NLBの後ろのECSでTLS終端するとACMが使えない:証明書を「取りに行く」か「配る」か

1
Posted at

はじめに

NLB を TCP リスナーにして、後ろの ECS コンテナで TLS 終端している構成がある。証明書はコンテナの中にあり、ACM のように「ロードバランサに紐づけて放置」ができない。

「更新スクリプトを書けばいい」と思って手をつけると、更新した証明書を誰が・どこに置き・どうやって各タスクに届け・どうやって動いているプロセスに読ませるかで詰まる。EC2 で certbot 1 本に任せていた仕事が、ECS では 1 本にまとまらないからだ。

この記事の主張は 1 行で言える。証明書更新を 発行・保管・配布・反映 の 4 段に分けて考える。「取りに行く(pull)か配る(push)か」は配布だけの話ではなく、配布と反映の組み合わせで決まる。

この記事で分かること:

  • そもそも自分の構成で ACM が本当に使えないのかの切り分け
  • EC2 の certbot 構成が ECS で崩れる理由
  • pull / push の 3 方式と、それぞれで「反映」がどう変わるか
  • 入手元(Let's Encrypt / 商用 CA / ACM エクスポート / ACM ACME)が変わると更新ジョブのどこが変わるか

ACME のチャレンジ方式(HTTP-01 / DNS-01)そのものの仕組みは扱わない。ACMを使わない証明書更新:ACME チャレンジと certbot 実践 に譲る。

この記事は動作検証をしていない整理記事である。 事実は AWS・nginx・Let's Encrypt の公式ドキュメントに依拠し、本文にリンクを載せる。AWS CLI のコマンド形は aws-cli 2.33.14 の help で実在を確認した。時点は 2026-09-02。


まず切り分け:本当に ACM が使えないのか

「NLB だから ACM が使えない」は誤り。
NLB の TLS リスナーには ACM の証明書を紐づけられる。ACM のマネージド更新もそのまま効く。

NLB のリスナーには 2 種類ある。

[TLS リスナー]  クライアント ──TLS──▶ NLB(ここで復号・証明書は ACM)──TCP/TLS──▶ ECS
[TCP リスナー]  クライアント ──TLS──▶ NLB(復号せず素通し)──TLS──▶ ECS(ここで復号・証明書はコンテナ内)
  • TLS リスナーは「暗号化・復号の仕事をロードバランサにオフロードする」ための選択肢である。証明書は ACM 推奨
  • TCP リスナーは「ターゲット側で復号させたい場合」に 443 番で作る。ロードバランサは復号せずに通す
  • ACM が使えなくなるのは TCP リスナーにしてコンテナで終端する時だけである

ECS 側で TLS 終端になる理由

理由 補足
mTLS(クライアント証明書)をアプリで検証したい NLB は mTLS 非対応。公式が「TCP リスナーにしてターゲットで実装せよ」と明記
HTTP 以外のプロトコル(SMTP / MQTT / 独自 TCP など) NLB の TLS リスナーで終端すること自体はできる。TCP にするのは、アプリが TLS の中身(ALPN や SNI)を自分で扱いたい時
エンドツーエンド暗号化の要件 ロードバランサで復号させたくない。秘密鍵を自分のシステムから出せない
固定 IP が必要で NLB を選び、TLS もそのままアプリ側に残った 移行時によくある経緯。技術的必然ではない
案件の事情 既存アプリが TLS 終端込みで作られている、など

👉 mTLS が理由なら、NLB では TCP リスナー一択になる(公式に「Network Load Balancers do not support mutual TLS authentication」とある)。
👉 自分の理由が表に無いなら、TLS リスナーに戻して ACM に任せられないか先に検討する。この記事の残りは「戻せない人」向けである。

出典: Listeners for your Network Load BalancersServer certificates for your Network Load Balancer


EC2 の certbot 構成をそのまま持ち込むと壊れる 3 点

EC2 なら certbot を入れて systemd timer で certbot renew を回し、deploy hook で nginx を reload すれば終わりである。1 台の中で発行・保管・配布・反映が全部完結している。ECS に持ち込むと、この前提が 3 つ崩れる。

1. コンテナのファイルシステムは揮発する

  • Fargate のエフェメラルストレージはタスク単位で割り当てられ、タスクが止まれば消える
  • certbot が /etc/letsencrypt/ に書いた証明書は、次のタスク起動時には無い
  • ❌ 「ならイメージに焼けばいい」→ 秘密鍵をコンテナレジストリに置くことになる。更新のたびにイメージを作り直すことにもなる

出典: Fargate task ephemeral storage

2. タスクが複数いる

  • certbot は「1 台の中で更新して、1 台に反映する」道具である
  • サービスのタスクが 3 つあれば、3 つに同じ証明書を届ける仕組みが別に要る
  • タスク数は Auto Scaling で増減するので「全タスクに ssh で配る」は成り立たない

3. HTTP-01 の 80 番がどのタスクに届くか制御できない

  • HTTP-01 は Let's Encrypt が http://example.com/.well-known/acme-challenge/<token> を取りに来る
  • NLB が 80 番をどのタスクに振るかは選べない。チャレンジファイルを置いたタスクに検証が届く保証がない
  • → ECS では DNS-01 が定番になる。DNS を書き換えるのはどのタスクからでもよいし、タスクの外からでもよい

👉 チャレンジ方式の違いは ACMを使わない証明書更新:ACME チャレンジと certbot 実践 へ。

つまり、1 台の中で完結していた仕事を、発行・保管・配布・反映の 4 つに分解して、それぞれ誰がやるかを決める必要がある。


4 段に分解する

その 4 段を順に並べると、こうなる。

各段の選択肢を先に並べておく。詳細は次章以降で扱う。

  • 発行・更新: スケジュール ECS タスク / Lambda / サイドカー
  • 保管: Secrets Manager / S3 / EFS / ACM
  • 配布: 起動時に取りに行く / 定期的に取りに行く / 共有ストレージに置いて配る
  • 反映: タスク入れ替え / reload シグナル

「取りに行く(pull)か配る(push)か」は配布だけの話ではない。配布と反映の組み合わせで決まる。 この組み合わせは 7 章と 8 章で扱う。


発行・更新:誰が certbot を動かすか

「誰が certbot を動かすか」の選択肢は 3 つある。

選択肢 特徴 向いている場面
スケジュール ECS タスク 更新日に 1 回だけ certbot を動かし、保管先に書いて終了する。常駐しない 最小構成。まずこれ
Lambda 同上。certbot をパッケージするか ACME クライアントライブラリを使う ECS タスクを増やしたくない時
サイドカー サービスタスクの隣で常駐し、定期的に更新する 更新と配布を 1 つにまとめたい時

👉 スケジュール ECS タスクの組み方は既存記事「ECS のスケジュール実行(EventBridge → ECS タスク)」へ。

👉 サイドカー方式はタスク数分だけ certbot が動く。タスクが 3 つなら 3 つの certbot が同じドメインの証明書を別々に取りにいくことになる。CA のレート制限とアカウント管理を考えると、更新は 1 か所に寄せるほうが楽である。

DNS-01 と Route 53

3 章で見た「80 番問題」の帰結として、ECS では DNS-01 を使うのが定番になる。

  • certbot の Route 53 プラグインが必要とする IAM 権限は 3 つ: route53:ListHostedZonesroute53:GetChangeroute53:ChangeResourceRecordSets(最後の 1 つは対象ホストゾーンに絞れる)
  • 更新ジョブのタスクロールにこの 3 つを付ける。認証情報は AWS の標準チェーンで拾われるので、タスクロールがあればキーの埋め込みは不要

出典: certbot-dns-route53


保管:更新後の証明書の「正」はどこか

更新ジョブが取ってきた証明書を、タスクが取りに来る場所を決める。ここが証明書の「正」になる。

保管先 仕様 ECS からの取り出し方
Secrets Manager 秘密値は最大 65,536 バイト。バージョンは 1 秘密あたり 100 まで タスク定義の secrets で環境変数に注入。JSON キー・バージョンステージ・バージョン ID を ARN 末尾で指定できる(Fargate 1.4.0 以降)
S3(暗号化バケット) サイズ制限は事実上ない entrypoint やアプリが SDK / CLI で取得してファイルに書く。AWS が推奨パターンの一つとして挙げている
EFS 全タスクから同じファイルが見える。Fargate は 1.4.0 以降で対応 タスク定義の volumes でマウント。取りに行く動作が要らない
ACM(エクスポート可能証明書) 198 日有効。期限 45 日前に ACM が更新 aws acm export-certificate で PEM を取り出す(パスフレーズ必須)。9 章で扱う

👉 証明書・チェーン・秘密鍵を合わせても数 KB。Secrets Manager の 64 KB 制限は問題にならない。

👉 Secrets Manager は PutSecretValue を 10 分に 1 回以上の頻度で呼ばないよう推奨している。証明書更新は数十日に 1 回なので普段は無関係だが、更新ジョブのリトライループを組む時だけ注意する。

出典: Secrets Manager quotasPass Secrets Manager secrets through ECS environment variablesUse Amazon EFS volumes with Amazon ECSPass sensitive data to an Amazon ECS container


配布:取りに行く(pull)か、配る(push)か

保管先にある証明書を、動いているタスクにどう届けるか。方式は 3 つある。

👉 ECS の secrets は環境変数にしか注入できない。 ファイルとしてマウントする機能は無い。PEM が環境変数に入るので、entrypoint でファイルに落とす一手間が要る
👉 注入は起動時の 1 回だけ。 公式は「秘密が更新されてもコンテナは新しい値を自動では受け取らない。新しいタスクを起動するか、サービスなら強制デプロイせよ」と明記している

起動時 pull:タスクが立ち上がる時に 1 回取りに行く

一番単純な方式である。タスク定義の secrets で環境変数に注入し、entrypoint がファイルに書いてからプロセスを起動する。

新しい証明書は新しいタスクにしか届かない。 動いているタスクは古いままである。反映はタスク入れ替え(8 章)とセットになる。

👉 動作確認はしていない。骨格を示す擬似コードである。

{
  "containerDefinitions": [
    {
      "name": "app",
      "secrets": [
        {
          "name": "TLS_FULLCHAIN",
          "valueFrom": "arn:aws:secretsmanager:{region}:111122223333:secret:example-com-tls-AbCdEf:fullchain::"
        },
        {
          "name": "TLS_PRIVKEY",
          "valueFrom": "arn:aws:secretsmanager:{region}:111122223333:secret:example-com-tls-AbCdEf:privkey::"
        }
      ]
    }
  ]
}

👉 秘密は {"fullchain": "...", "privkey": "..."} の JSON で 1 つにまとめ、ARN 末尾の JSON キーで取り分ける。末尾の :: はバージョンステージとバージョン ID を省略した形(既定は AWSCURRENT)。

entrypoint の擬似コードは次のようになる。

#!/bin/sh
set -eu
umask 077
mkdir -p /run/tls
printf '%s\n' "$TLS_FULLCHAIN" > /run/tls/fullchain.pem
printf '%s\n' "$TLS_PRIVKEY"   > /run/tls/privkey.pem
unset TLS_FULLCHAIN TLS_PRIVKEY
exec nginx -g 'daemon off;'

init サイドカー方式: 環境変数は docker inspect やログで漏れやすいので、AWS は「サイドカーが Secrets Manager から読んで共有ボリュームに書き、dependsOn でアプリより先に動かして終了させる」方式を推奨している。アプリ側は環境変数を触らず、ファイルを読むだけになる。

👉 5 章のサイドカーとは役割が違う。5 章のサイドカーは certbot を動かして証明書そのものを取ってくるが、この章のサイドカーは保管先から読んで置くだけである。

👉 動作確認はしていない。骨格を示す擬似コードである。

{
  "volumes": [{ "name": "tls" }],
  "containerDefinitions": [
    {
      "name": "tls-init",
      "essential": false,
      "mountPoints": [{ "sourceVolume": "tls", "containerPath": "/run/tls" }]
    },
    {
      "name": "app",
      "dependsOn": [{ "containerName": "tls-init", "condition": "SUCCESS" }],
      "mountPoints": [{ "sourceVolume": "tls", "containerPath": "/run/tls", "readOnly": true }]
    }
  ]
}

tls-init の中身は「Secrets Manager から GetSecretValue してファイルに書いて終了する」だけである。タスクロールに secretsmanager:GetSecretValue が要る。

出典: Pass sensitive data to an Amazon ECS container

定期 pull:サイドカーが保管先を見張り続ける

上の tls-init を終了させず、常駐させて定期的に保管先を見に行く形である。

  • 変わっていたら共有ボリュームに書き直す。動いているタスクに新しい証明書が届く
  • ただし、ファイルが変わっただけではプロセスは読み直さない。反映(8 章の reload シグナル)が別に要る
  • 見張り方: Secrets Manager なら DescribeSecretAWSCURRENT のバージョン ID を覚えておき、変わったら GetSecretValue。S3 なら HeadObject の ETag。ポーリング間隔は数分〜1 時間で十分(更新は数十日に 1 回)

👉 常駐するものが 1 つ増える。サイドカーが落ちた時にタスクをどうするか(essentialtrue にするか)を決めておく。

push(共有ストレージ):EFS に置けば全タスクから同時に見える

全タスクに EFS をマウントし、更新ジョブがそこへ書く。書いた瞬間に全タスクから見える。 取りに行く動作が無い。

これも反映は別に要る(8 章)。

👉 動作確認はしていない。骨格を示す擬似コードである。

{
  "volumes": [
    {
      "name": "tls",
      "efsVolumeConfiguration": {
        "fileSystemId": "fs-0123456789abcdef0",
        "transitEncryption": "ENABLED"
      }
    }
  ]
}

👉 Fargate は 1.4.0 以降で EFS に対応。Fargate では EFS を管理する supervisor コンテナがタスクの CPU・メモリを少し使う。
👉 秘密鍵をネットワークファイルシステムに置くことになる。アクセスポイントと IAM でタスクロールに絞る。

出典: Use Amazon EFS volumes with Amazon ECS

3 方式をまとめると次のようになる。

方式 新しい証明書が届く先 常駐するもの 反映
起動時 pull 新しいタスクだけ 無し タスク入れ替え(8 章)
定期 pull 動いているタスク サイドカー reload シグナル(8 章)
push(EFS) 動いているタスク 無し(EFS のマウント) reload シグナル or タスク入れ替え(8 章)

反映:ファイルを置き換えただけでは切り替わらない

プロセスは起動時に証明書と秘密鍵を読んでメモリに持ち、以後はファイルを見ない。

「EFS に新しい証明書を置いた。全タスクから見えている。だから反映された」は誤り。 次にプロセスが読み直すまで、古い証明書を返し続ける。期限切れの証明書を返すのはこの状態である。

反映の手段は 2 つある。

タスク入れ替え:強制デプロイで新しいタスクに読ませる

サービスを強制デプロイすると、同じタスク定義のまま新しいタスクがローリングで立ち上がる。新しいタスクは起動時 pull で新しい証明書を読む。

更新ジョブの最後にこれを呼ぶだけでよい。仕組みが一番少ない。

aws ecs update-service \
    --cluster my-cluster \
    --service my-service \
    --force-new-deployment

👉 公式も「秘密が変わったら強制デプロイか新しいタスクの起動が必要」と書いている。証明書更新はまさにこのケースである。
👉 ローリング中は新旧の証明書が混在する。どちらも有効期限内なので問題にはならない。

reload シグナル:動いているプロセスに読み直させる

nginx は HUP シグナルで設定を再読込する。公式の説明では、マスタープロセスが構文を検査し、新しい設定を適用して新しいワーカーを起動し、古いワーカーには graceful に終了するよう伝える。証明書ファイルもこの時に読み直される。

nginx -s reload は HUP を送るコマンドである。

前提が 2 つある。

  1. アプリが reload に対応していること。nginx などは対応している。自前の TCP サーバーは自分で実装する必要がある
  2. シグナルを送る側が同じ PID 名前空間にいること。サイドカーから送るなら、タスク定義で pidMode: task を指定する。「task を指定すると、タスク内の全コンテナが同じプロセス名前空間を共有する」。Fargate は 1.4.0 以降(Linux)で対応する

👉 動作確認はしていない。骨格を示す擬似コードである。

{
  "pidMode": "task"
}

👉 aws ecs execute-command でタスクに入って nginx -s reload を打つこともできる。手動の緊急対応としては使えるが、自動化の経路にはしない。タスクの数だけ打つことになる。

出典: nginx: Controlling nginxAmazon ECS task definition parameters

7 章の 3 方式と組み合わせるとこうなる。

配布 反映 動いているタスクへの反映 必要なもの
起動時 pull タスク入れ替え 入れ替え後 更新ジョブから強制デプロイを呼ぶ経路(タスクロールに ecs:UpdateService
定期 pull reload シグナル サイドカーが検知した後 pidMode: task、reload 対応のアプリ
push(EFS) reload シグナル or タスク入れ替え 書いた後 EFS、reload 手段

👉 「取りに行く」も「配る」も、最後にプロセスに読ませる手段が無ければ意味がない。 配布方式を選ぶ時は、反映方式とセットで選ぶ。


入手元が変わると何が変わるか

ここまでの 4 段のうち、入手元が変わっても確実に差し替わるのは「発行・更新」だけである。「保管」は、ACM を「正」にする(9 章のエクスポート可能証明書で、export をタスクの起動時 pull に組み込む)か、これまでどおり Secrets Manager などへ写すかの選択になる。「配布」と「反映」(7 章・8 章)は入手元に依存しない。

Let's Encrypt 商用 CA ACM エクスポート可能証明書 ACM ACME エンドポイント
有効期間 90 日(classic)。2027-02-10 から 64 日、2028-02-16 から 45 日。tlsserver プロファイルは 2026-05-13 から 45 日 CA による。2026-03-15 以降は最大 200 日、2027-03-15 以降 100 日、2029-03-15 以降 47 日 198 日 45 日
更新を動かすのは 自分の ACME クライアント 人(発行は手作業) ACM(期限 45 日前) 自分の ACME クライアント
更新をどう知るか クライアントの deploy hook EventBridge の ACM Certificate AvailableAction: RENEWAL クライアントの deploy hook
チャレンジ HTTP-01 / DNS-01(ECS では DNS-01) CA の手順 ACM の DNS 検証(CNAME) なし。管理者が事前に CNAME で承認
秘密鍵の所在 クライアント 自分 ACM が生成。エクスポート時にパスフレーズ付きで取り出す クライアント
料金(執筆時点:2026-09-02) 無料 CA による FQDN あたり 7 USD、ワイルドカード 79 USD(発行時と更新時) 最初の 1,000 ドメインは 1 ドメイン 1 USD/月、ワイルドカード 5 USD/月
ELB に載せられるか 不可(ACM にインポートすれば可) 不可(ACM にインポートすれば可) 不可

出典:

ACM エクスポート可能証明書:更新は ACM、配布と反映は自分

  • 発行時に --options Export=ENABLED を付ける。ACM が秘密鍵を生成し、198 日の証明書を出す
  • 取り出しは export-certificate。パスフレーズ必須。取り出した秘密鍵はパスフレーズで暗号化されているので、使う前に復号する
aws acm request-certificate \
    --domain-name www.example.com \
    --validation-method DNS \
    --options Export=ENABLED

aws acm export-certificate \
    --certificate-arn arn:aws:acm:{region}:111122223333:certificate/{certificate-id} \
    --passphrase fileb://passphrase.txt

👉 パスフレーズファイルは改行で終わってはいけない(公式の注意)。

更新は ACM が期限 45 日前に行う。更新対象になる条件は「発行または前回更新以降にエクスポートされていること」である。 取り出さずに放置した証明書は更新されない。

公式の Limitations にはこうある。「You must manage the deployment process for exported certificates」。つまり、更新は ACM の仕事だが、配布と反映は変わらず自分の仕事である。

push 型の典型はこう流れる。ACM Certificate AvailableAction: RENEWAL)→ EventBridge → Lambda が export-certificate を呼ぶ → Secrets Manager に PutSecretValue で書く → update-service --force-new-deployment発行・更新を ACM が持つだけで、残りは 6〜8 章そのままである。ACM をそのまま「正」にして、タスクの entrypoint や init サイドカーが起動時に export-certificate を呼ぶ(起動時 pull)形でもよい。Secrets Manager への写しが要らない代わりに、タスクロールに acm:ExportCertificate が要る。

ACM ACME エンドポイント:certbot がそのまま使える、チャレンジは無い

2026-07-06 に提供が始まった。ACM が ACME サーバーを提供し、certbot や acme.sh がそのまま使える。

仕組みはこうである。管理者が ACME エンドポイントを作り、ドメインを CNAME で事前に承認する(PRE_APPROVED)。クライアントは EAB(外部アカウント紐づけ)の資格情報で登録し、証明書を要求する。クライアント側でチャレンジは行わない。 3 章の 80 番問題も、DNS-01 のための Route 53 権限も要らない。

有効期間は 45 日である。更新は ACME クライアントの仕事であり、ACM のマネージド更新は適用外である。秘密鍵はクライアントが生成し、「ACM never sees the private key」。

ELB には載せられない。 つまり「ECS 側で TLS 終端する」用途のための機能である。

以下は AWS 公式ドキュメント記載のコマンドである。ACME エンドポイントを作る aws-cli のコマンドが 2.33.14 には無く、エンドポイントを用意できないため、手元では実行していない。

certbot certonly \
    --standalone \
    --non-interactive \
    --agree-tos \
    --email user@example.com \
    --server https://acm-acme-enroll.{region}.api.aws/{endpoint-id}/directory \
    --eab-kid {eab-key-id} \
    --eab-hmac-key {base64-mac-key} \
    --issuance-timeout 120 \
    --domain www.example.com

👉 発行に最大 2 分かかる。公式がタイムアウトを 120 秒以上にするよう注意している。
👉 同じ ACME アカウント・同じメタデータで更新すると、ACM 上の証明書 ARN は変わらない。

4 段に当てはめると、発行・更新は「certbot をスケジュール ECS タスクで動かす」構成のままでよい。変わるのは --server の向き先とチャレンジが要らないことだけである。保管以降は 6〜8 章そのままでよい。

👉 有効期間は業界全体で短くなっていく。CA/Browser Forum の決定で、公開証明書の最大有効期間は 2026-03-15 から 200 日、2027-03-15 から 100 日、2029-03-15 から 47 日になる。Let's Encrypt は 2028-02-16 に classic プロファイルを 45 日にする。「商用 CA だから年 1 回手で入れ替える」前提は、遠からず成り立たなくなる。 配布と反映を自動化しておく理由はここにもある。


選び方

状況 配布 反映
タスク入れ替えの断が許される(ほとんどの Web / API) 起動時 pull タスク入れ替え
入れ替えの断が許されない、かつアプリが reload 対応 定期 pull または push(EFS) reload シグナル
入れ替えの断が許されない、かつアプリが reload 非対応 起動時 pull タスク入れ替え(デプロイ設定で最小稼働率を上げて断を隠す)

迷ったら、いちばん仕組みの少ない組み合わせから入るのがよい。

最小構成の推奨:スケジュール ECS タスクで更新 → Secrets Manager に保管 → 起動時 pull → 強制デプロイで反映

理由は 4 つある。

  • 常駐するものが無い。サイドカーの生死を気にしなくてよい
  • pidMode も EFS も要らない
  • 「更新ジョブの最後に update-service --force-new-deployment を呼ぶ」の 1 行で反映が完結する
  • 入手元を Let's Encrypt から ACM ACME や ACM エクスポートに変えても、保管以降は変わらない

reload シグナル方式を選ぶのは、タスク入れ替えの断が本当に許されない時だけである。まずこの構成で組んで、断が問題になってから移ればよい。


まとめ

  • ❌「NLB だから ACM が使えない」ではない。TCP リスナーでコンテナが終端する時だけ自前更新が要る
  • EC2 の certbot 構成は、ファイルシステムの揮発・複数タスク・80 番の宛先の 3 点で崩れる
  • 発行・保管・配布・反映の 4 段に分解する。「取りに行くか配るか」は配布と反映の組み合わせで決まる
  • ファイルを置き換えただけでは切り替わらない。タスク入れ替えか reload シグナルが要る
  • 入手元が変わっても、差し替わるのは発行・更新だけ(ACM を正にするなら保管も)。ACM ACME はチャレンジ不要で ECS 終端向き。有効期間は短くなる一方なので、配布と反映は先に自動化しておく
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?