CloudFormation テンプレートとは、作りたい AWS リソースを YAML(または JSON)で宣言したファイルです。 必須のセクションは Resources だけで、そこに Parameters(入力値)、Mappings(対応表)、Conditions(条件)、Outputs(出力値)を足していくと、1つのテンプレートを開発用・本番用の両方に使い回せます。

この記事では、5つのセクションを全部使った完成形の YAML を1本載せ、スタックを作って describe-stacks の Outputs が返るところまでを出力つきで示します。後半は、!Ref や !Sub などの組み込み関数を「短縮形・完全形・返る値」の一覧にし、リソース別に Ref と GetAtt が何を返すかの早見表、テンプレート検査で出るエラー文と直し方を並べます。

確認環境:Ubuntu 24.04 / AWS CLI 1.46.1 / cfn-lint 1.57.1。AWS の API をローカルで再現するツール(moto 5.2.3)で実行しました。掲載するコマンドは実際の AWS でそのまま使える形にしてあります。

CloudFormation テンプレートの早見表

セクション 役割 必須 参照のしかた
AWSTemplateFormatVersion テンプレート形式の版('2010-09-09') 任意 −
Description テンプレートの説明文 任意 −
Parameters スタック作成時に渡す入力値 任意 !Ref パラメータ名
Mappings キーから値を引く対応表 任意 !FindInMap [表, キー1, キー2]
Conditions リソースを作るか、値をどちらにするかの条件 任意 Condition: 条件名、!If [条件名, 真, 偽]
Resources 作成するリソース 必須 !Ref 論理ID、!GetAtt 論理ID.属性
Outputs スタックが返す値(他スタックへの公開も可) 任意 describe-stacks、!ImportValue 公開名
よく使う関数 短縮形の例 返る値の例
Ref !Ref EnvName dev
Sub !Sub 'app-${EnvName}-${AWS::AccountId}' app-dev-123456789012
GetAtt !GetAtt JobQueue.Arn arn:aws:sqs:ap-northeast-1:123456789012:taskboard-dev-jobs
Join !Join ['-', [app, !Ref EnvName, queue]] app-dev-queue
If !If [IsProd, 'production', 'not production'] not production
FindInMap !FindInMap [EnvMap, !Ref EnvName, Size] small

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CloudFormationテンプレートの書き方と組み込み関数一覧」

テンプレートのセクション構成

公式ドキュメント(CloudFormation ユーザーガイドのテンプレートの構造のページ)に載っているセクションは次の10個で、必須は Resources だけです。

セクション 内容
Format version テンプレート形式の版
Description 説明文
Metadata テンプレートについての追加情報
Parameters スタックの作成・更新時に渡す値
Mappings 条件に応じた値を引く対応表
Conditions リソースを作るか、プロパティに値を入れるかの条件
Transform テンプレート処理時に適用するマクロ
Resources スタックのリソースとそのプロパティ(必須)
Outputs スタックのプロパティとして返す値
Rules 作成・更新時のパラメータの検証

この記事では、利用頻度の高い Parameters・Mappings・Conditions・Resources・Outputs の5つを扱います。

5つのセクションを全部使った完成 YAML

タスク管理アプリ用の保存先(S3 バケット、SQS キュー、SNS トピック)を作るテンプレートです。EnvName を prod にすると、デッドレターキューが追加され、タイムアウトなどの値が切り替わります。template.yaml として保存してください。

AWSTemplateFormatVersion: '2010-09-09'
Description: Task board storage (S3 bucket, SQS queue, SNS topic)

Parameters:
  AppName:
    Type: String
    Default: taskboard
    AllowedPattern: '^[a-z][a-z0-9-]{2,20}$'
    Description: Lowercase application name used as a name prefix
  EnvName:
    Type: String
    Default: dev
    AllowedValues: [dev, stg, prod]
    Description: Environment name
  AlertEmail:
    Type: String
    Default: ''
    Description: Email address for alerts (empty = no subscription)

