【初学者向け】【CloudFormation(YAML)】WordPressをFargateで構築。3層のマルチAZ。(ALB,EFS,Aurora)

もくじ

はじめに

書くこと

CloudFormationを使用して、WordPressを構築してみたいと思います。詳細は以下にリスト化しました。

  • 3層構成(パブリックサブネット、プライベートサブネット、データベースサブネット)で構築します。
  • WordPressにはECSを使用します。
  • データベースはAuroraを使用します。
  • WordPressのディレクトリ(wp-contentなど)をEFSにマウントします。
  • マルチAZで冗長化して、ALBで負荷分散します。
  • 構築にはCloudFormationを使用します。言語はYAMLで行います。

執筆の背景

  • ECSとCloudFormationの学習のために行いました。
  • 2層構成の記事はネット上に複数ありましたが、3層構成で行った記事はあまりなかったため、やってみることにしました。

対象の読者

基本的に初心者向けの記事となっています。

  • これからCloudFormationを触っていきたい人。
  • これからECSを勉強していく人。
  • コンテナはローカルで動かしたことがある人。

設計

構成図(完成形)

下に完成形の構成図を示します。

これからこの構成図の中のコンポーネントを、部分的に見ながら順番に作成していきます。

その前に、まずは設計について説明させてください。

WEB層

パブリックサブネットの部分がWEB層になります。ここにはALBとNat Gatewayを配置します。

ALBの役割は、アプリケーション層に配置するWordPressコンテナ(ECS)への負荷分散です。

Nat Gatewayの役割は、WordPressコンテナがインターネットへ接続するために必要となります。WordPressのコンソール画面から、プラグインやテーマなどを更新するときなどに必要になります。

  • Nat Gatewayが無いと、プライベートなECRからコンテナイメージをPullすることもできなかったです。

Nat Gatewayは配置しているだけで結構なお金がかかるので、構築が完了したら削除してよいと思います。

  • ユーザーからの接続にはNat Gatewayは使われません。ALBを経由でのアクセスとなります。

アプリケーション層

プライベートサブネットの部分になります。

WordPressのコンテナをECRを使用して配置します。

WordPressのモジュール(wp-contentなど)をEFSにマウントして保存しますが、そのときに使用するマウントターゲットもこちらのプライベートサブネットに配置します。

  • 本件では触れませんが、コンテナなのでEFSにほんとにマウントできているのか確認することができません
    そのため、適宜EC2を立てて手動でマウントし、WordPressのモジュールが作成されているか確認する必要がありました。

データベース層

データベースサブネットの部分になります。

ここにはAuroraをマルチAZで配置します。

冗長化

ここではus-east-1を使用します。us-east-1aus-east-1cで冗長化します。

これによって、片側のAZに障害があっても、業務継続可能な状態を維持するものとします。

後述しますが、ECSのサービスで、コンテナの希望数を2としてマルチAZ化します。
コンテナは同じイメージを使い、可変のWordPressモジュール(wp-contentなど)はEFSに保存されているので、AZ障害等で影響を受けるのを最小限にします。

Auroraもインスタンスを2つ起動してライターとリーダーのインスタンスを作ります。片側AZに障害があったら、片方のインスタンスがマスターに昇格して業務を継続します。

セキュリティグループ

アウトバウンドはすべてのセキュリティグループで全許可をします。

インバウンドのみ、許可する通信を制限します。

セキュリティグループはALB、ECS、EFS、Auroraに対してアタッチするものを作成します。

それぞれのセキュリティグループにおいて、許可するインバウンド通信は以下の表のとおりです。

ALBセキュリティグループ

ポートプロトコルソース備考
80TCP0.0.0.0/0ユーザーからの通信許可
443TCP0.0.0.0/0WordPressコンテナからの通信許可(テーマやプラグインのアップデートに使用する)

ECSセキュリティグループ

ポートプロトコルソース備考
80TCPALBセキュリティグループALBからの通信許可

EFSセキュリティグループ

ポートプロトコルソース備考
2049TCPECSセキュリティグループECSからの通信許可

Auroraセキュリティグループ

ポートプロトコルソース備考
3306TCPECSセキュリティグループECSからの通信許可

