🤞

[AWS CloudFormation]クロススタック参照でテンプレートを分割管理する

2022/02/06に公開

はじめに

AWS CloudFormationによる環境作成のコード化・自動化は、AWS Well-Architected Frameworkによって推奨されるベストプラクティスのひとつです。しかし、Cloud FormationのテンプレートファイルはJSONもしくはYAMLで記載する必要があり、ある程度複雑な構成を記述しようとすると肥大化して可読性・保守性が低くなりがちです。

本記事では、CloudFormationのOutputsおよびImportValue関数(クロススタック参照)を利用してテンプレートを分割管理する手法を試します。また、実際の運用においてどのような単位で分割するべきか?について、AWSベストプラクティスの提案を紹介します。

なお、本記事の引用元はAWSベストプラクティスおよびAWS Black Beltです。

結論

  • テンプレートファイルはOutputsを利用することで他ファイルから参照できる値を保持できる
  • Outputsにて保持した値はImportValue関数をもちいて別のテンプレートから参照可能
  • テンプレートの分割単位はライフサイクルと依存関係をベースとしたレイヤー単位とする

テンプレートの分割

以下構成をサンプルとしてCloud Formationテンプレートの分割を試していきます。

  1. VPCを作成する
  2. パブリックサブネットおよびプライベートを各1つずつ作成する
  3. パブリックサブネットにEC2インスタンスを作成する

VPCとサブネットの作成

まず、上記の1と2にあたる部分(ネットワーク部分)を作成します。テンプレートのサンプルはこのようになります。

network.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Create VPC & subnet
Resources:
  FirstVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: CloudFormation-VPC

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref FirstVPC
      Tags:
      - Key: Name
        Value: CloudFormation-VPC-PublicRT

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref FirstVPC
      Tags:
      - Key: Name
        Value: CloudFormation-VPC-PrivateRT

  PublicSubnet1A:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref FirstVPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: Name
        Value: CloudFormation-public-subnet-1a

  PubSubnet1ARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1A
      RouteTableId: !Ref PublicRouteTable

  PrivateSubnet1A:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref FirstVPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: Name
        Value: CloudFormation-private-subnet-1a

  PriSubnet1ARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1A
      RouteTableId: !Ref PrivateRouteTable

  InternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
      - Key: Name
        Value: CloudFormation-IGW

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref FirstVPC
      InternetGatewayId: !Ref InternetGateway

  Route:
    Type: AWS::EC2::Route
    DependsOn: InternetGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

Outputs:
  StackVPC:
    Description: VPC's ID
    Value: !Ref FirstVPC
    Export:
      Name: !Sub "${AWS::StackName}-VPCID"

  StackPublicSubnet1A:
    Description: Public Subnet's ID
    Value: !Ref PublicSubnet1A
    Export:
      Name: !Sub "${AWS::StackName}-PublicSubnet1A"

  StackPrivateSubnet1A:
    Description: Private Subnet's ID
    Value: !Ref PrivateSubnet1A
    Export:
      Name: !Sub "${AWS::StackName}-PrivateSubnet1A"

ポイントとなるOutputsを抜粋します。

Outputs:
  StackVPC:
    Description: VPC's ID
    Value: !Ref FirstVPC
    Export:
      Name: !Sub "${AWS::StackName}-VPCID"

<StackName>-VPCIDというキーでVPCのIDをエクスポートしており、このキーを指定することで別のテンプレートから参照できるようになります。

EC2の作成

つぎに、作成したパブリックサブネットにEC2を作成します。以下テンプレートを利用します。

application.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Create EC2 Instance
Parameters:
  KeyName: 
    Description : Name of an existing EC2 KeyPair.
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription : Can contain only ASCII characters.

Resources:
  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties: 
        # AMIは要差し替え
      ImageId: ami-03f91159819288efa
      InstanceType: t2.micro
      SubnetId: !ImportValue VPC-PublicSubnet1A
      BlockDeviceMappings:
        -
          DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: 8
      Tags:
      - Key: Name
        Value: myInstance
      KeyName: !Ref KeyName
      SecurityGroupIds:
         - !GetAtt "InstanceSecurityGroup.GroupId"

  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: connect with ssh 
      VpcId: !ImportValue VPC-VPCID
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: connect with ssh 
      VpcId: !ImportValue VPC-VPCID
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0

ポイントはImportValue関数です。<StackName>-VPCIDを指定することで、別テンプレートでOutputsした値を参照することができます。

以上がクロススタック参照を利用したテンプレート分割の基本的な流れとなります。

レイヤー単位による分割

次に、実際の運用においてどのように分割するべきか?という疑問が湧きます。AWSベストプラクティスでは以下の記載があります。

共通のライフサイクルと所有権を持つリソースのグループ化により、所有者は独自のプロセスやスケジュールを使用して、他のリソースに影響を与えることなくリソースのセットを変更できます。

これについて、AWS Black Beltにおいてより具体的な内容が提案されています(いずれも20200826 AWS Black Belt Online Seminar AWS CloudFormationより引用)。

インフラ全体のスタック分割

アプリケーションレイヤ内部のスタック分割

インフラ全体としてはネットワークレイヤ・セキュリティレイヤ・アプリケーションレイヤという区分、またアプリケーションレイヤの中でさらに共通レイヤ・データレイヤ・アプリケーションレイヤといった区分で分割していくようです。

各レイヤのライフサイクル(ネットワークレイヤはアプリケーションレイヤと比較してライフサイクルが長い)および依存関係(ネットワークレイヤは他のレイヤから依存される独立した層)を根拠として分割するので、直感的で管理しやすそうですね。
長大なテンプレートファイルとおさらばして、快適なインフラ自動化をしていきましょう!

Discussion