Mappings:
  EnvMap:
    dev:
      VisibilityTimeout: 30
      RetentionSeconds: 86400
    stg:
      VisibilityTimeout: 60
      RetentionSeconds: 345600
    prod:
      VisibilityTimeout: 120
      RetentionSeconds: 1209600

Conditions:
  IsProd: !Equals [!Ref EnvName, prod]
  HasAlertEmail: !Not [!Equals [!Ref AlertEmail, '']]

Resources:
  DataBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AppName}-${EnvName}-${AWS::AccountId}'
      VersioningConfiguration:
        Status: !If [IsProd, Enabled, Suspended]
      Tags:
        - Key: Env
          Value: !Ref EnvName

  DeadLetterQueue:
    Type: AWS::SQS::Queue
    Condition: IsProd
    Properties:
      QueueName: !Sub '${AppName}-${EnvName}-jobs-dlq'

  JobQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Join ['-', [!Ref AppName, !Ref EnvName, jobs]]
      VisibilityTimeout: !FindInMap [EnvMap, !Ref EnvName, VisibilityTimeout]
      MessageRetentionPeriod: !FindInMap [EnvMap, !Ref EnvName, RetentionSeconds]
      RedrivePolicy: !If
        - IsProd
        - deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn
          maxReceiveCount: 5
        - !Ref AWS::NoValue

  AlertTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub '${AppName}-${EnvName}-alerts'

  AlertSubscription:
    Type: AWS::SNS::Subscription
    Condition: HasAlertEmail
    Properties:
      TopicArn: !Ref AlertTopic
      Protocol: email
      Endpoint: !Ref AlertEmail

Outputs:
  BucketName:
    Description: Bucket name (Ref)
    Value: !Ref DataBucket
  BucketArn:
    Description: Bucket ARN (GetAtt)
    Value: !GetAtt DataBucket.Arn
  JobQueueUrl:
    Description: Queue URL (Ref)
    Value: !Ref JobQueue
  JobQueueArn:
    Description: Queue ARN (GetAtt), exported for other stacks
    Value: !GetAtt JobQueue.Arn
    Export:
      Name: !Sub '${AWS::StackName}-JobQueueArn'
  AlertTopicArn:
    Description: Topic ARN (Ref)
    Value: !Ref AlertTopic
  DeadLetterQueueArn:
    Condition: IsProd
    Description: Dead letter queue ARN (prod only)
    Value: !GetAtt DeadLetterQueue.Arn

各セクションの読み方

セクション このテンプレートでの使い方
Parameters AppName・EnvName・AlertEmail の3つ。Default があるので指定を省ける。AllowedValues と AllowedPattern で受け付ける値を絞る
Mappings EnvMap に環境ごとの VisibilityTimeout と RetentionSeconds を置き、!FindInMap で引く
Conditions IsProd(本番かどうか)と HasAlertEmail(メールアドレスが空でないか)
Resources DeadLetterQueue と AlertSubscription は Condition: 付きで、条件が真のときだけ作られる
Outputs Ref と GetAtt の結果を返す。JobQueueArn は Export で他スタックに公開。DeadLetterQueueArn は本番のときだけ出力

ポイントは !Ref AWS::NoValue です。 !If の「偽」の側に置くと、そのプロパティ自体が無かったことになります。上の RedrivePolicy は、本番のときだけ設定されます。

スタックを作って Outputs を確かめる

開発用(パラメータは既定値のまま)

aws cloudformation create-stack --stack-name taskboard-dev --template-body file://template.yaml
aws cloudformation wait stack-create-complete --stack-name taskboard-dev
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/taskboard-dev/84f4cfe5-4bee-4705-8f5e-2b035cbf94bd"
}

wait stack-create-complete は作成完了まで待つコマンドで、完了すると何も出力せずに戻ります。Outputs を見てみます。

