[CloudFormation] CFnで環境作り完全版 Part6. コンテナ編
目次
Introduction
この記事ではコンテナサービスであるECSを使用してWebサーバーを構築します。今回も前回と同じく多くのCFnを作成するため長くなります。頑張ってみていただけると嬉しいです。
今回作成するもの
AWSリソース

CFn相関図

HandsOn
ECSで使用するIAM Role
.pre-commit-config.yaml
cloudformation
L iam-user.yaml
iam-role.yaml(modify)
cloudtrail.yaml
vpc.yaml
subnet.yaml
sg.yaml
route53.yaml
acm.yaml
s3-bucket.yaml
secretsmanager.yaml
prefix
L alb.yaml
cf.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: iam-role. AWS::IAM::Role AWS::IAM::InstanceProfile"
Resources:
CloudTrailRole:
Type: AWS::IAM::Role
Properties:
RoleName: cloudtrail-role
Path: /service-role/
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- cloudtrail.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: cloudtrail-policy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: CloudWatchPutPolicy
Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*
ECSInstanceProfileRole:
Type: AWS::IAM::Role
Properties:
RoleName: ecs-instance-profile-role
Path: /
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
ECSInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref ECSInstanceProfileRole
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: task-execution-role
Path: /service-role/
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
Outputs:
CloudTrailRoleArn:
Description: Output Export AWS::IAM::Role.Arn CloudTrailRole
Value: !GetAtt CloudTrailRole.Arn
Export:
Name: cloudtrail-role-arn
ECSInstanceProfileArn:
Description: Output Export AWS::IAM::InstanceProfile.Arn ECSInstanceProfile
Value: !GetAtt ECSInstanceProfile.Arn
Export:
Name: ecs-instance-profile-arn
TaskExecutionRoleArn:
Description: Output Export AWS::IAM::Role.Arn TaskExecutionRole
Value: !GetAtt TaskExecutionRole.Arn
Export:
Name: task-execution-role-arn
ECSのコンテナインスタンスのIAMロールであるECSInstanceProfile
とタスクを実行するためのIAMロールであるTaskExecutionRole
の2つを作成します。
ECSInstanceProfileは、Amazon ECS コンテナインスタンスの IAM ロールを参考にしました。今回は、EC2で構築するECSのためコンテナインスタンスロールを必要としますが、Fargateの場合は必要ないみたいです。
タスク実行ロールのTaskExecutionRoleはタスク定義の作成で使用します。
各ECSロールの解説は、【ECS】ECSに関するIAM Roleを整理する【AWS】をご覧ください
ECS Cluster
ECSのクラスターを作成します。
.pre-commit-config.yaml
cloudformation
L iam-user.yaml
iam-role.yaml
cloudtrail.yaml
vpc.yaml
subnet.yaml
sg.yaml
route53.yaml
acm.yaml
s3-bucket.yaml
secretsmanager.yaml
prefix
L alb.yaml
cf.yaml
ecs-cluster.yaml(new)
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecs-cluster. AWS::ECS::Cluster AWS::EC2::LaunchTemplate AWS::AutoScaling::AutoScalingGroup"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Environment Name Configuration
Parameters:
- Prefix
- Env
- Project
- Label:
default: Network And Security Configuration
Parameters:
- SubnetId
- SecurityGroup
- Label:
default: AutoScalingGroup Parameter Configuration
Parameters:
- Priority1InstanceType
- Priority2InstanceType
- Priority3InstanceType
- AsgMaxSize
- AsgMinSize
- AsgDesiredSize
- AsgOnDemandBase
- AsgOnDemandPercentage
Parameters:
Prefix:
Type: String
Description: AWS resource version management prefix
Default: v1
Env:
Type: String
Description: Project environment
AllowedValues: [dev, prd]
Project:
Type: String
Description: Project Name
Default: example
SubnetId:
Type: List<AWS::EC2::Subnet::Id>
Description: Subnet for ECSInstance
SecurityGroup:
Type: List<AWS::EC2::SecurityGroup::Id>
Description: SecurityGroup for ECSInstance
Priority1InstanceType:
Type: String
Description: Allowed ECS InstanceType. priority 1
AllowedValues: [t3.micro, t2.micro, t3.small]
Priority2InstanceType:
Type: String
Description: Allowed ECS InstanceType. priority 2
AllowedValues: [t3.micro, t2.micro, t3.small]
Priority3InstanceType:
Type: String
Description: Allowed ECS InstanceType. priority 3
AllowedValues: [t3.micro, t2.micro, t3.small]
AsgMaxSize:
Type: Number
Description: AutoScaling maximum number of ECSInstance
Default: 2
AsgMinSize:
Type: Number
Description: AutoScaling minimum number of ECSInstance
Default: 1
AsgDesiredSize:
Type: Number
Description: AutoScaling desired number of ECSInstance
Default: 1
AsgOnDemandBase:
Type: Number
Description: "Minimum number of activations on-demand ECSInstance"
Default: 0
AsgOnDemandPercentage:
Type: Number
Description: Percentage of on-demand ECSInstance(0~100%)
Default: 0
MaxValue: 100
MinValue: 0
Mappings:
EnvMaps:
dev:
EbsVolume: 32
AMIId: ami-02378d43835d39ff4 # ecs_agent_version: 1.68.2 / image_version: 2.0.20230214
prd:
EbsVolume: 32
AMIId: ami-02378d43835d39ff4 # ecs_agent_version: 1.68.2 / image_version: 2.0.20230214
Resources:
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${Prefix}-${Env}-${Project}-cluster
ECSLT:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: !Sub ${Prefix}-${Env}-${Project}-lt
LaunchTemplateData:
SecurityGroupIds: !Ref SecurityGroup
InstanceInitiatedShutdownBehavior: terminate
IamInstanceProfile:
Arn: !ImportValue ecs-instance-profile-arn
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: !FindInMap [EnvMaps, !Ref Env, EbsVolume]
DeleteOnTermination: true
VolumeType: gp3
DisableApiTermination: false
ImageId: !FindInMap [EnvMaps, !Ref Env, AMIId]
UserData:
Fn::Base64: !Sub
- |
#!/bin/bash
echo ECS_CLUSTER=${ECS_CLUSTER} >> /etc/ecs/ecs.config
- ECS_CLUSTER: !Ref ECSCluster
ECSASG:
Type: AWS::AutoScaling::AutoScalingGroup
UpdatePolicy:
AutoScalingRollingUpdate:
MaxBatchSize: 1
MinInstancesInService: 1
PauseTime: PT2M
Properties:
AutoScalingGroupName: !Sub ${Prefix}-${Env}-${Project}-asg
MixedInstancesPolicy:
InstancesDistribution:
OnDemandBaseCapacity: !Ref AsgOnDemandBase
OnDemandPercentageAboveBaseCapacity: !Ref AsgOnDemandPercentage
LaunchTemplate:
LaunchTemplateSpecification:
LaunchTemplateId: !Ref ECSLT
Version: !GetAtt ECSLT.LatestVersionNumber
Overrides:
- InstanceType: !Ref Priority1InstanceType
- InstanceType: !Ref Priority2InstanceType
- InstanceType: !Ref Priority3InstanceType
MetricsCollection:
- Granularity: 1Minute
VPCZoneIdentifier: !Ref SubnetId
MinSize: !Ref AsgMinSize
MaxSize: !Ref AsgMaxSize
DesiredCapacity: !Ref AsgDesiredSize
Tags:
- Key: Name
Value: !Sub ${Prefix}-${Env}-${Project}-ecs-instance
PropagateAtLaunch: true
Outputs:
ECSClusterArn:
Description: Output Export AWS::ECS::Cluster.Arn
Value: !GetAtt ECSCluster.Arn
Export:
Name: !Sub ${Prefix}-${Env}-${Project}-cluster-arn
ECSClusterName:
Description: Output Export AWS::ECS::Cluster.Ref
Value: !Ref ECSCluster
Export:
Name: !Sub ${Prefix}-${Env}-${Project}-ecs-cluster-name
スタック名は、v1-dev-ecs-cluster
Parametersの説明は以下になります。
- SubnetId: PrivateなSubnetを選択してください。
- SecurityGroup: ECSインスタンス用のsgを選択
- Priority(x)InstanceType: ECSで立てるEC2のインスタンスタイプです。
1~3で立ち上がる優先順位が選択でき、1が一番優先順位が高いです。今回のテンプレートではかなり小さいインスタンスタイプのみ選択できるようにしていますが、場合によって選択できるタイプを増やしていただければ、ただむやみにでかいインスタンスタイプを選択すると莫大な料金の請求がくるため気をつけてください - AsgMaxSize: AutoScalingで増加するインスタンスの最大数。
今回は設定してないので意味をなしていない - AsgMinSize: AutoScalingで減少するインスタンスの最小数。
今回は設定してないので意味をなしていない - AsgDesiredSize: AutoScalingで希望するインスタンス数。
基本的にECSインスタンス数はここに入力した値の数になる - AsgOnDemandBase: オンデマンドインスタンスの最小数
- AsgOnDemandPercentage: オンデマンドインスタンスの存在パーセンテージ AsgOnDemandBaseと調整しながら設定していく。
今回は100%スポットインスタンスにして料金を抑えたいため、以下の画像の設定にします。

