CloudFormationについて（3）-【実践】ネストスタックの作成-

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

前回の[第2回](https://qiita.com/k-suto-hisys/items/76bed78c9f13507d8f68)では、実際に簡単なAWS環境を基にCloudFormationテンプレートを作成し、内容を説明してきました。
しかし、作成したテンプレートにはCloudFormationのメリットを享受するために改善の余地がありました。
そこで第3回の今回は第2回で作成したテンプレートを改善するとともにネストスタックの考え方についてまとめていきたいと思います。

## 1. 【再掲】作成するAWS環境

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3024510/a3a1c838-d6d6-0ec3-c2f6-ce9e7aea6998.png)

## 2. CloudFormationテンプレートの改善
[前回](https://qiita.com/k-suto-hisys/items/76bed78c9f13507d8f68)では、以下のリソースを１つのテンプレートで作成しました。

- VPC
- InternetGateway
- Subnet
- RouteTable
- EC2
- SecurityGroup
- IAMRole
- KeyPair

しかし、CloudFormationではある程度のリソースのまとまり（機能のまとまり等）ごとにテンプレートを分割し作成することが推奨されます。なぜなら分割することで、AWS環境ごとに各テンプレートを組み合わせて作成しやすくなるからです。つまり、テンプレートの再利用がしやすくなるということです。

そこで今回は以下のリソースごとにテンプレートを作成していきます。
|テンプレート名（スタック名）|記載するリソース|
|:--|:--|
|rootstack|networkstack、ec2stack|
|networkstack|VPC、InternetGateway、Subnet、RouteTable|
|ec2stack|EC2、SecurityGroup、IAMRole、KeyPair|

VPC、InternetGateway等のネットワークリソースをまとめた「networkstack」というテンプレートと、EC2とEC2に付随するリソースをまとめた「ec2stack」というテンプレートを作成します。
また、テンプレ―トを分割するにあたり新たに「rootstack」というテンプレートを作成します。

rootstackは、networkstackやec2stackの親にあたるテンプレートで、Resourcesとして
各スタックを記述します。このテンプレートがあることでAWS環境ごとに各テンプレートを組み合わせて作成しやすくなります。

各テンプレート（スタック）間の関係性は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3024510/9b624b4d-74da-8bbd-8da5-6e54cdce0ab1.png)

なお、今回のような構成を「[ネストスタック構成](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html)」と言います。

スタックを作成する際は
①子にあたる各テンプレートをS3バケットにアップロードする。
②CloudFormationの「スタックの作成」-「テンプレートの指定」で親にあたるテンプレートファイルを指定する。
③スタック名や各パラメータを入力し、スタックを作成する。
という手順になります。

そして、作成した各テンプレ―トは以下の通りになります。

```yaml:00_rootstack.yaml 
AWSTemplateFormatVersion: "2010-09-09"

Description: "Root Stack"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: CommonParameter
        Parameters:
          - TemplateBucket
          - PjName
          - Env
      - Label:
          default: NetworkStackParameter
        Parameters:
          - VpcCidr
          - PublicSubnetCidr
      - Label:
          default: Ec2StackParameter
        Parameters:
          - VolumeSize01
          - ImageId01
          - InstanceType01
          - SourceIp

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

Parameters: 
  TemplateBucket:
    Type: String
    Default: qiita-dev-s3
    Description: S3Bucket Name for Template
  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:
  # network stack
  NetworkStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub https://${TemplateBucket}.s3-${AWS::Region}.amazonaws.com/Templates/01_networkstack.yaml
      Parameters:
        PjName: !Ref PjName
        Env: !Ref Env
        VpcCidr: !Ref VpcCidr
        PublicSubnetCidr: !Ref PublicSubnetCidr

  # ec2 stack
  Ec2Stack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub https://${TemplateBucket}.s3-${AWS::Region}.amazonaws.com/Templates/02_ec2stack.yaml
      Parameters:
        PjName: !Ref PjName
        Env: !Ref Env
        VolumeSize01: !Ref VolumeSize01 
        ImageId01: !Ref ImageId01
        InstanceType01: !Ref InstanceType01
        Vpc: !GetAtt NetworkStack.Outputs.VpcId
        PublicSubnet: !GetAtt NetworkStack.Outputs.PublicSubnetId
        SourceIp: !Ref SourceIp

```

