0
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?

UserData更新のたびにEC2を作り直していた話 〜「定石」のcfn-hupを捨ててSSM State Managerを選んだ理由〜

0
Posted at

要約

  • 個別テナントEC2のUserDataは初回起動時にしか実行されず、設定変更のたびにインスタンスを作り直す羽目になっていた
  • 定石とされるcfn-init + cfn-hupをまず検討したが、即時性・ログ集約・追加IAM権限・追加プロセスの4点で見送った
  • 代わりにAWS Systems Manager State Manager(AWS::SSM::Document + AWS::SSM::Association)を採用し、スタック更新だけで即座に既存EC2へ反映できるようにした
  • 実装時に「CloudWatch Logs書き込み権限が意外と付いていない」など、いくつか誤算もあった

以下、経緯と設計を詳しく書きます。

はじめに

マルチテナントSaaSには、顧客ごとに専用のEC2環境をCloudFormationで構築する「個別テナント」層があります。最初のうちはこれで何も問題ありませんでした。

しかし顧客数が増えるにつれ、ある問題がじわじわ効いてきます。UserDataを直してupdate-stackしても、すでに稼働しているEC2には何も反映されないという問題です。

この記事では、この課題にどう向き合ったかを書きます。最初は「定石」とされる方式を検討したのですが、最終的には別のアプローチを選びました。なぜ定石を採らなかったのか、実際どう設計したのかをまとめます。

何に困っていたか

対象は1顧客1EC2の構成です。1つのUserDataに、次の処理が全部詰め込まれていました。

  • EBSマウント
  • DBコンテナ(PostgreSQL)の起動
  • ECSクラスターへの登録
  • TLS証明書取得
  • nginx設定

原因はシンプルです。UserDataはEC2の初回起動時にしか実行されません。 つまり、テンプレートを書き換えても、すでに動いているインスタンスの中身は何も変わらないのです。

これが実務でどう痛いかというと、たとえば「nginxの設定を1行だけ直したい」という小さな変更でも、選択肢は次の2つしかありませんでした。

  1. インスタンスを作り直す → DB停止・ECS再登録・証明書再取得を伴う、確実なダウンタイム
  2. 手動でSSHして直接設定を書き換える → 動くには動くが、テンプレート上の定義と実機の状態がズレていく

どちらも辛い。ここから、UserDataに頼らずに設定変更を反映する仕組みを探すことになりました。

第一候補:cfn-init + cfn-hup

最初に検討したのは、AWSの定石とされる方式です。

  • AWS::CloudFormation::Init:CloudFormationのMetadataに「あるべき構成」を宣言する
  • cfn-hup:EC2内で常駐し、Metadataの変化をポーリングで検知してcfn-initを再実行する

この2つを組み合わせれば、テンプレートを更新するだけで既存EC2に設定を反映できます。実績も豊富で、AWS公式ドキュメントにも数多く登場する方式です。

しかし、実際に採用を検討していく中で、いくつか引っかかる点が出てきました。

反映が即時ではない。 cfn-hupはポーリング方式です。間隔は既定でも数分単位なので、スタック更新が終わってもすぐには反映されません。

ログがEC2の中に閉じてしまう。 実行ログは/var/log/cfn-init.log/var/log/cfn-hup.logといった、EC2内のファイルに残るだけです。何か障害が起きるたびにEC2へ接続してログを見に行く必要があります。

追加のIAM権限が必要になる。 cloudformation:DescribeStackResourceという権限を新たに付与しなければなりません。

EC2内の管理対象が1つ増える。 cfn-hupという常駐プロセスを、新しく監視・運用する対象として抱えることになります。

これらは致命的な欠陥というわけではなく、「定石なりの妥当なトレードオフ」ではあります。ただ、このプロジェクトには決め手になる事情がもう一つありました。

決め手:すでにSSMを使っていた

このプロジェクトでは、証明書配布用のLambda(タグ付きの全EC2に一斉実行する仕組み)で、すでにSSM Run Commandを使っていました。そのためInstanceRoleにはAmazonSSMManagedInstanceCoreというポリシーがすでに付与されており、SSM Agentを動かすための基本権限も揃っていました。

つまり、「新しい仕組みを1つ足す」(cfn-hup)のか、「すでにある基盤の上に乗せる」(SSM)のか、という選択でした。後者のほうがIAM権限体系も運用ノウハウも一本化できると判断し、SSM State Managerでの設計に切り替えました。

採用した方式:SSM State Manager

登場人物

要素 役割
AWS::SSM::Document 「あるべき構成」の宣言。中身はシェルスクリプト
AWS::SSM::Association Documentをどのインスタンスに・いつ適用するかを結びつける
SSM Agent EC2上にすでに常駐しているプロセス。新規追加は不要
CloudWatch Logs 実行結果(stdout/stderr)の送信先

cfn-init方式との一番の違いは、「変更検知」と「実行」の両方をAWS SSMサービス自身が担うことです。EC2内に監視用の常駐プロセスを新たに足す必要がありません。しかもAssociationはCFnの通常のリソースとして存在するので、update-stackのときの差分検知の仕組みがそのまま使えます。

なぜ即時に反映されるのか

State Managerには、「Associationは新規作成時・更新時に即座に実行される」という仕様があります。つまり、CFnの更新でAssociationのプロパティが変われば、ポーリング待ちなしにEC2へ新しい内容が届くということです。

ここで一つ、設計上の懸念が浮かびました。「Documentの中身だけ書き換えても、Associationのプロパティ自体は変わらないのでは? だとしたら更新イベントが発生せず、結局反映されないのでは?」という懸念です。

