CloudFormationについて（2）-【実践】CloudFormationテンプレートの作成-

この記事は、[株式会社日立システムズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/hisys)の21日目の記事です。

[第1回](https://qiita.com/k-suto-hisys/items/6cf3f42b725387898b90)では、CloudFormationの概要についてまとめました。
第2回の今回は実際に簡単なCloudFormationテンプレートを作成し、内容を説明していきたいと思います。

# CloudFormationテンプレートの説明

## 1. 作成するAWS環境
今回作成するAWS環境の構成は以下の通りです。
シングルAZにPublicSubnetを作成し、EC2インスタンスを立ち上げるという簡単な構成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3024510/6ba93ac8-6687-2af3-6827-d50226187481.png)


## 2. 作成したCloudFormationテンプレート
実際に作成したテンプレートは以下の通りです。
今回はYAML形式で作成しています。

```yaml:qiita-stack.yaml 
AWSTemplateFormatVersion: "2010-09-09"

Description: "Qiita Stack"

# -------------------------------------------------------------------------------------------------------------- 
# Input Parameters
# -------------------------------------------------------------------------------------------------------------- 

Parameters:
  PjName:
    Type: String
    Default: Qiita
    Description: Project Name
  Env:
    Type: String
    Default: dev
    Description: Environment Name
  VpcCidr:
    Type: String
    Default: 192.168.0.0/16
    Description: Cidr of Vpc
  PublicSubnetCidr:
    Type: String
    Default: 192.168.0.0/24
    Description: Cidr of PublicSubnet
  VolumeSize01:
    Type: String
    Default: 8
    Description: VolumeSize of PublicSubnetEc2
  ImageId01:
    Type: AWS::EC2::Image::Id
    Default: ami-0de5311b2a443fb89
    Description: ImageId of PublicSubnetEc2
  InstanceType01:
    Type: String
    Default: t3.micro
    Description: InstanceType of PublicSubnetEc2
  SourceIp:
    Type: String
    Default: 10.0.0.0/8
    Description: SourceIp of Internet

Resources:
# --------------------------------------------------------------------------------------------------------------  
# Title: VPC  
# Description: VPC of Qiita-dev
# -------------------------------------------------------------------------------------------------------------- 

  Vpc:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: !Ref VpcCidr
      Tags: 
        - Key: Name
          Value: !Sub ${PjName}-${Env}-VPC

# --------------------------------------------------------------------------------------------------------------  
# Title: InternetGateway 
# Description: InternetGateway of Qiita-dev-VPC
# -------------------------------------------------------------------------------------------------------------- 

# InternetGateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub ${PjName}-${Env}-IGW

# Attach InternetGateway
  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway

# --------------------------------------------------------------------------------------------------------------  
# Title: Subnet
# Description: PublicSubnet of Qiita-dev-VPC
# -------------------------------------------------------------------------------------------------------------- 

# PublicSubnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties: 
      AvailabilityZone: 
        Fn::Select:
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref PublicSubnetCidr
      Tags: 
        - Key: Name
          Value: !Sub ${PjName}-${Env}-PublicSN
      VpcId: !Ref Vpc

# --------------------------------------------------------------------------------------------------------------  
# Title: RouteTable
# Description: RouteTable,Route,SubnetRouteTableAssociation of Qiita-dev-PublicSN
# -------------------------------------------------------------------------------------------------------------- 

# RouteTable of PublicSubnet
  PublicSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties: 
      Tags: 
        - Key: Name
          Value: !Sub ${PjName}-${Env}-PublicSN-RT
      VpcId: !Ref Vpc

# Route of PublicSubnetRouteTable
  PublicSubnetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicSubnetRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway 

# Subnet Route Assocciation of PublicSubnetRouteTable
  PublicSubnetSRTAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PublicSubnetRouteTable
      SubnetId: !Ref PublicSubnet

# --------------------------------------------------------------------------------------------------------------  
# Title: EC2
# Description: EC2 of Qiita-dev-PublicSN
# -------------------------------------------------------------------------------------------------------------- 

# EC2 of PublicSubnet
  PublicSubnetEc2: 
    Type: AWS::EC2::Instance
    Properties: 
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs: 
            VolumeType: gp2
            VolumeSize: !Ref VolumeSize01
      EbsOptimized: true
      ImageId: !Ref ImageId01
      InstanceType: !Ref InstanceType01
      KeyName: !Ref PublicSubnetEc2Key
      SecurityGroupIds:
        - !Ref Ec2SecurityGroup
      SubnetId: !Ref PublicSubnet
      Tags: 
        - Key: Name
          Value: !Sub ${PjName}-${Env}-EC2
      IamInstanceProfile: !Ref Ec2InstanceProfile

# --------------------------------------------------------------------------------------------------------------  
# Title: SecurityGroup
# Description: SecurityGroup of Qiita-dev-EC2
# -------------------------------------------------------------------------------------------------------------- 

# SecurityGroup of PublicSubnetEc2
  Ec2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: Security Group of PublicSubnet EC2 Instance
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref SourceIp
      Tags: 
        - Key: Name
          Value: !Sub ${PjName}-${Env}-EC2-SG
      VpcId: !Ref Vpc

# --------------------------------------------------------------------------------------------------------------  
# Title: IAM Role
# Description: IAM Role,InstanceProfile of Qiita-dev-EC2
# -------------------------------------------------------------------------------------------------------------- 

# IAM Role of PublicSubnetEc2
  Ec2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${PjName}-${Env}-EC2-Role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - ec2.amazonaws.com
            Action: 
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

# InstanceProfile of PublicSubnetEc2
  Ec2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
      - !Ref Ec2Role

# --------------------------------------------------------------------------------------------------------------  
# Title: Key Pair
# Description: Key Pair of Qiita-dev-EC2
# -------------------------------------------------------------------------------------------------------------- 

# KeyPair of PublicSubnetEc2
  PublicSubnetEc2Key:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: !Sub ${PjName}-${Env}-EC2-Key

```

