はじめに
書くこと
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-1a
とus-east-1c
で冗長化します。
これによって、片側のAZに障害があっても、業務継続可能な状態を維持するものとします。
後述しますが、ECSのサービスで、コンテナの希望数を2
としてマルチAZ化します。
コンテナは同じイメージを使い、可変のWordPressモジュール(wp-contentなど)はEFSに保存されているので、AZ障害等で影響を受けるのを最小限にします。
Auroraもインスタンスを2つ起動してライターとリーダーのインスタンスを作ります。片側AZに障害があったら、片方のインスタンスがマスターに昇格して業務を継続します。
セキュリティグループ
アウトバウンドはすべてのセキュリティグループで全許可をします。
インバウンドのみ、許可する通信を制限します。
セキュリティグループはALB、ECS、EFS、Auroraに対してアタッチするものを作成します。
それぞれのセキュリティグループにおいて、許可するインバウンド通信は以下の表のとおりです。
ALBセキュリティグループ
ポート | プロトコル | ソース | 備考 |
---|---|---|---|
80 | TCP | 0.0.0.0/0 | ユーザーからの通信許可 |
443 | TCP | 0.0.0.0/0 | WordPressコンテナからの通信許可(テーマやプラグインのアップデートに使用する) |
ECSセキュリティグループ
ポート | プロトコル | ソース | 備考 |
---|---|---|---|
80 | TCP | ALBセキュリティグループ | ALBからの通信許可 |
EFSセキュリティグループ
ポート | プロトコル | ソース | 備考 |
---|---|---|---|
2049 | TCP | ECSセキュリティグループ | ECSからの通信許可 |
Auroraセキュリティグループ
ポート | プロトコル | ソース | 備考 |
---|---|---|---|
3306 | TCP | ECSセキュリティグループ | 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で使用するファイルの一覧です。❶から順番に読み込んで使用します。
- Network.yml
- SecurityGroup.yml
- Storage.yml
- Database.yml
- LoadBallancer.yml
- 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
セクションにてEnv
とSysName
を定義しています。これは主にリソースのタグ名に使用するものとなります。Mappings
セクションについて- MapNameが
AzMap
は、AZを取得するマッピング変数です。組み込み関数!FindInMap
から参照されます。 - TopLevelKeyはすべて
us-east-1
にしました。(今回はus-east-1
リージョンのみを使用するため) - SecondLevelKeyは
pri
とsec
にしています。primaryとsecondaryの略です。 - パブリックサブネットのCIDRを
PublicCidrMap
、プライベートサブネットのCIDRをPrivateCidrMap
、データベースサブネットのCIDRをDatabaseCidrMap
としてマッピング変数で定義しています。こちらも!FindInMap
で参照されます。
- MapNameが
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
で参照します。
- VPCとサブネット(合計6個)をエクスポートしています。別のスタックから組み込み関数
- プライベートサブネットのみルートテーブルとルートが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/AmazonSSMReadOnlyAccess
とarn:aws:iam::aws:policy/SecretsManagerReadWrite
を割り当てています。 - タスク定義 → Dockerイメージの場所や設定、コンテナに割り当てるCPUとメモリの量を定義。ネットワークモードはFARGATEなので
awsvpc
になっています。 - ECSサービス → タスク定義で指定したコンテナの数を保つ役割。サブネットやセキュリティグループもここで指定する。あとALBに関連付けることもここで行っています。
- WordPressの環境変数について、
WORDPRESS_DB_HOST
とWORDPRESS_DB_NAME
とWORDPRESS_DB_USER
はContainerDefinitions
の中の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で書きながら理解する
コメント