(補足)やらないこと

  • SSL通信はやりません。なので証明書の取得も行いません。
  • カスタムドメインは使用しません。ALBのDNS名でアクセスします。
  • DBのパラメータには触れません。ほぼデフォルトのパラメータです。

構築

事前準備

WordPressのコンテナイメージをECRに保存

docker pull wordpress:latestコマンドでローカルに保存したイメージを、ECRのプライベートレジストリに保存しました。

データベースのパスワードをSystems Managerパラメータストアに保存

Systems Managerパラメータストアに、データベース用のパスワードをSecure Stringで保存しておきました。

名前はaurora-master-passwordで、バージョンは1です。

後ほどのDatabase.ymlから参照されます。

ファイル構成

CloudFormationで使用するファイルの一覧です。❶から順番に読み込んで使用します。

  1. Network.yml
  2. SecurityGroup.yml
  3. Storage.yml
  4. Database.yml
  5. LoadBallancer.yml
  6. Container.yml

Network.yml

ネットワークの土台を作成するスタックになります。Nat GatewayとEIPも含まれています。

構成図

Network.ymlで構築する部分の構成図です。

コード

Network.ymlのコードを以下に示します。説明はコードの後ろに記載しています。

AWSTemplateFormatVersion: "2010-09-09"
Description: "Network stack"

Parameters:
  Env: 
    Type: String
    Default: dev
  SysName:
    Type: String
    Default: ecswp

Mappings:
  AzMap:
    us-east-1:
      pri: us-east-1a
      sec: us-east-1c
  PublicCidrMap:
    us-east-1:
      pri: 10.0.1.0/24
      sec: 10.0.2.0/24
  PrivateCidrMap:
    us-east-1:
      pri: 10.0.10.0/24
      sec: 10.0.20.0/24
  DatabaseCidrMap:
    us-east-1:
      pri: 10.0.100.0/24
      sec: 10.0.200.0/24