```yaml:01_networkstack.yaml 
AWSTemplateFormatVersion: "2010-09-09"

Description: "Network Stack"

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

Parameters:
  PjName:
    Type: String
    Description: Project Name
  Env:
    Type: String
    Description: Environment Name
  VpcCidr:
    Type: String
    Description: Cidr of Vpc
  PublicSubnetCidr:
    Type: String
    Description: Cidr of PublicSubnet

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
  PublicNatgwSubnet01Route:
    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

# -------------------------------------------------------------------------------------------------------------- 
# Output Parameters
# -------------------------------------------------------------------------------------------------------------- 

Outputs:
  VpcId:
    Value: !Ref Vpc
  PublicSubnetId:
    Value: !Ref PublicSubnet

```

```yaml:02_ec2stack.yaml 
AWSTemplateFormatVersion: "2010-09-09"

Description: "EC2 Stack"

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

Parameters:
  PjName:
    Type: String
    Description: Project Name
  Env:
    Type: String
    Description: Environment Name
  VolumeSize01:
    Type: String
    Description: VolumeSize of PublicSubnetEc2
  ImageId01:
    Type: AWS::EC2::Image::Id
    Description: ImageId of PublicSubnetEc2
  InstanceType01:
    Type: String
    Description: InstanceType of PublicSubnetEc2
  Vpc:
    Type: AWS::EC2::VPC::Id
    Description: VpcId 
  PublicSubnet:
    Type: AWS::EC2::Subnet::Id
    Description: SubnetId of PublicSubnet
  SourceIp:
    Type: String
    Description: SourceIp of Internet

Resources:
# --------------------------------------------------------------------------------------------------------------  
# 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
```

networkstackやec2stackに関する記述に大きな変更はないため、
主にrootstackに関して説明します。



rootstackのParametersではnetworkstackやec2stackで使用するパラメータを記述しています。

```yaml:00_rootstack.yamlのParameters
Parameters: 
  TemplateBucket:
    Type: String
    Default: qiita-dev-s3
    Description: S3Bucket Name for Template
  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ではnetworkstackやec2stackに関する記述をしています。

```yaml:00_rootstack.yamlのResources
Resources:
  # network stack
  NetworkStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub https://${TemplateBucket}.s3-${AWS::Region}.amazonaws.com/Templates/01_networkstack.yaml
      Parameters:
        PjName: !Ref PjName
        Env: !Ref Env
        VpcCidr: !Ref VpcCidr
        PublicSubnetCidr: !Ref PublicSubnetCidr

  # ec2 stack
  Ec2Stack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub https://${TemplateBucket}.s3-${AWS::Region}.amazonaws.com/Templates/02_ec2stack.yaml
      Parameters:
        PjName: !Ref PjName
        Env: !Ref Env
        VolumeSize01: !Ref VolumeSize01 
        ImageId01: !Ref ImageId01
        InstanceType01: !Ref InstanceType01
        Vpc: !GetAtt NetworkStack.Outputs.VpcId
        PublicSubnet: !GetAtt NetworkStack.Outputs.PublicSubnetId
        SourceIp: !Ref SourceIp
```
Typeは[AWS::CloudFormation::Stack](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-stack.html)で、PropertiesのParametersには各スタックで利用する値を指定します。ここで指定した値は各スタックでParametersとして利用できます。
また、TemplateURLではテンプレートをS3に格納した先のURLを記述します。

ここでec2stackに着目します。
ec2stackではEC2の作成にあたり、networkstackで作成したPublicSubnetのSubnetIdを参照する必要があります。
またSecurityGroupの作成にあたり、networkstackで作成したVPCのVpcIdを参照する必要があります。
そこでnetworkstackでは[Outputsセクション](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)でec2stackに渡したいVpcIdやPublicSubnetのSubnetIdを定義します。

```yaml:networkstack.yamlのOutputsセクション
Outputs:
  VpcId:
    Value: !Ref Vpc
  PublicSubnetId:
    Value: !Ref PublicSubnet

```

そして、rootstack内のec2stackのParametersでは、リソースの属性値を返す[Fn::GetAtt関数](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html)を使用し「NetworkStack.Outputs.(LogicalID)」のように記述します。
このようにすることでec2stack側でVpcIdやPublicSubnetのSubnetIdを参照することができるようになります。

## まとめ
今回はCloudFormationのメリットを活かすために、ある程度のリソースのまとまり（機能のまとまり等）ごとにテンプレートを作成しました。
また、ネストスタックの考え方や別スタックに値を出力させる方法等をまとめました。
今回紹介した内容はCloudFormationテンプレートを作成する上で基本的な考え方になりますので、是非実践してみてください。

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