0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【IaC超入門】30日でAWS CloudFormationとTerraformをマスターするロードマップ - 11日目: CloudFormationのベストプラクティス:テンプレートの分割と再利用

0
Posted at

CloudFormationのベストプラクティス:テンプレートの分割と再利用

はじめに

CloudFormationを使ったインフラ管理において、初期段階では一つのテンプレートで複数のリソース(EC2インスタンス、S3バケット、VPC等)を管理することが多いでしょう。しかし、プロジェクトが成長するにつれて、この単一テンプレートアプローチには限界が見えてきます。

テンプレートが巨大化すると以下のような問題が発生します:

  • 数百〜数千行のYAMLファイルになり、特定のリソースを見つけるのに時間がかかる
  • 一部の変更でも全体のスタックを更新する必要があり、リスクが高い
  • 複数の開発者が同時に編集すると、競合が発生しやすい
  • 環境(開発、テスト、本番)ごとに微調整が困難

今回は、これらの課題を解決するテンプレート分割とOutputs/ImportValueを使った再利用のベストプラクティスを、実践的な例とともに解説します。

テンプレート分割の戦略

分割の考え方

テンプレートを分割する際は、以下の観点で整理することをお勧めします:

1. ライフサイクルによる分割

  • 基盤インフラ:VPC、サブネット、インターネットゲートウェイ(変更頻度:低)
  • 共有サービス:RDS、ElastiCache、ALB(変更頻度:中)
  • アプリケーション:EC2、Lambda、Auto Scaling Group(変更頻度:高)

2. 責任範囲による分割

  • ネットワークチーム:VPC、セキュリティグループ
  • データベースチーム:RDS、DynamoDB
  • アプリケーションチーム:EC2、Lambda、S3

分割のメリット

項目 単一テンプレート 分割テンプレート
保守性 変更箇所の特定が困難 変更範囲が明確
再利用性 プロジェクト固有 複数プロジェクトで共用可能
並行開発 競合が発生しやすい チームごとに独立開発可能
デプロイリスク 一部変更でも全体に影響 影響範囲を限定可能
テスト効率 全体のテストが必要 変更部分のみテスト可能

実践:OutputsとImportValueを使ったテンプレート連携

ステップ1:基盤ネットワークテンプレートの作成

まず、再利用可能なVPCとサブネットを定義します。

ファイル名:01-network.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: "基盤ネットワークインフラ - VPC、サブネット、インターネットゲートウェイ"