Resources:
  # ---------------------------------------
  # VPC
  # ---------------------------------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-vpc

  # ---------------------------------------
  # パブリックサブネット1
  # ---------------------------------------
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, pri ]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [ PublicCidrMap, !Ref AWS::Region, pri ]
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-public-subnet1

  # ---------------------------------------
  # パブリックサブネット2
  # ---------------------------------------
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, sec ]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [ PublicCidrMap, !Ref AWS::Region, sec ]
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-public-subnet2
  
  # ---------------------------------------
  # プライベートサブネット1
  # ---------------------------------------
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, pri ]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [ PrivateCidrMap, !Ref AWS::Region, pri ]
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-private-subnet1

  # ---------------------------------------
  # プライベートサブネット2
  # ---------------------------------------
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, sec ]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [ PrivateCidrMap, !Ref AWS::Region, sec ]
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-private-subnet2

  # ---------------------------------------
  # データベースサブネット1
  # ---------------------------------------
  DatabaseSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, pri ]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [ DatabaseCidrMap, !Ref AWS::Region, pri ]
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-database-subnet1

  # ---------------------------------------
  # データベースサブネット2
  # ---------------------------------------
  DatabaseSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, sec ]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [ DatabaseCidrMap, !Ref AWS::Region, sec ]
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-database-subnet2
  
  # ---------------------------------------
  # インターネットゲートウェイ
  # ---------------------------------------
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-igw
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  
  # ---------------------------------------
  # EIP1
  # ---------------------------------------
  EIP1:
    Type: AWS::EC2::EIP
    Properties:
      Domain: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-eip1

  # ---------------------------------------
  # EIP2
  # ---------------------------------------
  EIP2:
    Type: AWS::EC2::EIP
    Properties:
      Domain: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-eip2
  
  # ---------------------------------------
  # Nat Gateway1
  # ---------------------------------------
  NatGateway1:
    Type: AWS::EC2::NatGateway
    Properties: 
      AllocationId: !GetAtt EIP1.AllocationId
      ConnectivityType: public
      SubnetId: !Ref PublicSubnet1
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-nat-gateway1

  # ---------------------------------------
  # Nat Gateway2
  # ---------------------------------------
  NatGateway2:
    Type: AWS::EC2::NatGateway
    Properties: 
      ConnectivityType: public
      AllocationId: !GetAtt EIP2.AllocationId
      SubnetId: !Ref PublicSubnet2
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-nat-gateway2

  # ---------------------------------------
  # ルートテーブル(パブリックサブネット)※1と2で兼用
  # ---------------------------------------
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-public-route-table
  # ---------------------------------------
  # ルート(パブリックサブネット)※1と2で兼用
  # ---------------------------------------
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  # ---------------------------------------
  # ルートテーブル関連付け(パブリックサブネット1)
  # ---------------------------------------
  PublicRouteTableAssoc1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable
  # ---------------------------------------
  # ルートテーブル関連付け(パブリックサブネット2)
  # ---------------------------------------
  PublicRouteTableAssoc2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  # ---------------------------------------
  # ルートテーブル(プライベートサブネット1)
  # ---------------------------------------
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-private-route-table1
  # ---------------------------------------
  # ルートテーブル(プライベートサブネット2)
  # ---------------------------------------
  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-private-route-table2
  # ---------------------------------------
  # ルート(プライベートサブネット1)
  # ---------------------------------------
  PrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1
  # ---------------------------------------
  # ルート(プライベートサブネット2)
  # ---------------------------------------
  PrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2
  # ---------------------------------------
  # ルートテーブル関連付け(プライベートサブネット1)
  # ---------------------------------------
  PrivateRouteTableAssoc1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable1
  # ---------------------------------------
  # ルートテーブル関連付け(プライベートサブネット2)
  # ---------------------------------------
  PrivateRouteTableAssoc2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable2

  # ---------------------------------------
  # ルートテーブル(データベースサブネット)※1と2で兼用
  # ---------------------------------------
  DatabaseRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-database-route-table
  # ---------------------------------------
  # ルートテーブル関連付け(データベースサブネット1)
  # ---------------------------------------
  DatabaseRouteTableAssoc1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref DatabaseSubnet1
      RouteTableId: !Ref DatabaseRouteTable
  # ---------------------------------------
  # ルートテーブル関連付け(データベースサブネット2)
  # ---------------------------------------
  DatabaseRouteTableAssoc2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref DatabaseSubnet2
      RouteTableId: !Ref DatabaseRouteTable

Outputs:
  # エクスポート(VPCID)
  VPC:
    Description: "VPC"
    Value: !Ref VPC
    Export: 
      Name: !Sub ${AWS::StackName}-VPC
  
  # エクスポート(パブリックサブネット1)
  PublicSubnet1:
    Description: "Public Subnet 1"
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet1

  # エクスポート(パブリックサブネット2)
  PublicSubnet2:
    Description: "Public Subnet 2"
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet2
  
  # エクスポート(プライベートサブネット1)
  PrivateSubnet1:
    Description: "Private Subnet 1"
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet1

  # エクスポート(プライベートサブネット2)
  PrivateSubnet2:
    Description: "Private Subnet 2"
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet2
  
  # エクスポート(データベースサブネット1)
  DatabaseSubnet1:
    Description: "Database Subnet 1"
    Value: !Ref DatabaseSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-DatabaseSubnet1

  # エクスポート(データベースサブネット1)
  DatabaseSubnet2:
    Description: "Database Subnet 2"
    Value: !Ref DatabaseSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-DatabaseSubnet2
  • ParametersセクションにてEnvSysNameを定義しています。これは主にリソースのタグ名に使用するものとなります。
  • Mappingsセクションについて
    • MapNameがAzMapは、AZを取得するマッピング変数です。組み込み関数!FindInMapから参照されます。
    • TopLevelKeyはすべてus-east-1にしました。(今回はus-east-1リージョンのみを使用するため)
    • SecondLevelKeyはprisecにしています。primaryとsecondaryの略です。
    • パブリックサブネットのCIDRをPublicCidrMap、プライベートサブネットのCIDRをPrivateCidrMap、データベースサブネットのCIDRをDatabaseCidrMapとしてマッピング変数で定義しています。こちらも!FindInMapで参照されます。
  • Resourcesセクションで以下のリソースを作成しています。
    • VPC
    • サブネット(パブリック用×2、プライベート用×2、データベース用×2)
    • Elastic IP×2 (Nat Gateway用)
    • Nat Gateway×2
    • ルートテーブル(パブリックサブネット用×1、プライベートサブネット用×2、データベースサブネット用×1) (※)
    • ルート(パブリックサブネット用×1、プライベートサブネット用×2、データベースサブネット用×1) (※)
    • ルートテーブルとルート関連付け(パブリックサブネット用×2、プライベートサブネット用×2、データベースサブネット用×2)
  • Outputsセクションについて
    • VPCとサブネット(合計6個)をエクスポートしています。別のスタックから組み込み関数!ImportValueで参照します。
  • プライベートサブネットのみルートテーブルとルートが2つあるのは、デフォルトルートとなるNat Gatewayが異なるためです。