aws cloudformation describe-stacks --stack-name taskboard-dev --query 'Stacks[0].Outputs'
[
    {
        "OutputKey": "BucketName",
        "OutputValue": "taskboard-dev-123456789012",
        "Description": "Bucket name (Ref)"
    },
    {
        "OutputKey": "BucketArn",
        "OutputValue": "arn:aws:s3:::taskboard-dev-123456789012",
        "Description": "Bucket ARN (GetAtt)"
    },
    {
        "OutputKey": "JobQueueUrl",
        "OutputValue": "https://sqs.ap-northeast-1.amazonaws.com/123456789012/taskboard-dev-jobs",
        "Description": "Queue URL (Ref)"
    },
    {
        "OutputKey": "JobQueueArn",
        "OutputValue": "arn:aws:sqs:ap-northeast-1:123456789012:taskboard-dev-jobs",
        "Description": "Queue ARN (GetAtt), exported for other stacks",
        "ExportName": "taskboard-dev-JobQueueArn"
    },
    {
        "OutputKey": "AlertTopicArn",
        "OutputValue": "arn:aws:sns:ap-northeast-1:123456789012:taskboard-dev-alerts",
        "Description": "Topic ARN (Ref)"
    }
]

同じ SQS キューでも、!Ref は URL、!GetAtt ... .Arn は ARN を返していることがわかります。DeadLetterQueueArn は Condition: IsProd が偽なので、出力に含まれていません。

スタックに渡ったパラメータも確認できます。

aws cloudformation describe-stacks --stack-name taskboard-dev --query 'Stacks[0].[StackStatus,Parameters]'
[
    "CREATE_COMPLETE",
    [
        {
            "ParameterKey": "AppName",
            "ParameterValue": "taskboard"
        },
        {
            "ParameterKey": "EnvName",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "AlertEmail",
            "ParameterValue": ""
        }
    ]
]

本番用(パラメータを渡す)

aws cloudformation create-stack --stack-name taskboard-prod --template-body file://template.yaml \
  --parameters ParameterKey=EnvName,ParameterValue=prod ParameterKey=AlertEmail,ParameterValue=ops@example.com
aws cloudformation describe-stacks --stack-name taskboard-prod \
  --query 'Stacks[0].Outputs[].[OutputKey,OutputValue]' --output table
-----------------------------------------------------------------------------------------------------
|                                          DescribeStacks                                           |
+--------------------+------------------------------------------------------------------------------+
|  BucketName        |  taskboard-prod-123456789012                                                 |
|  BucketArn         |  arn:aws:s3:::taskboard-prod-123456789012                                    |
|  JobQueueUrl       |  https://sqs.ap-northeast-1.amazonaws.com/123456789012/taskboard-prod-jobs   |
|  JobQueueArn       |  arn:aws:sqs:ap-northeast-1:123456789012:taskboard-prod-jobs                 |
|  AlertTopicArn     |  arn:aws:sns:ap-northeast-1:123456789012:taskboard-prod-alerts               |
|  DeadLetterQueueArn|  arn:aws:sqs:ap-northeast-1:123456789012:taskboard-prod-jobs-dlq             |
+--------------------+------------------------------------------------------------------------------+

本番では DeadLetterQueueArn が増えました。Mappings と !If が効いているかは、作られたキューの属性で確かめられます。

aws sqs get-queue-attributes \
  --queue-url "$(aws sqs get-queue-url --queue-name taskboard-prod-jobs --output text)" \
  --attribute-names VisibilityTimeout MessageRetentionPeriod RedrivePolicy
{
    "Attributes": {
        "VisibilityTimeout": "120",
        "MessageRetentionPeriod": "1209600",
        "RedrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:ap-northeast-1:123456789012:taskboard-prod-jobs-dlq\", \"maxReceiveCount\": 5}"
    }
}

同じコマンドを taskboard-dev-jobs に対して実行した結果です。RedrivePolicy が無く、値は dev の行のものになっています。

