IaCの汎用化について考えてみる（その２）

# IaCの汎用化について（その２）

Shigeyukiです。

前回に続き、ブームであるIaCに関してこれからはじめる人向けに、重要となるIaCの汎用化について解説してみたいと思います。
前回は以下ブログを参照。

https://qiita.com/ueno120525/items/428082da8e34c1174bbe?utm_source=stock_summary_mail&utm_medium=email&utm_term=uenoshi120525&utm_content=IaC%E3%81%AE%E6%B1%8E%E7%94%A8%E5%8C%96%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E8%80%83%E3%81%88%E3%81%A6%E3%81%BF%E3%82%8B%EF%BC%88%E3%81%9D%E3%81%AE%EF%BC%91%EF%BC%89&utm_campaign=stock_summary_mail_2024-03-23




その１）基礎編 では、テンプレートを抽象化することでテンプレートが汎用化され、保守性が向上することについて解説しました。
今回は、中級編としてテンプレートを分割することによる汎用性について解説します。

# テンプレートを分割する目的
いくつかのシステムで同じ設定をもつインフラをIaCコードで管理すると、類似したテンプレートコードが量産され、システムの欠陥が発覚した場合や機能改修する場合に、修正の手間やテンプレートの管理に労力が必要となります。
そこで、テンプレートの「再利用しやすい=汎用性が高い」状態でテンプレートを分割して定義することで、IaCの共有化が図りやすくなったり、品質の向上・メンテナンスコストの削減につながります。
特にテンプレートの規模が大きくなったり、部分的に類似したシステムが増えてくるとそうしたメリットは大きくなります。

# テンプレートを分割するための検討事項
テンプレートを再利用しやすく分割する場合の検討事項として、以下を挙げます。

:::note info
**検討事項**
**① ライフサイクル**：１つのテンプレートに異なるライフサイクルをもつリソースを定義しないように分割を考える。
**② レイヤー別**：アプリ層、セキュリティ層、ネットワーク層の各層の単位で分割を考える。
**③ ステート別**：ステートレスなリソースとステートフルなリソースで分割を考える。
**④ 補足（汎用性）**：インフラ設定を限定的にさせないように、リソースの各プロパティのデフォルト値を明確し、汎用性を持たせたいプロパティ値はパラメタ化する。
:::


#### ①② ライフサイクル・レイヤー別からみたテンプレート分割
ライフサイクルとレイヤー別は共に類似した特性があり、原則、レイヤー別にテンプレートを分けることで、必然的にライフサイクルの特性で分割することとなる。

* ネットワーク層：更新頻度は一番低く、インフラ構築後はほとんど更新されない。
* セキュリティ層：セキュリティ・権限周りの変更に限り更新するため、更新頻度としては低い。
* アプリケーション層：業務仕様の変更など高頻度で更新される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3744312/926e3cf7-7273-e8ec-9e38-633f6eb1ef47.png)



#### ③ ステート別からみたテンプレート分割
アプリケーション層に該当するリソースをステートフルとステートレスを基準にテンプレートを分割すると再利用性が高くなります。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3744312/e209c4f9-fed6-73a4-f0c6-0ee71d3bfb0e.png)

#### ④ 補足（汎用性）
インフラ設定を限定的にさせないように、リソースの各プロパティのデフォルト値を明確し、汎用性を持たせたいプロパティ値はパラメタ化する。
テンプレートを再利用しやすくするために、拡張させたいプロパティ値はパラメタすることで、分割されたテンプレートがより再利用しやすくなります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3744312/bfcd6c71-88b7-8b54-d818-f25bd4af7de2.png)



:::note warn
注意
テンプレートを細かく分割すると返って、インフラ管理が煩雑となってしまうデメリットが大きくなるため、検討事項は必要に応じて実施する必要があります。
:::
# テンプレート分割したイメージ
検討事項で上げた考慮ポイントを踏まえ、EC2＋RDS構成のシステムに対するテンプレート分割したイメージは以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3744312/d1e89403-1b85-917c-db0e-b294fa0459f2.png)





実装イメージは以下となります。

