CloudFormationでEC2を作成するとき、スタック作成時に入力したパラメータをUserDataのシェルスクリプトへ渡せます。基本形はFn::Base64とFn::Subの組み合わせです。AWS Service CatalogのCloudFormation製品でも、同じテンプレート記法を利用できます。
この記事の要点
Fn::SubでCloudFormationパラメータをUserData内へ展開できる- UserDataは
Fn::Base64でBase64エンコードする - Service Catalog製品でもCloudFormationテンプレートの仕様は同じ
- UserDataの上限はBase64エンコード前で16KB
- パスワードやAPIキーをUserDataへ埋め込まない
- 秘密値はEC2のIAMロールを使って起動後に取得する
Fn::Subでパラメータを渡す基本形
次の例では、スタック作成時に指定した環境名とアプリケーション名をUserDataへ渡します。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
ApplicationEnvironment:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
ApplicationName:
Type: String
Default: sample-app
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Resources:
WebServer:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref LatestAmiId
InstanceType: t3.micro
UserData:
Fn::Base64:
!Sub |
#!/bin/bash
mkdir -p /etc/sample-app
cat > /etc/sample-app/app.conf <<'EOF'
APP_NAME=${ApplicationName}
APP_ENV=${ApplicationEnvironment}
STACK_NAME=${AWS::StackName}
AWS_REGION=${AWS::Region}
EOF
Fn::Subは、${ApplicationName}のようなテンプレートパラメータを実際の値へ置換します。${AWS::StackName}や${AWS::Region}などの擬似パラメータも利用できます。
Fn::Subで参照できる値
| 書き方 | 取得する値 |
|---|---|
${ParameterName} | Parametersセクションの値 |
${LogicalId} | リソースに対するRefの戻り値 |
${LogicalId.Attribute} | Fn::GetAttで取得できる属性 |
${AWS::Region} | 擬似パラメータ |
| 変数マップ | 別の組み込み関数で求めた値 |
シェル側で${VAR}をそのまま使いたい場合はCloudFormationの置換と衝突します。文字列として残すには${!VAR}と記述します。起動テンプレート内では、さらにバックスラッシュを付けます。
変数マップを使う書き方
UserData:
Fn::Base64:
Fn::Sub:
- |
#!/bin/bash
echo "BUCKET_NAME=${BucketName}" > /etc/app.env
echo "ENVIRONMENT=${EnvironmentName}" >> /etc/app.env
- BucketName: !Ref AppBucket
EnvironmentName: !Ref ApplicationEnvironment
参照する値に別名を付けたい場合や、Fn::FindInMapなどの結果を埋め込みたい場合は、Fn::Subの変数マップを使います。
Service Catalogでも同じ記法を使える
AWS Service CatalogのCloudFormation製品は、プロビジョニングアーティファクトとしてCloudFormationテンプレートを使用します。製品起動時の入力値はテンプレートのParametersへ渡されるため、UserData内でのFn::SubやRefの使い方も通常のCloudFormationスタックと同じです。
テンプレート制約や起動制約を設定している場合、利用者が選択できる値や、リソースを作成するIAMロールは制約の影響を受けます。EC2が起動後にAWS APIへアクセスする権限は、EC2へ付与したインスタンスプロファイルで決まります。
秘密情報をUserDataへ直接渡さない
UserDataにパスワード、APIキー、データベース認証情報を埋め込む設計は避けます。CloudFormationパラメータへNoEcho: trueを指定しても、値をUserDataへ展開すればEC2側へ平文で渡されます。NoEchoは画面表示をマスクする機能であり、秘密値を安全に保管する仕組みではありません。
CloudFormationの公式ドキュメントでは、Secrets ManagerやSSM Parameter Storeの安全な値への動的参照は、EC2のUserDataプロパティではサポートされていません。テンプレートへ動的参照を書けば安全にUserDataへ展開できる、という前提では設計できません。
安全な取得フロー
- 秘密情報をSecrets ManagerまたはParameter StoreのSecureStringへ保存する
- EC2へIAMロールとインスタンスプロファイルを付与する
- 対象の秘密情報だけを取得できる最小権限ポリシーを設定する
- UserDataには秘密値ではなく、シークレット名やパラメータ名だけを渡す
- 起動後のスクリプトやアプリケーションからAWS CLIまたはSDKで値を取得する
- 取得した値をログへ出力しない
secret_json=$(aws secretsmanager get-secret-value
--secret-id "$SECRET_ID"
--query SecretString
--output text
--region "$AWS_REGION")
# secret_jsonをログへ出力しない
# 必要な設定処理へ安全に渡す
UserData利用時の注意点
- サイズ:Base64エンコード前で16KBが上限
- 実行タイミング:一般的なLinuxのcloud-initでは初回起動時に実行され、再起動のたびに実行されるとは限らない
- 更新影響:
AWS::EC2::InstanceのUserData更新は、ルートがEBSの場合に再起動を伴う - 失敗検知:
cfn-signalとCreationPolicyを使い、初期化失敗をスタックへ返す - ログ:
/var/log/cloud-init-output.logなどを確認し、秘密値を出力しない - 長い処理:大きなスクリプトはS3や構成管理へ分離し、UserDataは取得と起動に絞る
よくあるエラー
| 症状 | 主な原因と対処 |
|---|---|
| テンプレートの構文エラー | !Base64 !Subを同じ行に連結せず、どちらかを長形式にする |
| シェル変数が空になる | ${VAR}をCloudFormationが置換している。${!VAR}を使う |
| UserDataが実行されない | shebang、改行、cloud-initの状態とログを確認する |
| Service Catalog経由でAWS API呼び出しに失敗 | 製品の起動ロールとEC2インスタンスロールを分けて確認する |
| 秘密値を取得できない | IAM、KMS権限、VPC内の通信経路を確認する |
まとめ
CloudFormationの入力パラメータは、Fn::Base64とFn::Subを使ってEC2 UserDataへ渡せます。この方法はAWS Service CatalogのCloudFormation製品でも共通です。秘密値はUserDataへ直接展開せず、EC2のIAMロールを使って起動後にSecrets ManagerやParameter Storeから取得してください。短い初期化はUserData、複雑な構成管理はcfn-initなどへ分けると、失敗原因を追いやすくなります。