{
    "Attributes": {
        "VisibilityTimeout": "30",
        "MessageRetentionPeriod": "86400"
    }
}
項目 dev prod 効いている仕組み
VisibilityTimeout 30 120 !FindInMap [EnvMap, !Ref EnvName, VisibilityTimeout]
MessageRetentionPeriod 86400 1209600 !FindInMap
RedrivePolicy なし あり !If [IsProd, {...}, !Ref AWS::NoValue]
DeadLetterQueue 作られない 作られる Condition: IsProd
Outputs の DeadLetterQueueArn 出ない 出る Outputs 側の Condition: IsProd

Export した値の一覧

aws cloudformation list-exports --query 'Exports[].[Name,Value]' --output text
taskboard-dev-JobQueueArn   arn:aws:sqs:ap-northeast-1:123456789012:taskboard-dev-jobs
taskboard-prod-JobQueueArn  arn:aws:sqs:ap-northeast-1:123456789012:taskboard-prod-jobs

公開名は !Sub '${AWS::StackName}-JobQueueArn' で作っているので、スタック名が頭に付きます。別のスタックからは !ImportValue taskboard-dev-JobQueueArn で受け取れます(次の章で実行します)。

作成と更新を1つのコマンドで行う deploy

create-stack は新規作成専用です。AWS CLI のリファレンスによると、aws cloudformation deploy は変更セットを作って実行するコマンドで、スタック名が既存なら更新、新しければ作成になります。

aws cloudformation deploy --template-file template.yaml --stack-name taskboard-prod \
  --parameter-overrides EnvName=prod AlertEmail=ops@example.com
オプション リファレンスの説明(要約)
--parameter-overrides キー=値 を空白区切りで並べる。更新時に省いたパラメータは、スタックの既存の値が使われる
--capabilities IAM リソースを含むテンプレートで必要。値は CAPABILITY_IAM か CAPABILITY_NAMED_IAM(名前を指定した IAM リソースがある場合)
--no-execute-changeset 変更セットを作るだけで実行しない。変更内容を見てから実行したいときに使う

スタックを削除する

aws cloudformation delete-stack --stack-name taskboard-dev
aws cloudformation describe-stacks --stack-name taskboard-dev --query 'Stacks[0].StackStatus'
An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id taskboard-dev does not exist

削除が終わると、スタック名での describe-stacks はこのメッセージになります。

組み込み関数一覧(短縮形・完全形・返る値)

「返る値」は、次のテンプレートでスタックを作り、Outputs に出た値をそのまま写したものです(EnvName は既定の dev、ImportValue は先ほどの taskboard-dev スタックの公開値)。

AWSTemplateFormatVersion: '2010-09-09'
Description: Intrinsic function samples (values appear in Outputs)

Parameters:
  EnvName:
    Type: String
    Default: dev

Mappings:
  EnvMap:
    dev:
      Size: small
    prod:
      Size: large

Conditions:
  IsProd: !Equals [!Ref EnvName, prod]

Resources:
  SampleQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub 'sample-${EnvName}'

Outputs:
  RefParameter:
    Value: !Ref EnvName
  RefResource:
    Value: !Ref SampleQueue
  RefPseudo:
    Value: !Ref AWS::Region
  SubSimple:
    Value: !Sub 'app-${EnvName}-${AWS::AccountId}'
  SubWithMap:
    Value: !Sub
      - '${Prefix}/${EnvName}/logs'
      - Prefix: archive
  SubGetAtt:
    Value: !Sub 'queue=${SampleQueue.QueueName}'
  GetAttArn:
    Value: !GetAtt SampleQueue.Arn
  JoinList:
    Value: !Join ['-', [app, !Ref EnvName, queue]]
  SplitThenJoin:
    Value: !Join ['|', !Split [',', 'web,api,batch']]
  SelectSecond:
    Value: !Select [1, [web, api, batch]]
  SplitThenSelect:
    Value: !Select [2, !Split [':', 'arn:aws:sqs:ap-northeast-1:123456789012:jobs']]
  IfValue:
    Value: !If [IsProd, 'production', 'not production']
  FindInMapValue:
    Value: !FindInMap [EnvMap, !Ref EnvName, Size]
  ImportValueArn:
    Value: !ImportValue taskboard-dev-JobQueueArn
