クラウド エンジニアブログ

[ゼロから始めるInfrastructure as Code] 第4回 テンプレートの分割

2023年2月22日掲載

始めに

クラウドインテグレーション課 クラウドエンジニアの濱田です。 前回はParametersセクションや組み込み関数を駆使して、動的にパラメータを設定する方法を学びました。 「ゼロから始めるInfrastructure as Code」、第4回目は、テンプレートの分割というテクニックについて勉強していきます。

前回のテンプレートは、VPCからEC2インスタンスまで一気通貫で作成するものになっています。 皆さんに考えて頂きたいのですが、運用の中で、EC2を増やしたくなったらどうしますか? 恐らく、以下の2択のどちらかだと思います。

  • テンプレートに追記する
  • 新しくEC2用のテンプレートを書く

「テンプレートに追記する」場合、だんだん1つのテンプレートが長くなって可読性が下がっていきます。 また、大体の場合、NWに比べてEC2の方が作成・更新・削除の頻度が激しいです。 EC2の修正の為に、毎回NWの部分にも目を通すのは大変です。

「新しくEC2用のテンプレートを書く」について、こちらは良いですね。 前回のテンプレートからEC2の部分を取り出して、NWはNW、EC2はEC2のテンプレートに分けた方が管理が楽になります。 では、どんな観点で分割していけばいいのか?

いきなりですが、まずは課題に取り組んで頂きましょう!

課題

  • 以下の要件を満たすテンプレートを記述して下さい。また、テンプレートはコンポーネント毎に分割して下さい。
    • 192.168.0.0/16のCidrを持つVPCを作成する
    • 作成したVPC内に、以下セグメントのサブネットを作成する
      • 192.168.0.0/24
    • Internet Gatewayを作成し、VPCにアタッチする
    • ルートテーブルを作成し、Subnetに関連付ける
    • ルートテーブルに0.0.0.0/0 -> IGWのルートを書く
    • EC2インスタンス用セキュリティグループを作成する
    • 作成したサブネット内に、EC2インスタンスを1台起動する

解説

ここから解説編です。 テンプレートの分割について、 ・どういった観点で行うべきなのか ・どんなテクニックがあるのか
という観点から解説していきます。

ライフサイクルによって整理する

まずは、テンプレートの分割はどういった観点から行うべきなのか、です。

ずばり! CloudFormationのテンプレートは、ライフサイクル毎に分けましょう。 ライフサイクルとは、作成・削除のタイミングが同じであることを指します。 今回の場合、 ・VPC、サブネット、インターネットゲートウェイなどのNW系 ・セキュリティグループ、EC2
に分けるのが良いでしょう。