SecurityGroup.yml

セキュリティグループを作成するスタックになります。構成図で追加する部分とコードの説明をします。

構成図(SecurityGroup.yml追加)

赤線で作成したセキュリティグループ4個が今回追加するものです。

コード

以下にコードを記載します。説明はコードの後ろに記載しています。

AWSTemplateFormatVersion: "2010-09-09"
Description: "Security group stack"

Parameters:
  Env: 
    Type: String
    Default: dev
  SysName:
    Type: String
    Default: ecswp

Resources:
  # ---------------------------------------
  # ALBセキュリティグループ
  # ---------------------------------------
  SecGroupALB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${Env}-${SysName}-sg-alb
      GroupDescription: Security Group for ALB
      VpcId: !ImportValue Network-VPC
      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
      SecurityGroupEgress:
        - IpProtocol: "-1"
          FromPort: -1
          ToPort: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-sg-alb

  # ---------------------------------------
  # ECSセキュリティグループ
  # ---------------------------------------
  SecGroupECS:
    Type: AWS::EC2::SecurityGroup
    DependsOn: SecGroupALB
    Properties:
      GroupName: !Sub ${Env}-${SysName}-sg-ecs
      GroupDescription: Security Group for ECS
      VpcId: !ImportValue Network-VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref SecGroupALB
      SecurityGroupEgress:
        - IpProtocol: "-1"
          FromPort: -1
          ToPort: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-sg-ecs
  
  # ---------------------------------------
  # EFSセキュリティグループ
  # ---------------------------------------
  SecGroupEFS:
    Type: AWS::EC2::SecurityGroup
    DependsOn: SecGroupECS
    Properties:
      GroupName: !Sub ${Env}-${SysName}-sg-efs
      GroupDescription: Security Group for EFS
      VpcId: !ImportValue Network-VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 2049
          ToPort: 2049
          SourceSecurityGroupId: !Ref SecGroupECS
        - IpProtocol: tcp
          FromPort: 2049
          ToPort: 2049
          SourceSecurityGroupId: !Ref SecGroupECS
      SecurityGroupEgress:
        - IpProtocol: "-1"
          FromPort: -1
          ToPort: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-sg-efs
  
  # ---------------------------------------
  # DBセキュリティグループ
  # ---------------------------------------
  SecGroupDB:
    Type: AWS::EC2::SecurityGroup
    DependsOn: SecGroupECS
    Properties:
      GroupName: !Sub ${Env}-${SysName}-sg-db
      GroupDescription: Security Group for DB
      VpcId: !ImportValue Network-VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !Ref SecGroupECS
      SecurityGroupEgress:
        - IpProtocol: "-1"
          FromPort: -1
          ToPort: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-sg-db
  