aws cloudformation create-stack --stack-name fn-demo --template-body file://functions.yaml
aws cloudformation describe-stacks --stack-name fn-demo \
  --query 'Stacks[0].Outputs[].[OutputKey,OutputValue]' --output text
RefParameter    dev
RefResource https://sqs.ap-northeast-1.amazonaws.com/123456789012/sample-dev
RefPseudo   ap-northeast-1
SubSimple   app-dev-123456789012
SubWithMap  archive/dev/logs
SubGetAtt   queue=sample-dev
GetAttArn   arn:aws:sqs:ap-northeast-1:123456789012:sample-dev
JoinList    app-dev-queue
SplitThenJoin   web|api|batch
SelectSecond    api
SplitThenSelect sqs
IfValue not production
FindInMapValue  small
ImportValueArn  arn:aws:sqs:ap-northeast-1:123456789012:taskboard-dev-jobs

EnvName=prod で作ると、IfValue は production、FindInMapValue は large になりました。

一覧表

関数 短縮形(YAML) 完全形(YAML) 返る値の例
Ref(パラメータ) !Ref EnvName Ref: EnvName dev
Ref(リソース) !Ref SampleQueue Ref: SampleQueue https://sqs.ap-northeast-1.amazonaws.com/123456789012/sample-dev
Ref(擬似パラメータ) !Ref AWS::Region Ref: AWS::Region ap-northeast-1
Sub !Sub 'app-${EnvName}-${AWS::AccountId}' Fn::Sub: 'app-${EnvName}-${AWS::AccountId}' app-dev-123456789012
Sub(変数の対応表つき) !Sub ['${Prefix}/${EnvName}/logs', {Prefix: archive}] Fn::Sub: [...] archive/dev/logs
Sub(属性の埋め込み) !Sub 'queue=${SampleQueue.QueueName}' Fn::Sub: 'queue=${SampleQueue.QueueName}' queue=sample-dev
GetAtt !GetAtt SampleQueue.Arn Fn::GetAtt: [SampleQueue, Arn] arn:aws:sqs:ap-northeast-1:123456789012:sample-dev
Join !Join ['-', [app, !Ref EnvName, queue]] Fn::Join: ['-', [app, {Ref: EnvName}, queue]] app-dev-queue
Select !Select [1, [web, api, batch]] Fn::Select: [1, [web, api, batch]] api(番号は0始まり)
Split !Split [',', 'web,api,batch'] Fn::Split: [',', 'web,api,batch'] リスト [web, api, batch](Join すると web|api|batch)
Split + Select !Select [2, !Split [':', 'arn:aws:sqs:...']] Fn::Select: [2, {'Fn::Split': [':', ...]}] sqs
Equals !Equals [!Ref EnvName, prod] Fn::Equals: [{Ref: EnvName}, prod] 真偽(Conditions で使う)
If !If [IsProd, 'production', 'not production'] Fn::If: [IsProd, 'production', 'not production'] not production(dev)/production(prod)
FindInMap !FindInMap [EnvMap, !Ref EnvName, Size] Fn::FindInMap: [EnvMap, {Ref: EnvName}, Size] small(dev)/large(prod)
ImportValue !ImportValue taskboard-dev-JobQueueArn Fn::ImportValue: taskboard-dev-JobQueueArn arn:aws:sqs:ap-northeast-1:123456789012:taskboard-dev-jobs
GetAZs !GetAZs '' Fn::GetAZs: '' アベイラビリティーゾーン名のリスト