各セクションごとに説明します。

### 2.1 AWSTemplateFormatVersion・Description
```yaml:AWSTemplateFormatVersion・Description
AWSTemplateFormatVersion: "2010-09-09"

Description: "Qiita Stack"
```
AWSTemplateFormatVersionでは
最新のテンプレートの形式バージョン「2010-09-09」を記述しています。
- [形式バージョン](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/format-version-structure.html)

Descriptionでは
今回のテンプレートに関して、説明文を記述しています。
-  [説明](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-description-structure.html)

### 2.2 Parameters
```yaml:Parameters
Parameters:
  PjName:
    Type: String
    Default: Qiita
    Description: Project Name
  Env:
    Type: String
    Default: dev
    Description: Environment Name
  VpcCidr:
    Type: String
    Default: 192.168.0.0/16
    Description: Cidr of Vpc
  PublicSubnetCidr:
    Type: String
    Default: 192.168.0.0/24
    Description: Cidr of PublicSubnet
  VolumeSize01:
    Type: String
    Default: 8
    Description: VolumeSize of PublicSubnetEc2
  ImageId01:
    Type: AWS::EC2::Image::Id
    Default: ami-0de5311b2a443fb89
    Description: ImageId of PublicSubnetEc2
  InstanceType01:
    Type: String
    Default: t3.micro
    Description: InstanceType of PublicSubnetEc2
  SourceIp:
    Type: String
    Default: 10.0.0.0/8
    Description: SourceIp of Internet
```
プロジェクト名や環境名からVPCやSubnetで設定するCIDRやEC2で設定するAMIID等を記述することでスタックを作成または更新時にGUI上でパラメータとして入力できるようになります。
- [パラメータ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html)

### 2.3 Resources
 
実際に構築したいリソースを記述しています。
今回は上から
- VPC
- InternetGateway
- Subnet
- RouteTable（宛先InternetGatewayへのデフォルトルートを設定）
- EC2
- SecurityGroup（特定送信元IPからのポート80に対するインバウンド通信を許可）
- IAMRole（EC2で[セッションマネージャー](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/session-manager.html)を使用できるようIAMPolicy「AmazonSSMManagedInstanceCore」を設定）
- KeyPair（EC2で使用するキーペアを作成）