Parameters:
  ProjectName:
    Type: String
    Default: "MyProject"
    Description: "プロジェクト名(リソースのタグとエクスポート名に使用)"
  
  VpcCidr:
    Type: String
    Default: "10.0.0.0/16"
    Description: "VPCのCIDRブロック"

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-VPC"
        - Key: Project
          Value: !Ref ProjectName

  # インターネットゲートウェイ
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-IGW"
        - Key: Project
          Value: !Ref ProjectName

  # VPCにインターネットゲートウェイをアタッチ
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # パブリックサブネット(AZ-a)
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-Public-Subnet-A"
        - Key: Type
          Value: "Public"

  # パブリックサブネット(AZ-c)
  PublicSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.2.0/24"
      AvailabilityZone: !Select [1, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-Public-Subnet-C"
        - Key: Type
          Value: "Public"

  # プライベートサブネット(AZ-a)
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.11.0/24"
      AvailabilityZone: !Select [0, !GetAZs ""]
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-Private-Subnet-A"
        - Key: Type
          Value: "Private"

  # プライベートサブネット(AZ-c)
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.12.0/24"
      AvailabilityZone: !Select [1, !GetAZs ""]
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-Private-Subnet-C"
        - Key: Type
          Value: "Private"

  # パブリックルートテーブル
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-Public-RouteTable"

  # パブリックルート
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  # パブリックサブネットとルートテーブルの関連付け
  PublicSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable

# 重要:他のテンプレートで使用するためにエクスポート
Outputs:
  VPCId:
    Description: "作成されたVPCのID"
    Value: !Ref VPC
    Export:
      Name: !Sub "${ProjectName}-VPC-ID"
  
  PublicSubnetAId:
    Description: "パブリックサブネット(AZ-a)のID"
    Value: !Ref PublicSubnetA
    Export:
      Name: !Sub "${ProjectName}-PublicSubnetA-ID"
  
  PublicSubnetCId:
    Description: "パブリックサブネット(AZ-c)のID"
    Value: !Ref PublicSubnetC
    Export:
      Name: !Sub "${ProjectName}-PublicSubnetC-ID"
  
  PrivateSubnetAId:
    Description: "プライベートサブネット(AZ-a)のID"
    Value: !Ref PrivateSubnetA
    Export:
      Name: !Sub "${ProjectName}-PrivateSubnetA-ID"
  
  PrivateSubnetCId:
    Description: "プライベートサブネット(AZ-c)のID"
    Value: !Ref PrivateSubnetC
    Export:
      Name: !Sub "${ProjectName}-PrivateSubnetC-ID"

  # マルチAZ構成用のサブネットリスト
  PublicSubnets:
    Description: "パブリックサブネットのリスト"
    Value: !Join [",", [!Ref PublicSubnetA, !Ref PublicSubnetC]]
    Export:
      Name: !Sub "${ProjectName}-PublicSubnets"
  
  PrivateSubnets:
    Description: "プライベートサブネットのリスト"
    Value: !Join [",", [!Ref PrivateSubnetA, !Ref PrivateSubnetC]]
    Export:
      Name: !Sub "${ProjectName}-PrivateSubnets"

ステップ2:アプリケーションテンプレートの作成

ネットワークリソースを再利用するアプリケーションテンプレートを作成します。

ファイル名:02-application.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: "アプリケーションインフラ - EC2インスタンス、セキュリティグループ"

Parameters:
  ProjectName:
    Type: String
    Default: "MyProject"
    Description: "プロジェクト名(ネットワークテンプレートと同じ値を指定)"
  
  InstanceType:
    Type: String
    Default: "t3.micro"
    AllowedValues: ["t3.micro", "t3.small", "t3.medium"]
    Description: "EC2インスタンスタイプ"
  
  KeyPairName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: "EC2インスタンスにSSH接続するためのキーペア名"

# 最新のAmazon Linux 2023 AMIを動的に取得
Mappings:
  RegionMap:
    us-east-1:
      AMI: ami-0c02fb55956c7d316
    ap-northeast-1:
      AMI: ami-0d52744d6551d851e

Resources:
  # Webサーバー用セキュリティグループ
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${ProjectName}-WebServer-SG"
      GroupDescription: "Webサーバー用セキュリティグループ"
      VpcId: !ImportValue 
        Fn::Sub: "${ProjectName}-VPC-ID"  # ネットワークテンプレートからVPC IDをインポート
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: "0.0.0.0/0"
          Description: "HTTP traffic"
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: "0.0.0.0/0"
          Description: "HTTPS traffic"
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: "10.0.0.0/16"
          Description: "SSH from VPC"
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-WebServer-SG"

  # EC2インスタンス(パブリックサブネットに配置)
  WebServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
      KeyName: !Ref KeyPairName
      SubnetId: !ImportValue
        Fn::Sub: "${ProjectName}-PublicSubnetA-ID"  # パブリックサブネットIDをインポート
      SecurityGroupIds:
        - !Ref WebServerSecurityGroup
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          yum update -y
          yum install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "<h1>Hello from ${ProjectName} Web Server!</h1>" > /var/www/html/index.html
          echo "<p>Instance ID: $(curl http://169.254.169.254/latest/meta-data/instance-id)</p>" >> /var/www/html/index.html
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-WebServer"
        - Key: Project
          Value: !Ref ProjectName

  # Application Load Balancer(マルチAZ構成)
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub "${ProjectName}-ALB"
      Type: application
      Scheme: internet-facing
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Subnets: !Split
        - ","
        - !ImportValue
          Fn::Sub: "${ProjectName}-PublicSubnets"  # 複数サブネットをインポート
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-ALB"

  # ALB用セキュリティグループ
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${ProjectName}-ALB-SG"
      GroupDescription: "Application Load Balancer用セキュリティグループ"
      VpcId: !ImportValue
        Fn::Sub: "${ProjectName}-VPC-ID"
      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"
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-ALB-SG"

Outputs:
  WebServerInstanceId:
    Description: "作成されたWebサーバーのインスタンスID"
    Value: !Ref WebServerInstance
    Export:
      Name: !Sub "${ProjectName}-WebServer-InstanceID"
  
  WebServerPublicIP:
    Description: "WebサーバーのパブリックIPアドレス"
    Value: !GetAtt WebServerInstance.PublicIp
    Export:
      Name: !Sub "${ProjectName}-WebServer-PublicIP"
  
  ApplicationLoadBalancerDNS:
    Description: "Application Load BalancerのDNS名"
    Value: !GetAtt ApplicationLoadBalancer.DNSName
    Export:
      Name: !Sub "${ProjectName}-ALB-DNS"

デプロイと運用のベストプラクティス

デプロイ順序の管理

# 1. ネットワークインフラを最初にデプロイ
aws cloudformation create-stack \
  --stack-name myproject-network \
  --template-body file://01-network.yaml \
  --parameters ParameterKey=ProjectName,ParameterValue=MyProject

# 2. スタックの作成完了を待つ
aws cloudformation wait stack-create-complete \
  --stack-name myproject-network

# 3. アプリケーションスタックをデプロイ
aws cloudformation create-stack \
  --stack-name myproject-application \
  --template-body file://02-application.yaml \
  --parameters \
    ParameterKey=ProjectName,ParameterValue=MyProject \
    ParameterKey=KeyPairName,ParameterValue=my-keypair

エラーハンドリングと依存関係

よくある問題と対策:

  1. エクスポート名の重複

    # ❌ 悪い例:固定名を使用
    Export:
      Name: "VPC-ID"
    
    # ✅ 良い例:パラメータを使って一意な名前を生成
    Export:
      Name: !Sub "${ProjectName}-VPC-ID"
    
  2. 削除順序の間違い

    # ❌ 間違った順序(依存するスタックから削除するとエラー)
    aws cloudformation delete-stack --stack-name myproject-network
    
    # ✅ 正しい順序(依存されるスタックを最後に削除)
    aws cloudformation delete-stack --stack-name myproject-application
    aws cloudformation wait stack-delete-complete --stack-name myproject-application
    aws cloudformation delete-stack --stack-name myproject-network
    

まとめ:効果的なテンプレート分割のポイント

設計原則

  1. 単一責任原則:各テンプレートは一つの責務に集中する
  2. 疎結合:テンプレート間の依存関係を最小限に抑える
  3. 高凝集:関連するリソースは同じテンプレートにまとめる
  4. 再利用性:パラメータを活用して汎用的に設計する

運用面での考慮事項

  • 命名規則:エクスポート名には必ずプロジェクト名やスタック名を含める
  • バージョン管理:テンプレートはGitで管理し、変更履歴を追跡可能にする
  • テスト戦略:各テンプレートを独立してテストできるようにする
  • 監視とアラート:スタックの状態を監視し、異常時の対応手順を整備する

テンプレート分割は初期の学習コストはありますが、中長期的には開発効率とインフラの品質向上に大きく貢献します。小さなプロジェクトから実践を始めて、徐々にベストプラクティスを身につけていくことをお勧めします。

次回は、AWSの公式IaCツールであるAWS CDK(Cloud Development Kit)について、CloudFormationとの違いを交えて解説します。お楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?