完全形でも同じ値になることは、Ref:・Fn::GetAtt:・Fn::Sub:・Fn::Join:・Fn::Select:・Fn::Split:・Fn::ImportValue: で書いたテンプレートを別に作り、Outputs(a-b、x、a+b+c、region=ap-northeast-1 など)で確かめました。

GetAZs は公式リファレンスの記述にもとづきます。指定したリージョンのアベイラビリティーゾーンをアルファベット順のリストで返し、空文字列 '' は AWS::Region(スタックを作るリージョン)と同じ意味です。リファレンスには us-east-1 での戻り値の例として [ "us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d", "us-east-1e" ] が載っています。サブネットの配置先を決めるときは、!Select [0, !GetAZs ''] のように Select と組み合わせます。

書き方のコツ

場面 向いている関数 理由
値をそのまま渡す !Ref もっとも短い
文字列の中に値を埋め込む !Sub ${名前} で読みやすい。${論理ID.属性} で GetAtt 相当も書ける
リストを区切り文字でつなぐ !Join リストをそのまま渡せる
環境ごとに値を変える !FindInMap 値を Mappings に集められる
作る・作らないを切り替える Condition: リソース単位で切り替わる
プロパティだけ切り替える !If と !Ref AWS::NoValue 偽のときプロパティごと省ける

公式リファレンスの組み込み関数の一覧には、このほか Fn::Base64、Fn::Cidr、Fn::ForEach、Fn::GetStackOutput、Fn::Length、Fn::ToJsonString、Fn::Transform、条件関数、ルール関数が載っています。組み込み関数を使える場所は、リソースのプロパティ、Outputs、メタデータ属性、更新ポリシー属性、そしてリソースを条件つきで作る箇所です。

擬似パラメータ

宣言なしで !Ref や !Sub から使える値です。今回の出力で確かめたものを載せます。

擬似パラメータ 返った値 使った場所
AWS::Region ap-northeast-1 !Ref AWS::Region
AWS::AccountId 123456789012 !Sub '...${AWS::AccountId}'
AWS::StackName taskboard-dev Export 名 taskboard-dev-JobQueueArn
AWS::NoValue (プロパティを省く) !If の偽の側

リソース別:Ref と GetAtt が返す値の早見表

Ref が返すものはリソースの種類によって違います。 名前のもの、URL のもの、ARN のものがあるので、迷ったらこの表を見てください。値は、各リソースを1つずつ作ったスタックの Outputs に出たものです。

リソースタイプ !Ref が返す値 例 よく使う !GetAtt 例
AWS::S3::Bucket バケット名 refdemo-bucket-123456789012 .Arn arn:aws:s3:::refdemo-bucket-123456789012
同上 − − .DomainName refdemo-bucket-123456789012.s3.amazonaws.com
同上 − − .RegionalDomainName refdemo-bucket-123456789012.s3.ap-northeast-1.amazonaws.com
AWS::SQS::Queue キューの URL https://sqs.ap-northeast-1.amazonaws.com/123456789012/refdemo-queue .Arn arn:aws:sqs:ap-northeast-1:123456789012:refdemo-queue
同上 − − .QueueName refdemo-queue
AWS::SNS::Topic トピックの ARN arn:aws:sns:ap-northeast-1:123456789012:refdemo-topic .TopicName refdemo-topic
AWS::DynamoDB::Table テーブル名 refdemo-table .Arn arn:aws:dynamodb:ap-northeast-1:123456789012:table/refdemo-table
AWS::IAM::Role ロール名 refdemo-role .Arn arn:aws:iam::123456789012:role/refdemo-role
AWS::Lambda::Function 関数名 refdemo-function .Arn arn:aws:lambda:ap-northeast-1:123456789012:function:refdemo-function
AWS::Logs::LogGroup ロググループ名 /refdemo/app − −
AWS::EC2::VPC VPC の ID vpc-ca5544c6cfae5dba2 .CidrBlock 10.0.0.0/16
AWS::EC2::Subnet サブネットの ID subnet-18e8c87291542b6e9 − −
AWS::EC2::SecurityGroup セキュリティグループの ID sg-a049e94be491bf0fa .GroupId sg-a049e94be491bf0fa
ほしい値 書き方の目安
ARN がほしい SNS トピックは !Ref、それ以外の多くは !GetAtt 論理ID.Arn
名前がほしい S3・DynamoDB・IAM ロール・Lambda は !Ref、SQS は !GetAtt 論理ID.QueueName、SNS は !GetAtt 論理ID.TopicName
URL がほしい SQS は !Ref