の作成に必要な内容を記述しています。
記述した各リソースの詳細に関しては以下を参照ください。
- [AWS リソースおよびプロパティタイプのリファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)

また、Resources内で組み込み関数を使用しています。
今回使用している組み込み関数に関して説明します。以下の通りです。

|#|組み込み関数名（短縮形）|説明|
|:--|:--|:--|
|1|Fn::GetAZs（!GetAZs）|指定したリージョンのアベイラビリティーゾーンの配列を返す。例）[ap-northeast-1a、ap-northeast-1c、ap-northeast-1d]|
|2|Fn::Select（!Select）|リストから値を取得する。|
|3|Fn::Sub（!Sub）|指定した変数の値に置き換える。|
|4|Ref（!Ref）|指定したパラメータまたはリソースの値を返す。|

- [組み込み関数リファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html)

では、「PublicSubnet」を参考に組み込み関数の内容を確認します。
```yaml:PublicSubnet
# PublicSubnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties: 
      AvailabilityZone: 
        Fn::Select:
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref PublicSubnetCidr
      Tags: 
        - Key: Name
          Value: !Sub ${PjName}-${Env}-PublicSN
      VpcId: !Ref Vpc

```

まず、Properties の「AvailabilityZone」ではSubnetを作成するAZを
「ap-northeast-1a」のように値を指定する必要があります。
今回はFn::Selectを使用し、Fn::GetAZsで取得したアベイラビリティーゾーンの配列から0番目の値、すなわち「ap-northeast-1a」を返すように記載しています。
（※今回のリソースを作成するリージョンはap-northeast-1）

なお、Fn::GetAZsでは「AWS::Region」をRefで参照していますが、
CloudFormationでは「AWS::Region」のように事前定義されたパラメータ（[擬似パラメータ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html)）がいくつか用意されています。
このようなパラメータはParametersで記述する必要がありません。
ちなみにAWS::Regionでは包括的なリソースが作成されているリージョンを表す文字列を返します。


次に「Tags」ではSubnetに割り当てるタグを指定できます。
例えば「Key:Name」に対し、
「Value：（プロジェクト名）-（環境名）-（リソース名）」という命名規則があった際にFn::Subで変数を指定することでプロジェクト名や環境名を都度入力せずに済みます。
また、Parametersで値を変更することで全てのタグのValue値を動的に変更できますので汎用性の高いテンプレートを作成することができます。

次に「VpcId」ではSubnetを作成するVPCのIDを指定する必要があります。
ただ、スタックを作成するまでVpcIdは判明しませんので、以上のようにRefを使用することで値を指定（参照）できます。

ここでSubnet がVpcIDを使用するためにスタックを作成する際はVPC→Subnetの順番で作成される必要があります。
ただ組み込み関数 Refを使用していると暗黙的に参照先のリソースから作成するようになります。
Properties内でリソース属性の中のDependsOn属性を利用し、明示的にリソースの作成順番を指定することもできますので詳細は以下を参照してみてください。
- [DependsOn 属性](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html)

その他のリソース属性に関しては以下を参照してみてください。
- [リソース属性リファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-product-attribute-reference.html)

# まとめ
今回は簡単なAWS環境を基にCloudFormationテンプレートを作成し、内容を説明してきました。
各セクションで記述する内容や組み込み関数等について理解いただけたと思います。
ただ、今回は簡単なAWS環境を想定し作成しましたが、実際の環境は多くのリソースが構築されていると思います。
その際に全てのリソースを１つのテンプレートで記載していくと、CloudFormationを利用するメリットが損なわれてしまう可能性があります。
そこで次回は、今回作成したテンプレートを改善すると同時に「ネストスタック」の考え方について紹介したいと思います。

※本記事は2022/12/13時点での内容になります。
　最新の情報に関しては公式ドキュメントを参照ください。