ここでは説明しないですが、AutoScalingの設定は少し複雑なため、AWS公式のUpdatePolicy 属性やDevelopersIOのCloudFormation UpdatePolicyを使用してAuto Scaling Groupの更新を処理するを見てみてください。
CloudWatchLogs
ECSで動くアプリケーションログを保存するためのCloudWatchLogsを作成します。
.pre-commit-config.yaml
cloudformation
L iam-user.yaml
iam-role.yaml
cloudtrail.yaml
vpc.yaml
subnet.yaml
sg.yaml
route53.yaml
acm.yaml
s3-bucket.yaml
secretsmanager.yaml
prefix
L alb.yaml
cf.yaml
ecs-cluster.yaml
cw-logs.yaml(new)
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-cw-logs. AWS::Logs::LogGroup"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Environment Name Configuration"
Parameters:
- Prefix
- Env
- Project
Parameters:
Prefix:
Type: String
Description: AWS resource version management prefix
Default: v1
Env:
Type: String
Description: Project environment
AllowedValues: [dev, prd]
Project:
Type: String
Description: Project Name
Default: example
Resources:
ECSCWLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub ecs/${Prefix}-${Env}-${Project}
RetentionInDays: 7
Outputs:
ECSCWLogGroupName:
Description: Output Export AWS::Logs::LogGroup.Ref ECSCWLogGroup
Value: !Ref ECSCWLogGroup
Export:
Name: !Sub ${Prefix}-${Env}-${Project}-ecs-cw-loggroup-name
スタック名は、v1-dev-cw-logs
ログの保存期間は7日間にします。
ECS TaskDefinition
次にタスク定義を作成します。
タスク定義は設定項目がかなり多いので今回は必要最低限の項目のみ記載します。
.pre-commit-config.yaml
cloudformation
L iam-user.yaml
iam-role.yaml
cloudtrail.yaml
vpc.yaml
subnet.yaml
sg.yaml
route53.yaml
acm.yaml
s3-bucket.yaml
secretsmanager.yaml
prefix
L alb.yaml
cf.yaml
ecs-cluster.yaml
ecs-task-def.yaml(new)
cw-logs.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecs-task-def. AWS::ECS::TaskDefinition"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Environment Name Configuration
Parameters:
- Prefix
- Env
- Project
Parameters:
Prefix:
Type: String
Description: AWS resource version management prefix
Default: v1
Env:
Type: String
Description: Project environment
AllowedValues: [dev, prd]
Project:
Type: String
Description: Project Name
Default: example
Mappings:
EnvMaps:
dev:
Cpu: 256
Memory: 516
prd:
Cpu: 256
Memory: 516
Resources:
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub ${Prefix}-${Env}-${Project}-task
RuntimePlatform:
CpuArchitecture: X86_64
RequiresCompatibilities:
- EC2
Cpu: !FindInMap [EnvMaps, !Ref Env, Cpu]
Memory: !FindInMap [EnvMaps, !Ref Env, Memory]
ExecutionRoleArn: !ImportValue task-execution-role-arn
ContainerDefinitions:
- Name: !Sub ${Prefix}-${Env}-${Project}-container
Cpu: !FindInMap [EnvMaps, !Ref Env, Cpu]
Memory: !FindInMap [EnvMaps, !Ref Env, Memory]
Image:
Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecr-uri
PortMappings:
- ContainerPort: 80
HostPort: 0
Protocol: tcp
Essential: true
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group:
Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecs-cw-loggroup-name
awslogs-region: ap-northeast-1
スタック名は、v1-dev-ecs-task-def
コンテナのイメージは、まだGithubからコードを取ってくるリソースを作成していないため一時的にnginxの最新イメージを指定します。
ECS Service
ECSのタスク定義を作成したので、次にそれを使用してコンテナを立ち上げるECS Serviceを作成します。
.pre-commit-config.yaml
cloudformation
L iam-user.yaml
iam-role.yaml
cloudtrail.yaml
vpc.yaml
subnet.yaml
sg.yaml
route53.yaml
acm.yaml
s3-bucket.yaml
secretsmanager.yaml
prefix
L alb.yaml
cf.yaml
ecs-cluster.yaml
ecs-task-def.yaml
ecs-service.yaml(new)
cw-logs.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecs-service. AWS::ECS::Service"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Environment Name Configuration"
Parameters:
- Prefix
- Env
- Project
- Label:
default: "ECS Service Parameter Configuration"
Parameters:
- DesiredCount
Parameters:
Prefix:
Type: String
Description: AWS resource version management prefix
Default: v1
Env:
Type: String
Description: Project environment
AllowedValues: [dev, prd]
Project:
Type: String
Description: Project Name
Default: example
DesiredCount:
Type: Number
Description: ECS Service task DesiredCount
Default: 1
MaximumPercent:
Type: Number
Description: ECS Service task max percentage
Default: 100
MinimumHealthyPercent:
Type: Number
Description: ECS Service task min percentage
Default: 0
Resources:
ECSService:
Type: AWS::ECS::Service
Properties:
ServiceName: !Sub ${Prefix}-${Env}-${Project}-service
Cluster:
Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-cluster-arn
DeploymentConfiguration:
MaximumPercent: !Ref MaximumPercent
MinimumHealthyPercent: !Ref MinimumHealthyPercent
DesiredCount: !Ref DesiredCount
HealthCheckGracePeriodSeconds: 0
LaunchType: EC2
LoadBalancers:
- TargetGroupArn:
Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-tg-arn
ContainerPort: 80
ContainerName: !Sub ${Prefix}-${Env}-${Project}-container
SchedulingStrategy: REPLICA
TaskDefinition: !Sub ${Prefix}-${Env}-${Project}-task # リビジョンで固定したくないためImportValueを使用しない
DeploymentController:
Type: ECS
Outputs:
ECSServiceArn:
Description: Output Export AWS::ECS::Service.Ref ECSService
Value: !Ref ECSService
Export:
Name: !Sub ${Prefix}-${Env}-${Project}-ecs-service-name
ECSServiceName:
Description: Output Export AWS::ECS::Service.Ref ECSService
Value: !GetAtt ECSService.Name
Export:
Name: !Sub ${Prefix}-${Env}-${Project}-ecs-service-arn
スタック名は、v1-dev-ecs-service
ECS Serviceでのポイントは、DeploymentConfigurationです。この値はデプロイ時やリビジョン変更時にタスクをどの程度まで増減させるか決める設定になります。通常運用では半分まで減少を許容し最大で2倍増加することを求める設定をします(Max: 200, Min: 50)。今回はタスクが入れ替わる際、タスクが0になってよい設定にするのでMax: 100, Min: 0にします。
また、TaskDefinitionはImportValueでARNを利用しようとするとリビジョンも指定しないといけないのでSub関数でリビジョンは指定しないようにして最新のリビジョンで動くようにします。
これでnginxのコンテナが立ちました。 アクセスしてみましょう。
$ curl https://looseller.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
ブラウザだと画像のような画面になります。

終わりに
Webサービスが動くところまで作成できました。ECSサービスだけでも多くのサービスがあるため初めて触れる人は理解が大変だと思いますが、構築して上で理解できていくと思うので根気強くやっていただければと思います。
ついに次の記事で一旦最後になります。ゴールデンウィークでCFnの記事を書き切ろうと持って思っていましたが、ちょっとオーバーしそうです…