親スタックに対するテンプレート「cfn-abcsystem.yml」がこちらになります。
```cfn-abcsystem.yml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  VPCRange:
    Type: String
    Description: "VPC Subnet Range"
  PublicSubnetRangeA:
    Type: String
    Description: "Public SubnetA Range"
  PublicSubnetRangeB:
    Type: String
    Description: "Public SubnetB Range"
  PrivateSubnetRangeA:
    Type: String
    Description: "Private SubnetA Range"
  PrivateSubnetRangeB:
    Type: String
    Description: "Private SubnetB Range"
  DBUsername:
    Type: String
    Description: "Db User Name"
  MultiAZFlg:
    Type: String
    Description: "Multi Az true or false String"
  MySQLMajorNo:
    Type: String
    Description: "Major No of MySQL"
  MySQLMijorNo:
    Type: String
    Description: "Minor No of MySQL"
  Ec2InstanceType:
    Type: String
    Description: "EC2 Instance Type"
  DBInstanceType:
    Type: String
    Description: "EC2 Instance Type"

Resources:
  VpcStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./cfn-split-vpc.yml
      Parameters:
        NestVPCRange: !Ref VPCRange
        PublicSubnetRangeA: !Ref PublicSubnetRangeA
        PublicSubnetRangeB: !Ref PublicSubnetRangeB
        PrivateSubnetRangeA: !Ref PrivateSubnetRangeA
        PrivateSubnetRangeB: !Ref PrivateSubnetRangeB

  VpcSecurityStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./cfn-split-security.yml
      Parameters:
        TargetVpc: !GetAtt VpcStack.Outputs.VpcId

  Ec2Stack1:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./cfn-split-app.yml
      Parameters:
        EC2InstanceType: !Ref Ec2InstanceType
        DeploySubnetId: !GetAtt VpcStack.Outputs.PublicSubnetA
        InstanceName: NestedStackEC2-1
        SecurityGroupId: !GetAtt VpcSecurityStack.Outputs.EC2SecurityGroupId
        AttachRoleId: !GetAtt VpcSecurityStack.Outputs.EC2RoleId

  Ec2Stack2:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./cfn-split-app.yml
      Parameters:
        EC2InstanceType: !Ref Ec2InstanceType
        DeploySubnetId: !GetAtt VpcStack.Outputs.PublicSubnetB
        InstanceName: NestedStackEC2-2
        SecurityGroupId: !GetAtt VpcSecurityStack.Outputs.EC2SecurityGroupId
        AttachRoleId: !GetAtt VpcSecurityStack.Outputs.EC2RoleId

  RdsInstance:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./cfn-split-db.yml
      Parameters:
        RDSSecurityGroupId: !GetAtt VpcSecurityStack.Outputs.RDSSecurityGroupId
        DbAccessSecurityGroupId: !GetAtt VpcSecurityStack.Outputs.EC2SecurityGroupId
        DBName: "MyRDS"
        DBClass: !Ref DBInstanceType
        DBAllocatedStorage: 7
        DBUsername: !Ref DBUsername
        MultiAZ: !Ref MultiAZFlg
        MySQLMajorVersion: !Ref MySQLMajorNo
        MySQLMinorVersion: !Ref MySQLMijorNo
        DBSubnet1: !GetAtt VpcStack.Outputs.PrivateSubnetA
        DBSubnet2: !GetAtt VpcStack.Outputs.PrivateSubnetB

```

こちらがパラメータファイルとなり、抽象化されたテンプレートに対して、リソースの設定を付与するものとなります。

```parameter.yml
[
  {
    "ParameterKey": "PrivateSubnetRangeA",
    "ParameterValue": "10.0.2.0/24"
  },
  {
    "ParameterKey": "PrivateSubnetRangeB",
    "ParameterValue": "10.0.4.0/24"
  },
  {
    "ParameterKey": "PublicSubnetRangeA",
    "ParameterValue": "10.0.1.0/24"
  },
  {
    "ParameterKey": "PublicSubnetRangeB",
    "ParameterValue": "10.0.11.0/24"
  },
  {
    "ParameterKey": "VPCRange",
    "ParameterValue": "10.0.0.0/16"
  },
  {
    "ParameterKey": "DBUsername",
    "ParameterValue": "admin"
  },
  {
    "ParameterKey": "MultiAZFlg",
    "ParameterValue": "true"
  },
  {
    "ParameterKey": "MySQLMajorNo",
    "ParameterValue": "8.0"
  },
  {
    "ParameterKey": "MySQLMijorNo",
    "ParameterValue": "35"
  },
  {
    "ParameterKey": "Ec2InstanceType",
    "ParameterValue": "t2.micro"
  },
  {
    "ParameterKey": "DBInstanceType",
    "ParameterValue": "db.m5d.large"
  }
]

```


