AWS CloudFormation 実践 第3回 Parameters、Mappingsの活用

前回まで

CloudFormation連載の続きです。

前回は、CloudFormationのテンプレートを使って、AWSインフラ環境に下図のEC2環境を構築しました。

今回はテンプレートファイルの補足として、Parameters、Mappingsを追記することで、一つのテンプレートファイルから複数の条件に対応した環境を構築します。

CloudFormationの基礎知識や、テンプレートの基礎、実行方法は前回の記事をご確認ください。

<テンプレートファイル フォーマット>

AWSTemplateFormatVersion: "version date"

Description:
  String

Metadata:
  template metadata

Parameters:            # ← 今回はここの話
  set of parameters

Rules:
  set of rules

Mappings:              # ← 今回はここの話
  set of mappings

Conditions:
  set of conditions

Transform:
  set of transforms

Resources:
  set of resources

Outputs:
  set of outputs

Parameters とは

Parametersは、Cloud Formationテンプレートに記載するセクションの一つです。

実行時 (スタックを作成または更新するとき) にテンプレートに渡す値を定義するものであり、渡された値はテンプレートの Resources および Outputs セクションで使用することができます。

具体的な例を挙げてみましょう。 前回のテンプレートファイルを見直してみると、これにはVCPのIPアドレスがテンプレート内に固定値(10.0.0.0/21)で書かれています。この状態ですと、IPアドレスの変更要件がある都度、テンプレート自体を書き換える必要があるため、コード保守性が下がってしまいます。

Parametersでは、このような値をテンプレートファイルへのパラメタ(引数)として定義し、スタック作成時にコマンドから指定することができます。

具体的には次のようなコードになります。

VPCIPアドレスをパラメタ化したテンプレート>

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  # VPC Cidr(Required)
  cftVpcCidr:
    Type: String
    Description: (Required) Enter VPC IP Address.

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref cftVpcCidr    # ここで上のParameterを使う
      EnableDnsSupport: true

実行時には、デプロイコマンドにオプション --parameter-overrides を指定してパラメタに設定する値を指定します。

# デプロイコマンド全文
$ aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-prd --parameter-overrides cftVpcCidr=10.0.0.0/21

ただ実際の使われかたとして、上のように設定値をParametersで指定することは少なく、次のMappingと合わせて使うことが多いと思います。

Mappings とは

Mappingsも、Cloud Formationテンプレートに記載するセクションの一つであり、キーと値の関係を持つテーブル表の情報です。具体的なマッピングデータを以下に示します。

<環境 Map>

キー VpcCidr SubnetCidr Suffix
Dev(開発) 10.0.0.0/21 10.0.1.0/24 dev
Stg(検証) 10.1.0.0/21 10.1.1.0/24 stg
Prd(本番) 10.2.0.0/21 10.2.1.0/24 prd

上記は、環境名のキーごとに、VPC・サブネットのIPアドレスと、タグ名に使用されるサフィックスを表にまとめたものです。Cloud FormationのMappingsには上表を定義しておき、実行時には前述Parametersに定義したキーを指定することで、テンプレート内の各項目の値を実行時に決定することができます。ルックアップテーブルのようなものですね。

下にMappingsを使用したテンプレートファイルを記載します。

Parameters:
  # Environment
  cftEnv:
    Type: String
    Default: Dev      # ← Defaultは"Dev"(開発)
    AllowedValues:    # ← 下のいずれかから選択
    - Prd
    - Stg
    - Dev
    Description: Enter Prd, Stg or Dev. Default is Dev.

Mappings:
  # Environment Mapping
  cftEnvMap:
    Prd:
      VpcCidr: 10.2.0.0/21
      PublicSubnetCidr: 10.2.1.0/24
      PrivateSubnetCidr: 10.2.2.0/24
      Suffix: prd
    Stg:
      VpcCidr: 10.1.0.0/21
      PublicSubnetCidr: 10.1.1.0/24
      PrivateSubnetCidr: 10.1.2.0/24
      Suffix: stg
    Dev:
      VpcCidr: 10.0.0.0/21
      PublicSubnetCidr: 10.0.1.0/24
      PrivateSubnetCidr: 10.0.2.0/24
      Suffix: dev

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, VpcCidr] 
        #              ↑ !FindInMap関数でMappingsからVpcCidr値を取得する
      EnableDnsSupport: true
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-vpc-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

Mappingsを使用することによって、Parametersで入力された値に応じて、一つのテンプレートファイルで異なる環境の構築を指示することができるようになります。

EC2環境の構築

前回まで作成したテンプレートファイルを、Prd、Stg環境でも使用できるように書き換えました。

▼(テンプレート全文はここをクリックしてください)

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  # My IP Address(Required)
  cftMyIpAddress:
    Type: String
    Description: (Required)Mapping to Inbound rule on ec2'security group.

  # Environment
  cftEnv:
    Type: String
    Default: Dev
    AllowedValues:
    - Prd
    - Stg
    - Dev
    Description: Enter Prd, Stg or Dev. Default is Dev.

Mappings:
  # Environment Mapping
  cftEnvMap:
    Prd:
      Suffix: prd
      InstanceType: t3.small
      VpcCidr: 10.2.0.0/21
      PublicSubnetCidr: 10.2.1.0/24
      PrivateSubnetCidr: 10.2.2.0/24
    Stg:
      Suffix: stg
      InstanceType: t3.small
      VpcCidr: 10.1.0.0/21
      PublicSubnetCidr: 10.1.1.0/24
      PrivateSubnetCidr: 10.1.2.0/24
    Dev:
      Suffix: dev
      InstanceType: t3.micro
      VpcCidr: 10.0.0.0/21
      PublicSubnetCidr: 10.0.1.0/24
      PrivateSubnetCidr: 10.0.2.0/24

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [cftEnvMap, !Ref cåftEnv, VpcCidr]
      EnableDnsSupport: true
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-vpc-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-igw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

  # Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PublicSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb

  # Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PrivateSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb

  # NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-natgw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-public-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${privateIp}/32
        - {privateIp: !Ref cftMyIpAddress}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

  # EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-private-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${privateIp}/32
        - {privateIp: !GetAtt cftPublicEc2.PrivateIp}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

# ======================
#  Outputs Statement
# ======================
# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

Parametersに定義した内容は以下の2つです。

  • cftMyIpAddress:SSH接続で接続元となる自端末のグローバルIPアドレスです。
  • cftEnv:デプロイする環境です。Dev、Stg、Envのいずれかを指定します。

上記のテンプレートファイルを以下の通り実行することで、環境に応じたVPCがそれぞれ構築されます。(実行時、スタック名がそれぞれ違うものになるように注意してください。同じ名前で実行すると、環境を置き換えてしまうので。)

# 開発(Dev)環境 スタック
aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-dev --parameter-overrides cftMyIpAddress=xxx.xxx.xxx.xxx cftEnv=Dev

# 検証(Stg)環境 スタック
aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-stg --parameter-overrides cftMyIpAddress=xxx.xxx.xxx.xxx cftEnv=Stg

# 本番(Prd)環境 スタック
aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-prd --parameter-overrides cftMyIpAddress=xxx.xxx.xxx.xxx cftEnv=Prd

<実行後のEC2一覧>

まとめ

以上の通り、Parameters と Mappings を使用することによって、よりIaCらしいテンプレートファイルを作ることができました。環境ごとに手動で構築すると、同じ構成を作っているつもりでも作業ミスが発生することもあリます。

作業効率だけではなく、品質維持のためにも IaC は重宝しますね。

なお、この結果については、以下のGitHubに掲載していますのでご確認ください。 github.com