Outputs:
  # エクスポート(ECSセキュリティグループ)
  SecGruopECS:
    Description: "ECS Security Group ID"
    Value: !Ref SecGroupECS
    Export: 
      Name: !Sub ${AWS::StackName}-SecGroupECS

  # エクスポート(EFSセキュリティグループ)
  SecGruopEFS:
    Description: "EFS Security Group ID"
    Value: !Ref SecGroupEFS
    Export: 
      Name: !Sub ${AWS::StackName}-SecGroupEFS

  # エクスポート(DBセキュリティグループ)
  SecGruopDB:
    Description: "DB Security Group ID"
    Value: !Ref SecGroupDB
    Export: 
      Name: !Sub ${AWS::StackName}-SecGroupDB
  
  # エクスポート(ALBセキュリティグループ)
  SecGruopALB:
    Description: "ALB Security Group ID"
    Value: !Ref SecGroupALB
    Export: 
      Name: !Sub ${AWS::StackName}-SecGroupALB
  • Resourcesセクションで作成しているセキュリティグループは以下です。
    • ALB用
    • ECS用
    • EFS用
    • DB用
  • セキュリティグループのルールは設計セクションで述べたとおりです。
  • Outputsセクションで、上記のセキュリティグループをエクスポートしています。

Storage.yml

EFSストレージを作成するスタックになります。

構成図(Storage.yml追加)

追加するリソースを構成図で示します。EFSとマウントターゲットになります。

コード

Storage.ymlのコードです。説明はコードの後ろです。

AWSTemplateFormatVersion: "2010-09-09"
Description: "Storage stack"

Parameters:
  Env: 
    Type: String
    Default: dev
  SysName:
    Type: String
    Default: ecswp

Resources:
  # ---------------------------------------
  # EFS
  # ---------------------------------------
  EFS:
    Type: AWS::EFS::FileSystem
    Properties: 
      Encrypted: true
      FileSystemTags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-efs
  
  # ---------------------------------------
  # マウントターゲット1
  # ---------------------------------------
  MountTarget1:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref EFS
      SubnetId: !ImportValue Network-PrivateSubnet1
      SecurityGroups: [ !ImportValue SecurityGroup-SecGroupEFS ]
  # ---------------------------------------
  # マウントターゲット2
  # ---------------------------------------
  MountTarget2:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref EFS
      SubnetId: !ImportValue Network-PrivateSubnet2
      SecurityGroups: [ !ImportValue SecurityGroup-SecGroupEFS ]

Outputs:
  # エクスポート(EFS)
  EFS:
    Description: "EFS file system ID"
    Value: !Ref EFS
    Export: 
      Name: !Sub ${AWS::StackName}-EFS
  • ResourcesセクションではEFSファイルシステムと、マウントターゲット×2を作成しています。マウントターゲットが2つあるのは、マルチAZ構成としているためです。
  • OutputsセクションでEFSのファイルシステムをエクスポートしています。Container.ymlスタックから参照されます。

Database.yml

Auroraデータベースクラスターを作成するスタックです。

構成図(Database.yml追加)

構成図にはAuroraのライターインスタンスとリーダーインスタンスを追加しています。

コード

Database.ymlのコードです。説明はコードの後ろに記載しています。

AWSTemplateFormatVersion: "2010-09-09"
Description: "Database stack"

Parameters:
  Env: 
    Type: String
    Default: dev
  SysName:
    Type: String
    Default: ecswp

Mappings:
  AzMap:
    us-east-1:
      pri: us-east-1a
      sec: us-east-1c