では、テンプレートを分割してみましょう。

  • network.yaml
    AWSTemplateFormatVersion: "2010-09-09"
    Parameters:
      VpcCidr:
        Type: String
      
      SubnetCidr:
        Type: String
    
    Resources:
      Vpc:
        Type: AWS::EC2::VPC
        Properties:
          CidrBlock: !Ref VpcCidr
    
      InternetGW:
        Type: AWS::EC2::InternetGateway
    
      AttachGateway:
        Type: AWS::EC2::VPCGatewayAttachment
        Properties:
          VpcId: !Ref Vpc
          InternetGatewayId: !Ref InternetGW
    
      Subnet:
        Type: AWS::EC2::Subnet
        Properties:
          AvailabilityZone: !Select
            - 0
            - Fn::GetAZs: !Ref 'AWS::Region'
          VpcId: !Ref Vpc
          CidrBlock: !Ref SubnetCidr
    
      RouteTable:
        Type: AWS::EC2::RouteTable
        Properties:
          VpcId: !Ref Vpc
    
      Route:
        DependsOn: AttachGateway
        Type: AWS::EC2::Route
        Properties:
          RouteTableId: !Ref RouteTable
          DestinationCidrBlock: 0.0.0.0/0
          GatewayId: !Ref InternetGW
    
      AssociateRouteTable:
        Type: AWS::EC2::SubnetRouteTableAssociation
        Properties:
          SubnetId: !Ref Subnet
          RouteTableId: !Ref RouteTable
    
  • ec2.yaml
    AWSTemplateFormatVersion: "2010-09-09"
    Parameters:
      VpcId:
        Type: AWS::EC2::VPC::Id
    
      SubnetId:
        Type: AWS::EC2::Subnet::Id
    
      KeyName:
        Type: AWS::EC2::KeyPair::KeyName
    
      InstanceName:
        Type: String
    
      ImageId:
        Type: AWS::SSM::Parameter::Value
        Default: "/aws/service/ami-windows-latest/Windows_Server-2022-Japanese-Full-Base"
    
      InstanceType:
        Type: String
    
      VolumeSize:
        Type: Number
        MinValue: 60
    
    Resources:
      SecurityGroup:
        Type: AWS::EC2::SecurityGroup
        Properties:
          VpcId: !Ref Vpc
          GroupName: !Sub '${InstanceName}_SecurityGroup'
          GroupDescription: Enable Private access
          SecurityGroupIngress:
            - CidrIp: 10.0.0.0/8
              IpProtocol: -1
            - CidrIp: 172.16.0.0/12
              IpProtocol: -1
            - CidrIp: 192.168.0.0/16
              IpProtocol: -1
          SecurityGroupEgress:
            - CidrIp: 0.0.0.0/0
              IpProtocol: -1
    
      EC2:
        Type: AWS::EC2::Instance
        Properties:
          ImageId: !Ref ImageId
          InstanceType: !Ref InstanceType
          KeyName: !Ref KeyName
          BlockDeviceMappings: 
            - DeviceName: '/dev/sda1'
              Ebs:
                VolumeType: gp3
                VolumeSize: !Ref VolumeSize
                DeleteOnTermination: true
          NetworkInterfaces:
            - AssociatePublicIpAddress: false
              DeviceIndex: '0'
              SubnetId: !Ref SubnetId
              GroupSet: 
                - !Ref SecurityGroup
          Tags:
            -
              Key: Name
              Value: !Ref InstanceName
    

ec2.yamlのパラメータで「AWS::EC2::VPC::Id」と「AWS::EC2::Subnet::Id」を使ってあげれば、プルダウンでそれぞれのIDを選べるようになります。 これでnetworkとec2を分割出来ました。

パラメータを参照する方法を知る

