CloudFormationの超詳細解説 2/4 テンプレート編

# はじめに
この記事はDevOps on AWS大全の一部です。
DevOps on AWS大全の一覧は[こちら](https://qiita.com/tech4anyone/items/b06f88035d27c6ef13b2)。

この記事ではCloudFomationのテンプレートに記載する内容を超詳細にまとめています。

具体的には以下流れで説明します。
- Resources
- Paramaters
- Mappings
- Outputs
- Conditions
- 組み込み関数
- References

AWSの区分でいう「Level 200：トピックの入門知識を持っていることを前提に、ベストプラクティス、サービス機能を解説するレベル」の内容です。

# この記事を読んでほしい人
- CloudFormationがどういうサービスか説明できるようになりたい人
- CloudFormationを採用するときのベストプラクティスを説明できるようになりたい人
- AWS Certified DevOps Engineer Professionalを目指している人

# Resources
## Resourcesとは
Resourcesはどういったコンポーネントを作るか記載している部分になります。
なお、CloudFormationテンプレートの中では必須の記載事項です。

例えば、EC2インスタンスやSecurityGroup、Subnetなど1つずつコンポーネントをResources部分に記載していきます。

```yaml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: MyVPC
	  
  MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: us-east-1a
      Tags:
        - Key: Name
          Value: MySubnet
```

なお、ResourcesにかけないサービスはCloudFormationで管理することはできません。
AWSはあまりにサービスが多すぎるため、すべてを紹介することはできませんがプロジェクトでよく使うであろうサービスは基本的に網羅されています。

詳細は[AWS公式](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)をご覧ください。
実プロジェクトでCloudFormationを使う際には、アーキテクチャが決まった段階でAWS公式のResources一覧を確認し、CloudFormationにできるものは例外なくCloudFormationテンプレートにおこすことがベストプラクティスです。

中途半端にCloudFormationの管理外リソースを作ると、別の環境を作るときに作業漏れにつながるため、CloudFormationにできるものはすべてCloudFormationにするという心意気で頑張りましょう。

## CustomResource
公式に提供されていないリソースを作りたい場合や、特定の処理をCloudFormationで実行したい場合にはCustomResourceが利用できます。

CustomResourceに記載した処理はSNSまたはLambdaを処理のバックエンドとして利用できます。
よくあるのは、CloudFormation Stackを削除するときにS3バケットの中身を空にするCustomResourceを定義するシナリオです。

試験でもよく出ますがS3バケットはオブジェクトがある状態では削除できません。
そのため、事前にS3バケットを空にしておくかCustomResourceを利用してS3バケットを空にする処理を組みます。

![](https://storage.googleapis.com/zenn-user-upload/2f89e734970e-20231112.png)

## CloudFormationヘルパースクリプト
EC2インスタンスを構築する場合、インスタンスの起動処理の中で初期設定をするという場面は多くあります。
そういった場面で使えるのがCloudFormationヘルパースクリプトです。

似たようなことを実現できる方法としてEC2 User Dataがあります。
一見するとCloudFormationヘルパースクリプトもEC2 User Dataもできることは変わりません。
しかし、CloudFormationヘルパースクリプトの場合、スクリプトが失敗するとCloudFormation自体も失敗するのに対して、User Dataは処理が失敗してもCloudFormationは成功してしまいます。

そのため、初期設定処理が失敗しているのにCloudFormationが正常であるためにサービスも正常だと誤認することを避けるためにCloudFormationを使ってリソース構築をする場合はCloudFormationヘルパースクリプトの利用が推奨です。

CloudFormationヘルパースクリプトの中には1)cfn-init、2)cfn-signal、3)WaitConditions、4)cfn-hupという4種類のスクリプトが存在し、それぞれ使いどころが異なります。