Resources:
  # ---------------------------------------
  # DBサブネットグループ
  # ---------------------------------------
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: !Sub ${Env}-${SysName}-db-subnet-group
      DBSubnetGroupDescription: "Database subnet group"
      SubnetIds:
        - !ImportValue Network-DatabaseSubnet1
        - !ImportValue Network-DatabaseSubnet2
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-db-subnet-group

  # ---------------------------------------
  # DBクラスターパラメータグループ
  # ---------------------------------------
  DBClusterParameterGroup:
    Type: AWS::RDS::DBClusterParameterGroup
    Properties: 
      DBClusterParameterGroupName: !Sub ${Env}-${SysName}-db-cluster-parameter-group
      Description: "Database cluster parameter group"
      Family: aurora-mysql5.7
      Parameters: 
        time_zone: Asia/Tokyo
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-db-cluster-parameter-group
  
  # ---------------------------------------
  # DBインスタンスパラメータグループ
  # ---------------------------------------
  DBInstanceParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties: 
      DBParameterGroupName: !Sub ${Env}-${SysName}-db-instance-parameter-group
      Description: "Database instance parameter group"
      Family: aurora-mysql5.7
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-db-instance-parameter-group

  # ---------------------------------------
  # DBクラスター
  # ---------------------------------------
  DBCluster:
    Type: AWS::RDS::DBCluster
    Properties: 
      DatabaseName: wordpress
      DBClusterIdentifier: !Sub ${Env}-${SysName}-db-cluster
      DBClusterParameterGroupName: !Ref DBClusterParameterGroup
      DBInstanceParameterGroupName: !Ref DBInstanceParameterGroup
      DBSubnetGroupName: !Ref DBSubnetGroup
      DeletionProtection: false
      StorageEncrypted: true
      Engine: aurora-mysql
      EngineVersion: 5.7.mysql_aurora.2.11.2
      MasterUsername: root
      MasterUserPassword: '{{resolve:ssm-secure:aurora-master-password:1}}'
      Port: 3306
      VpcSecurityGroupIds: 
        - !ImportValue SecurityGroup-SecGroupDB
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-db-cluster
  
  # ---------------------------------------
  # DBインスタンス1
  # ---------------------------------------
  DBInstance1:
    Type: AWS::RDS::DBInstance
    Properties:
      DBParameterGroupName: !Ref DBInstanceParameterGroup
      Engine: aurora-mysql
      DBClusterIdentifier: !Ref DBCluster
      PubliclyAccessible: false
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, pri ]
      DBInstanceClass: db.t2.small
      DBInstanceIdentifier: !Sub ${Env}-${SysName}-db-instance-1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-db-instance-1

  # ---------------------------------------
  # DBインスタンス2
  # ---------------------------------------
  DBInstance2:
    Type: AWS::RDS::DBInstance
    Properties:
      DBParameterGroupName: !Ref DBInstanceParameterGroup
      Engine: aurora-mysql
      DBClusterIdentifier: !Ref DBCluster
      PubliclyAccessible: false
      AvailabilityZone: !FindInMap [ AzMap, !Ref AWS::Region, sec ]
      DBInstanceClass: db.t2.small
      DBInstanceIdentifier: !Sub ${Env}-${SysName}-db-instance-2
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SysName}-db-instance-2

Outputs:
  # エクスポート(ライターのエンドポイント)
  EndpointAddress:
    Description: "Database endpoint address"
    Value: !GetAtt DBCluster.Endpoint.Address
    Export: 
      Name: !Sub ${AWS::StackName}-EndpointAddress
  • ParametersセクションとMappingsセクションはNetworkスタックとほぼ同じです。
  • Resourcesセクションにて、以下のリソースを定義しています。
    • DBサブネットグループ → データベースサブネット×2を含むサブネット
    • DBクラスターパラメータグループ → DBクラスター用のパラメータグループ
    • DBインスタンスパラメータグループ → DBインスタンス用のパラメータグループ
    • DBクラスター → AuroraのDBクラスター
      • MasterUserPasswordにて'{{resolve:ssm-secure:aurora-master-password:1}}'としてSystems Managerパラメータストアに保存したパスワードを取得しています。
    • DBインスタンス1 → DBクラスターに紐づくDBインスタンス
    • DBインスタンス2 → DBクラスターに紐づくDBインスタンス
  • Outputsセクションではライターインスタンスのエンドポイントをエクスポートしています。Containerスタックから!ImportValueで参照されます。

LoadBalancer.yml

ロードバランサー(ALB)を作成するスタックです。

構成図(LoadBalancer.yml追加)

LoadBalancer.yml実行後の構成図です。パブリックサブネットにまたがってALBが作られています。

コード

LoadBalancer.ymlのコードです。説明はコードの後ろです。

AWSTemplateFormatVersion: "2010-09-09"
Description: "Load balancer stack"

Parameters:
  Env: 
    Type: String
    Default: dev
  SysName:
    Type: String
    Default: ecswp