ところで、VpcIdはシステム全体で1つのものを使う事が多いですよね。 これを都度入力するのはちょっと面倒です。 実は、CloudFormationにはスタック間でパラメータを参照する方法が用意されています。

    • クロススタック参照
    • ダイナミック参照
  • クロススタック参照 CloudFormationのOutputsセクションを上手く使う事で、スタック間でパラメータ参照できます。 使い方はまず、参照元のテンプレートにOutputsセクションを記述します。
    参照元: vpc.yaml

    AWSTemplateFormatVersion: "2010-09-09"
    Parameters:
      VpcCidr:
        Type: String
    
    Resources:
      Vpc:
        Type: AWS::EC2::VPC
        Properties:
          CidrBlock: !Ref VpcCidr
    
    Outputs:
      VpcId:
        Value: !Ref Vpc
        Export:
          Name: network-VpcId
    

    次に、参照先で、Fn::ImportValueを使って、Export -> Nameに設定した値を引数に取ります。
    参照先: ec2.yaml

    AWSTemplateFormatVersion: "2010-09-09"
    Parameters:
      VpcId:
        Type: AWS::EC2::VPC::Id
    
    Resources:
      SecurityGroup:
        Type: AWS::EC2::SecurityGroup
        Properties:
          VpcId: !ImportValue network-VpcId  # <--ココです
          GroupName: !Sub '${InstanceName}_SecurityGroup'
          GroupDescription: Enable Private access
          SecurityGroupIngress:
            - CidrIp: 10.0.0.0/8
              IpProtocol: -1
            - CidrIp: 172.16.0.0/12
              IpProtocol: -1
            - CidrIp: 192.168.0.0/16
              IpProtocol: -1
          SecurityGroupEgress:
            - CidrIp: 0.0.0.0/0
              IpProtocol: -1
    

    このように、VPCのような共有リソースのIDに対してOutputsセクションを定義、Exportで命名しておく事で、他のスタックから容易に参照できるようになります。 しかし便利な反面、注意事項もあります。 Exportした値が他のスタックから参照されている場合、元のリソースの置き換えが出来なくなります。 例えばCloudFormationでS3を作成し、他のスタックからバケット名を参照している場合、S3用スタックを削除できません。 場合によっては運用が止まる危険性もあるので、よく計画した上で利用しましょう。

  • ダイナミック参照 スタック間のパラメータ参照方法その2は、SSMパラメータストアを経由して参照する方法です。 使い方は、まず参照元のテンプレートでリソースを作成後、パラメータストアへ格納します。
    参照元: vpc.yaml

    AWSTemplateFormatVersion: "2010-09-09"
    Parameters:
      VpcCidr:
        Type: String
    
    Resources:
      Vpc:
        Type: AWS::EC2::VPC
        Properties:
          CidrBlock: !Ref VpcCidr
    
      VpcId:
        Type: AWS::SSM::Parameter
        Properties: 
          Description: 'VpcId'
          Name: 'VpcId'
          Type: String
          Value: !Ref Vpc
    

    そして、参照先でパラメータ名を指定するだけです。 ※ 書き方: ‘{{resolve:ssm:パラメータ名}}’
    参照先: ec2.yaml

    AWSTemplateFormatVersion: "2010-09-09"
    Parameters:
      VpcId:
        Type: AWS::EC2::VPC::Id
    
    Resources:
      SecurityGroup:
        Type: AWS::EC2::SecurityGroup
        Properties:
          VpcId: '{{resolve:ssm:VpcId}}'  # <--ココです
          GroupName: !Sub '${InstanceName}_SecurityGroup'
          GroupDescription: Enable Private access
          SecurityGroupIngress:
            - CidrIp: 10.0.0.0/8
              IpProtocol: -1
            - CidrIp: 172.16.0.0/12
              IpProtocol: -1
            - CidrIp: 192.168.0.0/16
              IpProtocol: -1
          SecurityGroupEgress:
            - CidrIp: 0.0.0.0/0
              IpProtocol: -1
    

    こちらはSSMパラメータストアが間に挟まる分、スタックの更新に影響を与えません。 個人的には、ExportするよりSSMを使う方が好きだったりします。

以上がスタックを跨いだパラメータの参照方法です。

テンプレート分割のメリット

テンプレートを分割すると何がいいのか? 以下の2つが挙げられます。

    • 可読性を上げられる
    • 再利用が可能になる
  • 可読性を上げられる こちらは最初に軽くお話した通り、テンプレートは長くなるとどんどん読みづらくなります。 ライフサイクル毎に分割することで、ある程度長さを抑えられるようになります。
  • 再利用が可能になる こちらが一番大きなメリットです。 スタックを作成する際、再利用可能なテンプレートをモジュールとして用意しておくことで、整合性の担保や、コード量の抑制を実現できます。

例えば、以下のように構成することで、root.yamlを修正するだけで、EC2やRDSを増やせるようになります。

これを実際に運用するには、AWS CLIと併用するなどひと手間必要なのですが、それはまた別の機会にご説明します。

テンプレートの分割については以上です。 ここまででCloudFormationの基本的な書き方の勉強は終了です。

「ゼロから始めるInfrastructure as Code」は次回で最後です。 最終回ではCloudFormationのベストプラクティスについて総括します。 最後までお楽しみに! 以上、エンジニアの濱田でした!

関連コラム

このコラムに関連する製品

このコラムに関連する
導入事例

このコラムに関連する
セミナー・イベント

[ゼロから始めるInfrastructure as Code] 第4回 テンプレートの分割

SHARE
シェアシェア ポストポスト
[ゼロから始めるInfrastructure as Code] 第4回 テンプレートの分割
SHARE
ポスト シェア