子スタック（ネットワークリソース）に対するテンプレート「cfn-abcsystem-vpc.yml」がこちらになります。
```cfn-abcsystem-vpc.yml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  NestVPCRange:
    Type: String
  PublicSubnetRangeA:
    Type: String
  PublicSubnetRangeB:
    Type: String
  PrivateSubnetRangeA:
    Type: String
  PrivateSubnetRangeB:
    Type: String

Resources:
  EC2VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref NestVPCRange
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: "Name"
          Value: "cf_VPC"

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref EC2VPC
      CidrBlock: !Ref PublicSubnetRangeA
      AvailabilityZone:
        !Select
          - 0
          - Fn::GetAZs: !Ref AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: "Name"
          Value: "cf_PublicSubnet"

  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref EC2VPC
      CidrBlock: !Ref PublicSubnetRangeB
      AvailabilityZone:
        !Select
          - 2
          - Fn::GetAZs: !Ref AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: "Name"
          Value: "cf_PublicSubnet"

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref EC2VPC
      CidrBlock: !Ref PrivateSubnetRangeA
      AvailabilityZone:
        !Select
          - 0
          - Fn::GetAZs: !Ref AWS::Region
      MapPublicIpOnLaunch: false
      Tags:
        - Key: "Name"
          Value: "cfn_PrivateSubnet"

  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref EC2VPC
      CidrBlock: !Ref PrivateSubnetRangeB
      AvailabilityZone:
        !Select
          - 2
          - Fn::GetAZs: !Ref AWS::Region
      MapPublicIpOnLaunch: false
      Tags:
        - Key: "Name"
          Value: "cfn_PrivateSubnet"

  PublicRouteIngress:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EC2VPC

  PublicRouteIngressAssociation1A:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteIngress
      SubnetId: !Ref PublicSubnetA

  PublicRouteIngressAssociation1B:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteIngress
      SubnetId: !Ref PublicSubnetB

  Igw:
    Type: AWS::EC2::InternetGateway

  VpcIgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref EC2VPC
      InternetGatewayId: !Ref Igw

  RouteIngressDefault:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteIngress
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref Igw
    DependsOn:
      - VpcIgwAttachment

Outputs:
  VpcId:
    Value: !Ref EC2VPC
  PublicSubnetA:
    Value: !Ref PublicSubnetA
  PublicSubnetB:
    Value: !Ref PublicSubnetB
  PrivateSubnetA:
    Value: !Ref PrivateSubnetA
  PrivateSubnetB:
    Value: !Ref PrivateSubnetB

```

子スタック（セキュリティリソース）に対するテンプレート「cfn-abcsystem-security.yml」がこちらになります。
```cfn-abcsystem-security.yml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  TargetVpc:
    Type: AWS::EC2::VPC::Id
    Description: "Target VPC"

Resources:
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "EC2 SG"
      GroupName: !Sub ${AWS::StackName}-ec2-sg
      VpcId: !Ref TargetVpc
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-ec2-sg
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0

  RDSSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref TargetVpc
      GroupName: !Sub "${AWS::StackName}-rds-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${AWS::StackName}-rds-sg"
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0

  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      Path: "/"
      RoleName: !Sub ${AWS::StackName}-ec2-role
      Tags: 
      - Key: Name
        Value: !Sub ${AWS::StackName}-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"

Outputs:
  EC2SecurityGroupId:
    Value: !Ref EC2SecurityGroup
  EC2RoleId:
    Value: !Ref EC2Role
  RDSSecurityGroupId:
    Value: !Ref RDSSecurityGroup


```

子スタック（アプリリソース）に対するテンプレート「cfn-abcsystem-app.yml」がこちらになります。
```cfn-abcsystem-app.yml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  DeploySubnetId:
    Type: AWS::EC2::Subnet::Id
  LatestLinuxAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
  InstanceName:
    Type: String
  SecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id
  EC2InstanceType:
    Type: String
    AllowedValues:
      - t2.micro
      - m1.small
      - m1.large
  AttachRoleId:
    Type: String

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestLinuxAmiId
      InstanceType: !Ref EC2InstanceType
      IamInstanceProfile: !Ref EC2InstanceProfile

      NetworkInterfaces:
        - 
          SubnetId: !Ref DeploySubnetId
          GroupSet:
            - !Ref SecurityGroupId
          DeviceIndex: 0
      Tags:
        - Key: Name
          Value: !Ref InstanceName

  EC2InstanceProfile: 
    Type: AWS::IAM::InstanceProfile
    Properties: 
      Path: "/"
      Roles: 
        - !Ref AttachRoleId
      InstanceProfileName: !Sub ${AWS::StackName}-ec2-profile

```