IAM ロールの ARN を Lambda 関数の Role に渡すときは !GetAtt Role.Arn、SNS のサブスクリプションの TopicArn には !Ref Topic、というように、渡す先が求める形(名前か ARN か)に合わせて Ref と GetAtt を選びます。

テンプレート検査で出るエラー文と直し方

デプロイの前に、テンプレートを手元で検査しておくと修正が早く済みます。ここでは CloudFormation のテンプレート検査ツール cfn-lint(pip install cfn-lint)の出力を載せます。AWS 側で構文を確かめる aws cloudformation validate-template --template-body file://template.yaml と併用できます。

cfn-lint template.yaml

問題が無ければ何も出力せず、終了コード 0 で戻ります(上の完成 YAML はこの状態です)。

未定義のパラメータを参照している

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  JobQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub '${EnvName}-jobs'
      Tags:
        - Key: App
          Value: !Ref AppName
E1019 'EnvName' is not one of ['JobQueue', 'AWS::AccountId', 'AWS::NoValue', 'AWS::NotificationARNs', 'AWS::Partition', 'AWS::Region', 'AWS::StackId', 'AWS::StackName', 'AWS::URLSuffix']
undefined-ref.yaml:6:7

E1020 'AppName' is not one of ['JobQueue', 'AWS::AccountId', 'AWS::NoValue', 'AWS::NotificationARNs', 'AWS::Partition', 'AWS::Region', 'AWS::StackId', 'AWS::StackName', 'AWS::URLSuffix']
undefined-ref.yaml:9:11
…(省略)…

is not one of [...] の中身が、その場所で参照できる名前の一覧(リソースの論理 ID と擬似パラメータ)です。EnvName と AppName が入っていないので、Parameters に足します。

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  EnvName:
    Type: String
    Default: dev
  AppName:
    Type: String
    Default: taskboard
Resources:
  JobQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub '${EnvName}-jobs'
      Tags:
        - Key: App
          Value: !Ref AppName

直したあとの cfn-lint は出力なし(終了コード 0)でした。

循環参照になっている

2つのリソースが互いの属性を参照すると、どちらを先に作ればよいか決まりません。

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  QueueA:
    Type: AWS::SQS::Queue
    Properties:
      Tags:
        - Key: Pair
          Value: !GetAtt QueueB.QueueName
  QueueB:
    Type: AWS::SQS::Queue
    Properties:
      Tags:
        - Key: Pair
          Value: !GetAtt QueueA.QueueName
E3004 Circular Dependencies for resource QueueA. Circular dependency with [QueueB]
circular.yaml:3:3

E3004 Circular Dependencies for resource QueueB. Circular dependency with [QueueA]
circular.yaml:9:3

直し方は、相手の「属性」を参照するのをやめ、パラメータから同じ文字列を組み立てることです。 名前を自分で決めておけば、相手のリソースを待つ必要がなくなります。

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  EnvName:
    Type: String
    Default: dev
Resources:
  QueueA:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub '${EnvName}-queue-a'
      Tags:
        - Key: Pair
          Value: !Sub '${EnvName}-queue-b'
  QueueB:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub '${EnvName}-queue-b'
      Tags:
        - Key: Pair
          Value: !Sub '${EnvName}-queue-a'