Resources:
  # ---------------------------------------
  # ロードバランサー
  # ---------------------------------------
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties: 
      Type: application
      IpAddressType: ipv4
      LoadBalancerAttributes: 
        - Key: deletion_protection.enabled
          Value: false
      Name: !Sub ${Env}-${SysName}-alb
      Scheme: internet-facing
      SecurityGroups: 
        - !ImportValue SecurityGroup-SecGroupALB
      Subnets: 
        - !ImportValue Network-PublicSubnet1
        - !ImportValue Network-PublicSubnet2
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-alb
  
  # ---------------------------------------
  # ターゲットグループ
  # ---------------------------------------
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties: 
      VpcId: !ImportValue Network-VPC
      TargetType: ip
      IpAddressType: ipv4
      Matcher: 
        HttpCode: 200-302
      Name: !Sub ${Env}-${SysName}-target-group
      Port: 80
      Protocol: HTTP
      HealthCheckEnabled: true
      HealthCheckPath: /var/www/html/index.php
      HealthCheckPort: 80
      HealthCheckProtocol: HTTP
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-target-group
  
  # ---------------------------------------
  # リスナー
  # ---------------------------------------
  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties: 
      DefaultActions: 
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP

Outputs:
  # エクスポート(ALB)
  ALB:
    Description: "ALB"
    Value: !Ref ALB
    Export: 
      Name: !Sub ${AWS::StackName}-ALB

  # エクスポート(ターゲットグループ)
  TargetGroup:
    Description: "Target group"
    Value: !Ref TargetGroup
    Export: 
      Name: !Sub ${AWS::StackName}-TargetGroup
  • Resourcesセクションで以下のリソースを作成しています。
    • ロードバランサー → ALB
    • ターゲットグループ → コンテナが所属することになるターゲットグループ
    • リスナー → 80ポートでリッスンしてターゲットグループに転送します
  • OutputsセクションでALBとターゲットグループをエクスポートしています。Container.ymlから参照されます。

Container.yml

ECSを作成するスタックです。

構成図(Container.yml追加後)※完成形

Container.ymlを実行すると完成形の構成図となります。

コード

Container.ymlのコードです。説明はコードの後ろです。

AWSTemplateFormatVersion: "2010-09-09"
Description: "Container stack"

Parameters:
  Env: 
    Type: String
    Default: dev
  SysName:
    Type: String
    Default: ecswp

Resources:
  # ---------------------------------------
  # ECSクラスター
  # ---------------------------------------
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${Env}-${SysName}-ecs-cluster

  # ---------------------------------------
  # タスク実行ロール
  # ---------------------------------------
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: "Allow"
            Principal: 
              Service: 
                - "ecs-tasks.amazonaws.com"
            Action: 
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
        - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
        - arn:aws:iam::aws:policy/SecretsManagerReadWrite

  # ---------------------------------------
  # タスク定義
  # ---------------------------------------
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties: 
      Cpu: 1 vCPU
      Memory: 2 GB
      Family: !Sub ${Env}-${SysName}-task-definition
      RequiresCompatibilities:
       - FARGATE
      ExecutionRoleArn: !Ref TaskExecutionRole
      RuntimePlatform: 
        CpuArchitecture: X86_64
        OperatingSystemFamily: LINUX
      NetworkMode: awsvpc
      ContainerDefinitions: 
        - Name: test-wordpress
          Essential: true
          Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/wordpress:latest
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: tcp
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-create-group: true
              awslogs-group: "/ecs/wordpress"
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: ecswp
          MountPoints:
            - SourceVolume: efs
              ContainerPath: "/var/www/html/"
          Environment:
            - Name: WORDPRESS_DB_HOST
              Value: !ImportValue Database-EndpointAddress
            - Name: WORDPRESS_DB_NAME
              Value: wordpress
            - Name: WORDPRESS_DB_USER
              Value: root
          Secrets:
            - Name: WORDPRESS_DB_PASSWORD
              ValueFrom: aurora-master-password
      Volumes: 
        - Name: efs
          EFSVolumeConfiguration:
            FilesystemId: !ImportValue Storage-EFS
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-task-definition
  
  # ---------------------------------------
  # ECSサービス
  # ---------------------------------------
  ECSService:
    Type: AWS::ECS::Service
    Properties: 
      Cluster: !Ref ECSCluster
      DesiredCount: 2
      LaunchType: FARGATE
      LoadBalancers: 
        - ContainerName: test-wordpress
          ContainerPort: 80
          TargetGroupArn: !ImportValue LoadBalancer-TargetGroup
      NetworkConfiguration: 
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups: 
            - !ImportValue SecurityGroup-SecGroupECS
          Subnets: 
            - !ImportValue Network-PrivateSubnet1
            - !ImportValue Network-PrivateSubnet2
      ServiceName: !Sub ${Env}-${SysName}-ecs-service
      TaskDefinition: !Ref TaskDefinition
      Tags: 
        - Key: Name
          Value: !Sub ${Env}-${SysName}-ecs-service
  • Resourcesセクションで以下のリソースを作成しています。
    • ECSクラスター → コンテナ実行環境の境界
    • タスク実行ロール → タスクが他のAWSサービスの使用を許可するロール。今回はタスクがSystems ManagerパラメータストアからSecret Stringを取得する必要があるため、デフォルトのタスク実行ロールに加えて、arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccessarn:aws:iam::aws:policy/SecretsManagerReadWriteを割り当てています。
    • タスク定義 → Dockerイメージの場所や設定、コンテナに割り当てるCPUとメモリの量を定義。ネットワークモードはFARGATEなのでawsvpcになっています。
    • ECSサービス → タスク定義で指定したコンテナの数を保つ役割。サブネットやセキュリティグループもここで指定する。あとALBに関連付けることもここで行っています。
  • WordPressの環境変数について、WORDPRESS_DB_HOSTWORDPRESS_DB_NAMEWORDPRESS_DB_USERContainerDefinitionsの中のEnvironmentで指定しています。WORDPRESS_DB_PASSWORDのみ、Secretsで指定しています。これは、Systems Managerパラメータストアを参照する'{{resolve:ssm-secure:aurora-master-password:1}}'というやり方がTaskDefinitionでサポートされていないためです。