子スタック（DBリソース）に対するテンプレート「cfn-abcsystem-db.yml」がこちらになります。
```cfn-abcsystem-app.yml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  RDSSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id
  DbAccessSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id
  DBName:
    Type: String
  DBClass:
    Type: String
    Default: db.m5d.large
    AllowedValues:
      - db.m5d.large
      - db.m5d.xlarge
      - db.m5d.2xlarge
  DBAllocatedStorage:
    Type: Number
    Description: The size of the database(Gb)
    Default: 5
    MinValue: 5
    MaxValue: 1024
  DBUsername:
    Type: String
  DBSubnet1:
    Type: String
  DBSubnet2:
    Type: String
  MySQLMajorVersion:
    Type: String
    Default: "8.0"
  MySQLMinorVersion:
    Type: String
    Default: "35"
  DBEngine:
    Type: String
    Default: MySQL
  DBInstanceStorageSize:
    Type: String
    Default: "30"
  DBInstanceStorageType:
    Type: String
    Default: "gp2"
  DBMasterUserName:
    Type: String
    Default: "admin"
    NoEcho: true
    MinLength: 5
    MaxLength: 16
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
    ConstraintDescription: "must begin with a letter and contain only alphanumeric characters."
  MultiAZ: 
    Type: String
    AllowedValues: [ "true", "false" ]


Resources:
  RDSSecret: # ここでRDSのパスワードとなるランダム文字列を生成
    Type: "AWS::SecretsManager::Secret"
    Properties:
      Name: "RDSSecretInfo"
      GenerateSecretString:
        SecretStringTemplate: '{"username": "admin"}'
        GenerateStringKey: "password"
        PasswordLength: 16
        ExcludeCharacters: '"@/\'

  DBSubnetGroup: 
    Type: "AWS::RDS::DBSubnetGroup"
    Properties: 
      DBSubnetGroupName: !Sub "${DBName}-subnet"
      DBSubnetGroupDescription: "-"
      SubnetIds: 
        - !Ref DBSubnet1
        - !Ref DBSubnet2

  DBInstance: 
    Type: "AWS::RDS::DBInstance"
    Properties: 
      DBInstanceIdentifier: !Sub "${DBName}"
      Engine: !Ref DBEngine
      EngineVersion: !Sub "${MySQLMajorVersion}.${MySQLMinorVersion}"
      DBInstanceClass: !Ref DBClass
      AllocatedStorage: !Ref DBInstanceStorageSize
      StorageType: !Ref DBInstanceStorageType
      DBName: !Ref DBName
      MasterUsername: !Ref DBMasterUserName
      MasterUserPassword: !Sub '{{resolve:secretsmanager:${RDSSecret}:SecretString:password}}'
      DBSubnetGroupName: !Ref DBSubnetGroup
      PubliclyAccessible: false
      MultiAZ: !Ref MultiAZ
      AutoMinorVersionUpgrade: false
      DBParameterGroupName: !Ref DBParameterGroup  
      VPCSecurityGroups:
        - !Ref RDSSecurityGroupId
      CopyTagsToSnapshot: true
      BackupRetentionPeriod: 7
      Tags: 
        - Key: "Name"
          Value: !Sub "${DBName}"
    DeletionPolicy: "Delete"

  DBParameterGroup:
    Type: "AWS::RDS::DBParameterGroup"
    Properties:
      Family: !Sub "MySQL${MySQLMajorVersion}"
      Description: !Sub "${DBName}-parm"

```
# 発展
分割されたテンプレートファイルは、別システムで流用することが可能である。
以下には、システムAで構築したDBスタックを設定値違いで、システムBにもDBスタックを同一テンプレートを利用するイメージとなります。
ここでのポイントは、システムAとシステムBで個別にシステム固有の設定値をパラメータファイルとして管理することで、同一DBテンプレートに対して、動的にリソース設定を切り替えられるようになる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3744312/2574ff40-e472-8536-0405-f2363502ca4c.png)




# まとめ
IaC汎用化について、CloudFormationを例にしてテンプレート分割について解説してみました。
再利用しやすい状態でテンプレート分割することで、テンプレートの汎用性が高くなり、IaCの共有化・品質の向上・メンテナンスコストの削減につながります。
巨大なテンプレート１つとするのではなく、保守性が維持させるためにもテンプレートを分割してみてはいかがでしょうか。
その３）上級編では、IaCによるインフラ分割について解説します。
この記事がCloudFormationによるIaCの品質向上につながると幸いです。