こちらも直したあとは出力なし(終了コード 0)です。

そのほかのエラー文と直し方

それぞれ最小のテンプレートで再現した出力です。

原因 cfn-lint の出力 直し方
Resources セクションが無い E1001 'Resources' is a required property リソースを1つ以上書く
インデントがずれている E0000 mapping values are not allowed in this context(indent.yaml:7:19) 表示された行・桁の字下げを揃える
定義していない条件名を使った E3015 'IsProd' is not one of [] Conditions に IsProd を定義する
無い属性を GetAtt した E6101 'Url' is not one of ['Arn', …, 'QueueName', 'QueueUrl', …] 一覧にある属性名(QueueUrl など)に直す
リソースタイプの綴り E3006 Resource type 'AWS::SQS::Queu' does not exist in 'us-east-1' AWS::SQS::Queue に直す
Mappings に無いキーを引いた E1011 'prod' is not one of ['dev'] for mapping 'EnvMap' Mappings に prod の行を足す
プロパティ名の綴り E3002 Additional properties are not allowed ('QueueNames' was unexpected. Did you mean 'QueueName'?) 提案された名前に直す
値の型が違う E3012 'thirty' is not of type 'integer' 数値で書く
使っていないパラメータ(警告) W2001 Parameter EnvName not used. 使うか、消す
変数の無い Sub(警告) W1020 'Fn::Sub' isn't needed because there are no variables !Sub を外す

出力の2行目(ファイル名:行:桁)が該当箇所です。E で始まるものはエラー、W で始まるものは警告で、終了コードはエラーがあると 2、警告だけだと 4 でした。

よくある質問

Q. テンプレートに最低限必要なものは何ですか? A. Resources セクションと、その中のリソース1つ以上です。AWSTemplateFormatVersion や Description は任意ですが、書いておくと読み手に親切です。

Q. !Ref と !GetAtt はどう使い分けますか? A. !Ref はリソースの「代表的な識別子」(S3 はバケット名、SQS は URL、SNS は ARN)を返し、!GetAtt は属性を名前で指定して取り出します。ARN がほしい場面の多くは !GetAtt 論理ID.Arn です。

Q. !Sub と !Join はどちらを使えばよいですか? A. 文字列に値を埋め込むなら !Sub が読みやすく、リストを区切り文字でつなぐなら !Join が向いています。同じ app-dev-queue はどちらでも作れます。

Q. 開発用と本番用でテンプレートを分ける必要はありますか? A. 1本で済ませられます。違いを Parameters で受け取り、値の違いは Mappings と !FindInMap、作る・作らないの違いは Conditions で表します。この記事の完成 YAML がその形です。

まとめ

  • テンプレートで必須なのは Resources だけ。Parameters・Mappings・Conditions・Outputs を足すと使い回せる
  • !Ref が返す値はリソースごとに違う(S3 は名前、SQS は URL、SNS は ARN)。ARN は多くの場合 !GetAtt 論理ID.Arn
  • 文字列の組み立ては !Sub、環境ごとの値は !FindInMap、切り替えは Condition: と !If
  • プロパティごと省きたいときは !Ref AWS::NoValue
  • スタック間の受け渡しは Outputs の Export と !ImportValue
  • デプロイ後は describe-stacks --query 'Stacks[0].Outputs' で値を確かめる
  • デプロイ前に cfn-lint で検査すると、未定義の参照や循環参照を行番号つきで見つけられる

参考・出典

  • AWS CloudFormation User Guide「CloudFormation template sections」: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html
  • AWS CloudFormation Template Reference「Intrinsic function reference」: https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/intrinsic-function-reference.html
  • AWS CloudFormation Template Reference「Fn::GetAZs」: https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/intrinsic-function-reference-getavailabilityzones.html
  • AWS CLI Command Reference「cloudformation deploy」: https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deploy.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CloudFormationテンプレートの書き方と組み込み関数一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?