これを防ぐため、Documentに渡す実際の値(ドメイン名やポート番号など)を、必ずAssociationのParameters経由で渡すという設計にしました。こうしておけば、CFnパラメータが変わるたびにDocumentの内容も変わり、それに連動してAssociationのParametersも必ず変わります。結果として、CloudFormationは確実に更新を検知してくれます。

初回専用の処理はこれまで通りUserDataに残す

考え方自体はcfn-init方式のときと変わりません。Document化した処理は、Associationが実行されるたびに走り得ます。逆に言えば、初回起動時にしか実行してはいけない処理はDocument化せず、UserDataに残す必要があります。

今回の切り分けは次の通りです。

処理 Document化 理由
nginx設定 する 変更頻度が高く、無停止で反映したい
証明書取得 する 同上
ECSクラスター登録 しない 初回起動時のみ必要。デッドロック回避の制約があり繰り返し実行に不向き
EBSマウント しない 初回アタッチ完了を待つ特殊な処理のため
DBコンテナ起動 しない(将来のPhaseで再検討) 再起動を伴う操作を毎回走らせると影響が大きい

実装イメージ

nginxの設定には多くのCFnパラメータが絡んでいました。そこで、次のようにハイブリッドな方式を取りました。

  • ドメインやポート番号のような単純な値 → SSM独自の{{ }}記法でDocumentのparametersに切り出す
  • 複数行になるserverブロックのような部分 → これまで通りCFnのFn::SubでContentに直接埋め込む
NginxConfigDocument:
  Type: AWS::SSM::Document
  Properties:
    DocumentType: Command
    Content:
      schemaVersion: '2.2'
      parameters:
        domain: { type: String }
        appPort: { type: String }
      mainSteps:
        - action: aws:runShellScript
          inputs:
            runCommand:
              - |
                cat > /etc/nginx/nginx.conf <<'NGINXCONF'
                server {
                  server_name {{ domain }};
                  location / {
                    proxy_pass http://127.0.0.1:{{ appPort }};
                  }
                }
                NGINXCONF
                nginx -t
                systemctl reload nginx

NginxConfigAssociation:
  Type: AWS::SSM::Association
  Properties:
    Name: !Ref NginxConfigDocument
    Targets:
      - Key: InstanceIds
        Values: [!Ref Ec2Instance]
    Parameters:
      domain: [!Ref DomainName]
      appPort: [!Ref NaviInternalPort]
    OutputLocation:
      CloudWatchOutputConfig:
        CloudWatchLogGroupName: !Ref SsmAssociationLogGroup
        CloudWatchOutputEnabled: true

このスクリプトにはnginx -tによる文法チェックを残してあります。チェックが通ったときだけreloadするので、壊れた設定がそのまま本番に反映される心配はありません。チェックに失敗した場合はコマンドが非ゼロ終了し、実行結果は「Failed」として記録される仕組みです。

対象EC2の指定方法にも触れておきます。タグではなくInstanceIdsで1台を明示的に指定しています。1顧客1EC2の構成なので、タグ指定にしてしまうと他社のEC2を誤って巻き込むリスクがあるためです。

証明書取得のDocumentには、もう一段仕掛けがあります。共有シークレットの更新にも追従したいため、スタック更新に連動した実行に加えて、ScheduleExpression: 'rate(1 day)'で日次の定期実行も併用しています。新旧の証明書をcmp -sで比較し、差分がある場合だけreloadする冪等性チェックは、既存のスクリプトからそのまま流用しました。

実装してみて分かった誤算

設計段階では「AmazonSSMManagedInstanceCoreさえあればSSM周りは万事OK」だと思っていました。ところが実装してみると、CloudWatch Logsへの書き込み権限(logs:CreateLogStreamなど)はこのポリシーに含まれていないことが分かりました。実行結果をCloudWatch Logsに送るには、ログ出力先のロググループに限定した権限を個別に追加する必要があります。

もう一つ気づいたのは、ログの置き場所についてです。SSM用のロググループを単体で新規作成すると、既存のアプリログが使っている「CloudWatch Logs → Firehose → S3」というアーカイブ経路から孤立してしまいます。そこで、既存と同じパターンでSSMの実行ログもS3側に退避されるよう、構成を追加しました。

段階移行の進め方

全部を一気にDocument化するのはリスクが高いと判断し、影響範囲の小さいものから順に進める計画にしています。

  1. Phase1(今回の範囲):nginx設定・証明書取得をDocument化
  2. Phase2:DBコンテナ起動のDocument化を検討。再起動を伴うため、瞬断が許容範囲かを確認しながら進める
  3. Phase3:ECSクラスター登録のDocument化を検討し、全体の移行を完了させる

まとめ

  • UserDataは「起動時に一度だけ実行される」仕組みです。繰り返し変更したい設定までそこに詰め込むと、変更のたびにインスタンス再作成か手動オペレーションかの二択になり、運用が破綻します
  • 「定石」とされる方式が、必ずしも自分たちの状況にベストとは限りません。すでにSSMを使っている環境なら、cfn-init+cfn-hupよりもState Managerのほうが理にかなっているケースがあります
  • DocumentとAssociationのParametersを連動させておくと、「中身は変わったのに更新イベントが発火しない」という事故を構造的に防げます
  • マネージドポリシーに頼りきらず、実装した機能に本当に必要な権限は個別に確認したほうがいい、という教訓も得ました

「みんなが使っているから」で定石を選ぶ前に、自分たちの環境ですでに何を使っているかを整理してみると、意外と別の選択肢のほうが合っていることもある。そんな話でした。

0
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
0
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?