各スクリプトを説明する前に以下にCloudFormationヘルパースクリプトの全体像をまとめました。
![](https://storage.googleapis.com/zenn-user-upload/44a5c57430de-20231112.png)

上図を見ながら以下の説明を読んでください。

まずcfn-initについてです。
cfn-initはEC2起動時に実施する処理を記述します。
例えばyum installを実行して、特定のパッケージをインストールしたりディレクトリを作って権限設定したりといういわゆる起動処理でやる内容を記述できます。

次にcfn-signalとWaitConditionsです。
すでに述べた通りCloudFormationヘルパースクリプトはスクリプトの成否をCloudFormationの成否に影響させることができます。

その際に使うのがcfn-signalとWaitConditionsです。
cfn-signalでCloudFormationに送る終了ステータスを記述します。
WaitConditionsではCloudFormationが何回signalを受け取れば成功とするか、どれぐらいの時間待ってタイムアウトさせるかの2つを記述します。

最後にcfn-hupです。
cfn-hupを記述すると15分に一回、CloudFormationのメタデータを確認しに行くようになります。
確認した結果、メタデータに変更が加わっていれば指定した処理を実行します。

# Paramaters
## Paramatersとは
Resourceesで書いたCloudFormationテンプレートは値をべたで書きましたが、実プロジェクトではアンチパターンになります。

なぜなら、CloudFormationテンプレートを使いまわしたい、あるいは1つのテンプレートで複数の環境を管理したい、という場合に環境変数となる値CloudFormationテンプレート全体に散らばり管理しずらくなってしまうからです。

そこで活用したいのがParamatersです。
環境変数となる値をCloudFormationの冒頭にParametersとしてまとめて宣言することが可能になります。

```diff_yaml
AWSTemplateFormatVersion: '2010-09-09'

+ Parameters:
+  VpcCidrBlock:
+    Type: String
+    Default: 10.0.0.0/16
+    Description: CIDR block for the VPC
+
+  SubnetCidrBlock:
+    Type: String
+    Default: 10.0.0.0/24
+    Description: CIDR block for the Subnet

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
+      CidrBlock: !Ref VpcCidrBlock
-      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: MyVPC

  MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
+      CidrBlock: !Ref SubnetCidrBlock
-      CidrBlock: 10.0.0.0/24
      AvailabilityZone: us-east-1a
      Tags:
        - Key: Name
          Value: MySubnet
```

これを見るとべたで書かれていたVPCとSubnetのCIDRがParametersにまとまり、Resourcesの中にあるCidrBlock部分は!Refを使ってParametersの中から参照しているのがわかると思います。

こうすることでほかの人にテンプレートを渡しても、Parametersだけそれぞれのプロジェクトの要件に合わせて書き換えて、という使い方ができるようになります。

また、Parametersに書いた値はDefault指定をしておくと、CloudFormationを実行するときに書き換えることが可能です。
例えばマネジメントコンソールから実行する場合は、パラメータを入力する画面が出てくるのでそこで、値を書き換えれば上書きされます。

1つ、コツとして覚えてほしいのがAllowedValuesあるいはAllowedPatternというParametersです。

```yaml
Parameters:
  EnvironmentType:
    Type: String
    Default: Development
    AllowedValues: [Development, Production]
    Description: Specify the environment type (Development or Production)
```

こんな感じでAllowedValuesを配列で定義すると、EnvironmentTypeの値はDevelopmentかProductionしか取れなくなります。

上記ではあわせてDefaultも設定したので、指定しなければDevelopmentという値がEnvironmentTypeというパラメータに格納され、Productionに書き換えることも可能になる、という読み方ができます。

ちなみに、AllowedValuesは管理者が事前に許可するパラメータを決めたいときに有用です。

例えば、コスト管理をするためにEC2のインスタンスタイプは小さいものだけに制限したい、という場合によく使われます。
この点が試験でも聞かれるので覚えておきましょう。

## 疑似パラメータ
疑似パラメータはAWSですでに宣言されているパラメータで、Paramatersに宣言しなくても利用することができます。

|擬似パラメータ | 説明 | 出力例|
|--- | --- | ---|
|AWS::AccountId | AWSアカウントID | 123456789012|
|AWS::NotificationARNs | スタックの通知 Amazon リソースネーム | arn:aws:sns:ap-northeast-1:871728391824:topic1|
|AWS::NoValue | 組み込み関数 Fn::If で使用すると対象プロパティを削除 |  |
|AWS::Partition | パーティション | aws|
|AWS::Region | リージョン | ap-northeast-1|
|AWS::StackId | スタックのID | arn:aws:cloudformation:ap-northeast-1:123456789012:stack/test/12345678-8b4e-9abc-def0-123456789abc|
|AWS::StackName | スタック名 | test|
|AWS::URLSuffix | ドメインのサフィックス | amazonaws.com|

すべてを覚える必要はありませんが、試験でよく聞かれるのはAccountIdとRegionで疑似パラメータを使えることです。
この2つは実プロジェクトでもよく使います。

# Mappings
## Mappingsとは
Mappingsはある変数に対してあらかじめ取り得るすべての値が決められる場合に有用です。

先ほどは1つのVPCと1つのSubnetだけを定義していたのでParametersで十分でした。
しかし、実プロジェクトでは開発環境と本番環境を1つのCloudFormationテンプレートで管理したくなります。
でないと、開発環境のCloudFormationテンプレートを改修したのに、本番環境のCloudFormationテンプレートに反映し忘れる、というアンチパターンに見事にはまります。

そこで登場するのがMappingsです。
例えば、EnvironmentMapというMapNameを定義して、DevelopmentとProductionごとにVpcCidrBlockとSubnetCidrBlockを書き起こしてみましょう。

```yaml
Mappings:
  EnvironmentMap:
    Development:
      VpcCidrBlock: "10.0.0.0/16"
      SubnetCidrBlock: "10.0.0.0/24"
    Production:
      VpcCidrBlock: "172.16.0.0/16"
      SubnetCidrBlock: "172.16.0.0/24"
```

EnvironmentMapがMapName、DevelopmentとProductionがTopLevelKey、VpcCidrBlockとSubnetCidrBlockがSecondLevelKeyです。

Resourcesの中でMappingsの値を参照する際にはこのMapNameのこのTopLevelKeyの中のこのSecondLevelKeyを参照して、値を返してほしい、という書き方になります。

ここまでの話を踏まえて、CloudFomration実行時にDevelopmentをパラメータとして入力すれば10.x.x.xのCIDRで、Productionをパラメータとして入力すれば172.x.x.xのCIDRでVPCとSubnetを構築するテンプレートに書き換えたものが以下になります。

```diff_yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
-  VpcCidrBlock:
-    Type: String
-    Default: 10.0.0.0/16
-    Description: CIDR block for the VPC
-
-  SubnetCidrBlock:
-    Type: String
-    Default: 10.0.0.0/24
-    Description: CIDR block for the Subnet

+  EnvironmentType:
+    Type: String
+    Default: Development
+    AllowedValues: [Development, Production]
+    Description: Specify the environment type (Development or Production)

+Mappings:
+  EnvironmentMap:
+    Development:
+      VpcCidrBlock: "10.0.0.0/16"
+      SubnetCidrBlock: "10.0.0.0/24"
+    Production:
+      VpcCidrBlock: "172.16.0.0/16"
+      SubnetCidrBlock: "172.16.0.0/24"

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
+     CidrBlock: !FindInMap [EnvironmentMap, !Ref EnvironmentType, VpcCidrBlock]
-     CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-VPC"

  MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
+     CidrBlock: !FindInMap [EnvironmentMap, !Ref EnvironmentType, SubnetCidrBlock]
-     CidrBlock: !Ref SubnetCidrBlock
      AvailabilityZone: us-east-1a
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-Subnet"

```

 !FindInMap [EnvironmentMap, !Ref EnvironmentType, VpcCidrBlock]でCIDRを参照しています。
 
 具体的にはEnvironmentMapというMapNameのパラメータとして指定したEnvironmentTypeというTopLevelKeyのVpcCidrBlockというSecondLevelKeyの値を参照します。

なので、パラメータでDevelopmentを指定すれば10.0.0.0/16がProductionを指定すれば172.16.0.0/16が返ってきます。

# Outputs
OutputsはほかのCloudFormation Stackで利用したい値を出力するために使います。
例えば、CloudFormationでVPCを作成したときにVPC IDをOutputsで出力し、ほかのCloudFormation Stackでその値を利用したいときに使います。

ただし、ほかのCloudFormation Stackで利用する場合、つまりクロススタック参照をする場合にはOutputsの中にExportを記載する必要があります。
Exportを記載すると、記載したNameでほかのStackからそのNameを参照することができるようになります。

Exportはスタック間で一意にする必要があるので注意しましょう。

ここまでの議論を踏まえてOutputsを追加して、ほかのCloudFormation StackでVPC IDとSubnet IDを参照できるようにCloudFomrtionテンプレートを書き換えたのが以下になります。
```diff_yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  EnvironmentType:
    Type: String
    Default: Development
    AllowedValues: [Development, Production]
    Description: Specify the environment type (Development or Production)

Mappings:
  EnvironmentMap:
    Development:
      VpcCidrBlock: "10.0.0.0/16"
      SubnetCidrBlock: "10.0.0.0/24"
    Production:
      VpcCidrBlock: "172.16.0.0/16"
      SubnetCidrBlock: "172.16.0.0/24"

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [EnvironmentMap, !Ref EnvironmentType, VpcCidrBlock]
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-VPC"

  MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: !FindInMap [EnvironmentMap, !Ref EnvironmentType, SubnetCidrBlock]
      AvailabilityZone: us-east-1a
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-Subnet"

+Outputs:
+  MyVPCId:
+    Description: VPC ID
+    Value: !Ref MyVPC
+    Export:
+      Name: !Sub "${EnvironmentType}-VPCId"
+
+  MySubnetId:
+    Description: Subnet ID
+    Value: !Ref MySubnet
+    Export:
+      Name: !Sub "${EnvironmentType}-SubnetId"
```

# Conditions
Conditionsは条件に基づいて、作るリソースやOutputsの生成を制御するために使用します。
たとえば、開発環境でだけスタブサーバを作り、商用環境では作らない、という場合にConditionsを利用して実現します。

以下ではパラメータとしてDevelopmentを指定した時だけ、Outputsで値を出力し、それ以外の時には値を返さないように書き換えました。

```diff_yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  EnvironmentType:
    Type: String
    Default: Development
    AllowedValues: [Development, Production]
    Description: Specify the environment type (Development or Production)

Mappings:
  EnvironmentMap:
    Development:
      VpcCidrBlock: "10.0.0.0/16"
      SubnetCidrBlock: "10.0.0.0/24"
    Production:
      VpcCidrBlock: "172.16.0.0/16"
      SubnetCidrBlock: "172.16.0.0/24"

+Conditions:
+  CreateDevelopmentResources: !Equals [ !Ref EnvironmentType, Development ]

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Condition: CreateDevelopmentResources
    Properties:
      CidrBlock: !FindInMap [EnvironmentMap, Development, VpcCidrBlock]
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-VPC"

  MySubnet:
    Type: AWS::EC2::Subnet
    Condition: CreateDevelopmentResources
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: !FindInMap [EnvironmentMap, Development, SubnetCidrBlock]
      AvailabilityZone: us-east-1a
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-Subnet"

Outputs:
  MyVPCId:
    Description: VPC ID
+    Value: !If [CreateDevelopmentResources, !Ref MyVPC, !Ref "AWS::NoValue"]
-   Value: !Ref MySubnet
    Export:
      Name: !Sub "${EnvironmentType}-VPCId"

  MySubnetId:
    Description: Subnet ID
+   Value: !If [CreateDevelopmentResources, !Ref MySubnet, !Ref "AWS::NoValue"]
-   Value: !Ref MySubnet
    Export:
      Name: !Sub "${EnvironmentType}-SubnetId"
```


# 組み込み関数
組み込み関数はCloudFormationテンプレート上で利用できる組み込み関数です。
現時点で16個あるのですべては解説せず、よく使うものだけを解説します。

以下では各関数を正式に書いた場合/省略形で書いた場合を並べています。

## Fn::Ref/!Ref
Paramatersで指定した値やResourcesで定義したリソース名を参照するために使います。
この記事の中紹介しているCloudFormationテンプレートでも多数登場しています。

## Fn::GetAtt/!GetAtt
Resourcesで定義したリソースの属性を取得するために使います。
例えば、EC2インスタンスを作ったときに!GetAttでAZを取得し、同じAZにEBSボリュームを作成する、というシナリオで利用します。

## Fn::FindInMap/!FindInMap
Mappingsで定義した値を参照する場合に利用します。
!FindInMap [ MapName, TopLevelKey, SecondLevelKey ]というように書くとMapNameに定義されているTopLevelKeyの中のSecondLevelKeyの値を参照します。

## Fn::ImportValue/!ImportValue
他のCloudFormation StackでOutputs内のExportを用いて出力した値を利用する場合に使います。

## Fn::Sub/!Sub
入力文字列の変数を、指定した値に置き換えます。
!Sub "${EnvironmentType}-SubnetId"と書けばParametersでEnvironmentTypeに入力した値に置換されます。

## 条件関数(Fn::If/!If、Fn::Not/!Not、Fn::Equals/!Equalsなど)
Conditionsなどで使うロジックです。
CreateDevelopmentResources: !Equals [ !Ref EnvironmentType, Development ]であればEnvironmentTypeがDevelopmentであれば真となります。

# References
ParametersとMappingsで変数の説明はすでに行っていますが、ここではSytems Manager Parameter StoreあるいはSecretsManagerを用いて動的に変数を取得する方法について説明します。

これは、AMI IDのように頻繁に変わるかつ様々なCloudFormationテンプレートから参照したい値や、パスワードやクレデンシャルなどパラメータの中に記述すべきで名はない値を動的に取得する際に非常によく使用する手法です。
実プロジェクトでも試験でもよく出てくるのでしっかり覚えておきましょう。

![](https://storage.googleapis.com/zenn-user-upload/634479a7e707-20231112.png)

動的に取得できるパラメータの種別3つあるので以下にまとめました。

|種別 | 説明 | 具体例 |
|--- | --- | ---|
| ssm | SSM Parameter Storeに格納されているプレーンテキスト用 |AMI ID|
| ssm-secure | SSM Parameter Storeに格納されているsecure strings用 |ライセンスキー|
| secretsmanager | AWS Secrets Managerに格納されているsecret values用|RDSのマスターパスワード|

どこにどういうタイプで保存した値を参照したいかで利用する種別が異なるので注意しましょう。

例えばSubnetを構築するAZをSSM ParamteterStoreから取得する場合は以下のようなCloudFormationテンプレートになります。

```diff_yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  EnvironmentType:
    Type: String
    Default: Development
    AllowedValues: [Development, Production]
    Description: Specify the environment type (Development or Production)

Mappings:
  EnvironmentMap:
    Development:
      VpcCidrBlock: "10.0.0.0/16"
      SubnetCidrBlock: "10.0.0.0/24"
    Production:
      VpcCidrBlock: "172.16.0.0/16"
      SubnetCidrBlock: "172.16.0.0/24"

Conditions:
  CreateDevelopmentResources: !Equals [ !Ref EnvironmentType, Development ]

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Condition: CreateDevelopmentResources
    Properties:
      CidrBlock: !FindInMap [EnvironmentMap, Development, VpcCidrBlock]
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-VPC"

+ MySSMAvailabilityZoneParameter:
+   Type: AWS::SSM::Parameter::Value
+   Condition: CreateDevelopmentResources
+   Properties:
+     Name: /MyStack/Development/SubnetAvailabilityZone

  MySubnet:
    Type: AWS::EC2::Subnet
    Condition: CreateDevelopmentResources
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: !FindInMap [EnvironmentMap, Development, SubnetCidrBlock]
+     AvailabilityZone: !Ref MySSMAvailabilityZoneParameter
-     AvailabilityZone: us-east-1a
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentType}-Subnet"
```

このテンプレートでは、AWS::SSM::Parameter::Value リソースを使用して、SSM Parameter Storeからパラメータ /MyStack/Development/SubnetAvailabilityZone の値を取得しています。

# まとめ
この記事ではCloudFomationのテンプレートに記載する内容を超詳細にまとめました

- Resources
- Paramaters
- Mappings
- Outputs
- Conditions
- 組み込み関数
- References

次回はCloudFormationの超詳細解説 3/4 実行編です。