デプロイ

それでは書いてきたYAMLコードのスタックをデプロイします。

以下のコマンドを順番に実行していくだけです。(aws configureでターミナルにAWS認証情報を設定することをお忘れなく)

  • Network.yml
aws cloudformation create-stack --stack-name Network --template-body file://[ファイルまでのパス]/Network.yml
  • SecurityGroup.yml
aws cloudformation create-stack --stack-name SecurityGroup --template-body file://[ファイルまでのパス]/SecurityGroup.yml
  • Storage.yml
aws cloudformation create-stack --stack-name Storage --template-body file://[ファイルまでのパス]/Storage.yml
  • Database.yml
aws cloudformation create-stack --stack-name Database --template-body file://[ファイルまでのパス]/Database.yml
  • LoadBalancer.yml
aws cloudformation create-stack --stack-name LoadBalancer --template-body file://[ファイルまでのパス]/LoadBalancer.yml
  • Container.yml
aws cloudformation create-stack --stack-name Container --template-body file://[ファイルまでのパス]/Container.yml --capabilities CAPABILITY_NAMED_IAM

デプロイが完了しているか、AWSコンソールにログインして確認します。下の画像には関係ないものも含まれています。。。

動作確認

ALBのDNS名にブラウザでアクセスします。するとWordPressのインストール画面が表示されます。

「日本語」を選択して「次へ」をクリックします。

ようこそ画面が表示されるので、WordPressサイトの情報を好きに入力して「WordPressをインストール」ボタンをクリックします。

「成功しました!」と表示されればインストール成功です。

ようこその画面で入力したIDとパスワードでログインします。

ログインできました。

テーマやプラグインがアップデートできるか確認してみます。

更新できました。インバウンドもアウトバウンドも問題なく動作していることが確認できました。

参考サイト

CloudFormtaionのECSタスク定義にて、ログ設定をawslogsにしようとしたらハマった

EFSをECS Fargateにマウントする定義をCFnで書きながら理解する

【初学者向け】AWS Fargateを利用してWordPressサイトを構築

【祝!】ECSへの機密情報受け渡しがCloudFormationに対応しました!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!

コメント

コメントする

コメントは日本語で入力してください。(スパム対策)

CAPTCHA

もくじ
閉じる