想定読者
- aws の基礎知識がある
- docker の基礎知識がある
前準備
① aws cli のインストール
② IAM ユーザの作成
CloudFormation でリソースを構築できる IAM ユーザーの作成。以下の点に注意して作成します。
- CloudFormation を実行できる権限があるか(admin 権限がついていれば作成可能)
- プログラムによりアクセスを許可する
- IAM ユーザー発行時に発行される、アクセスキー ID、シークレットアクセスキーをメモしておく
IAMユーザーの作成では下記のサイトが参考になります。
必要に応じて IAM ユーザーの権限を絞ってください。一般的に最低限の権限を与えることが良いとされています。
IAM ポリシーを作成する場合、最小限のアクセス権を付与するという標準的なセキュリティアドバイスに従うか、タスクの実行に必要なアクセス許可のみ付与します。
出典:https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/best-practices.html#grant-least-privilege
③ 上記で作成したユーザーの アクセスキー ID、シークレットアクセスキー を aws cli の credentials に設定
以下を ~/.aws/credentials に追記
[cloudformation_sample]
aws_access_key_id=${アクセスキー ID}
aws_secret_access_key=${シークレットアクセスキー}
以下を ~/.aws/config に追記
[profile cloudformation_sample]
region=ap-northeast-1
④ docker の install
⑤ Dockerfile の作成
本記事では以下の Dockerfile
を使います。あまり意味のない Dockerfile
ですが、build して image を作成したいのでこのようにしています。
FROM nginx
⑥ Route53 でドメインの取得(オプション)
なくても大丈夫です。CloudFormation を使った Route53 の設定もしたい方は必要です。
概要
CloudFormation を使って下図で示す構成を構築します。
ECR に登録された docker image をもとに、Fargate を使ってコンテナを起動します。コンテナへのアクセスはロードバランサを使って制御します。コンテナのログは CloudWatch へ、ロードバランサへのログは S3 に残します。また、コンテナの cpu 使用率が閾値を超えた際に自動でスケールするように、AutoScaling を設定していきます。
CloudFormation で作るメリット
簡単に同じ環境を構築、削除できることです。
例えば、検証用のテスト環境の構築を任されたとします。もちろん、AWS上のコンソールでポチポチして作ることも可能ですが、本記事の用に多くのリソースを作成する場合は大変です。また、人間なので必ずミスも起きます。作成のみならず削除も大変です。料金がかかるサービスもあるので、削除が必要ですが、削除漏れや、誤って必要なリソースを削除してしまう可能性があります。
一方で、CloudFormation を使ってコードでインフラを定義していれば、コマンド一つで検証環境の構築・削除ができます。工数も削減できますし、100%再現できます。削除漏れもありません。
システム構築方法
-
下記ファイルをカレントディレクトリに用意する
以下のようになっていればOK
❯ ls Dockerfile parameters.ecr.json parameters.ecs.json template.ecr.yml template.ecs.yml
-
ECR の作成
❯ aws cloudformation create-stack \ --profile cloudformation_sample \ --stack-name cloudformation-sample-ecr \ --capabilities CAPABILITY_IAM \ --template-body file://$(pwd)/template.ecr.yml \ --parameters file://$(pwd)/parameters.ecr.json ## output { "StackId": "arn:aws:cloudformation:ap-northeast-1:${account_id}:stack/cloudformation-sample-ecr/${id}" }
account_id
は後で使うのでメモしておく -
ECR に docker image を登録
❯ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com ❯ docker build -t cloudformation-sample-ecr . ❯ docker tag cloudformation-sample-ecr ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/cloudformation-sample-ecr:latest ❯ docker push ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/cloudformation-sample-ecr:latest
-
ECS の作成
❯ aws cloudformation create-stack \ --profile cloudformation_sample \ --stack-name cloudformation-sample-ecs \ --capabilities CAPABILITY_IAM \ --template-body file://$(pwd)/template.ecs.yml \ --parameters file://$(pwd)/parameters.ecs.json
-
確認
ALB の DNS 名にアクセスして nginx の画面が映れば OK
パラメーターの作成
以下のような理由でパラメーターを別ファイルで定義しています。
パラメーターをテンプレートファイルから分離していると、再利用性が高まる。例えば、パラメータファイルをコピーして vpc や subnet の id を書き換えるだけで全く同じ環境を dev と prod に簡単に構築できる。
また、github 等にソースコードをアップロードすることを考えても template に id などをハードコーディングしていると誰からでも id が見えてセキュリテイィー的によろしくないと思われます。
parameters.ecr.json
[
{
"ParameterKey": "ProjectName",
"ParameterValue": "cloudformation_sample"
}
]
parameters.ecs.json
-
${VPC ID}
,${Private Subnet IDs}
,${Public Subnet IDs}
はご自身のものに書き換えてください - ALB の CIDR が
0.0.0.0/0
とフルオープンになっているので必要に応じて変更してください
[
{
"ParameterKey": "ProjectName",
"ParameterValue": "cloudformation_sample"
},
{
"ParameterKey": "VpcId",
"ParameterValue": "${VPC ID}"
},
{
"ParameterKey": "PrivateSubnetIds",
"ParameterValue": "${Private Subnet IDs}" // ","区切りで書く
},
{
"ParameterKey": "PublicSubnetIds", // ","区切りで書く
"ParameterValue": "${Public Subnet IDs}"
},
{
"ParameterKey": "ALBAllowCidrIp",
"ParameterValue": "0.0.0.0/0"
},
{
"ParameterKey": "ECSDesiredCapacity",
"ParameterValue": "1"
},
{
"ParameterKey": "ECSMaxCapacity",
"ParameterValue": "2"
},
{
"ParameterKey": "ECSContainerPort",
"ParameterValue": "80"
},
{
"ParameterKey": "ECSContainerLogsRetentionInDays",
"ParameterValue": "30"
},
{
"ParameterKey": "ECSTaskCpuMB",
"ParameterValue": "512"
},
{
"ParameterKey": "ECSTaskMemory",
"ParameterValue": "2048"
},
{
"ParameterKey": "TargetGroupHealthCheckPath",
"ParameterValue": "/"
}
]
テンプレートの作成
template.ecr.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Create ECR
Parameters:
ProjectName:
Type: String
Resources:
ECR:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Sub ${ProjectName}-ecr
Outputs:
ECR:
Value: !GetAtt ECR.RepositoryUri
Export:
Name: !Sub ${ProjectName}-ecr-repository-uri
template.ecs.yml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
ProjectName:
Type: String
Description: Project name.
VpcId:
Type: AWS::EC2::VPC::Id
Description: Select a VPC that allows instances access to the Internet.
PrivateSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: Select at public subnets in your selected VPC.
PublicSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: Select at public subnets in your selected VPC.
ALBAllowCidrIp:
Type: String
Description: CIDR IP of VPN.
ECSDesiredCapacity:
Type: Number
Description: Number of instances to launch in your ECS cluster.
ECSMaxCapacity:
Type: Number
Description: Maximum number of instances that can be launched in your ECS cluster.
ECSContainerPort:
Type: Number
Description: Port Number of container created by the task.
ECSContainerLogsRetentionInDays:
Type: Number
Description: Number of days to retain the log events of container created by the task.
ECSTaskCpuMB:
Type: Number
Description: Number of cpu units used by the task.
ECSTaskMemory:
Type: Number
Description: Amount of memory used by the task.
TargetGroupHealthCheckPath:
Type: String
Description: Destination for health checks on the targets.
Mappings:
S3Config:
us-east-1:
BucketPrincipal: '127311923021'
us-east-2:
BucketPrincipal: '033677994240'
us-west-1:
BucketPrincipal: '027434742980'
us-west-2:
BucketPrincipal: '797873946194'
ca-central-1:
BucketPrincipal: '985666609251'
eu-west-1:
BucketPrincipal: '156460612806'
eu-central-1:
BucketPrincipal: '054676820928'
eu-west-2:
BucketPrincipal: '652711504416'
ap-northeast-1:
BucketPrincipal: '582318560864'
ap-northeast-2:
BucketPrincipal: '600734575887'
ap-southeast-1:
BucketPrincipal: '114774131450'
ap-southeast-2:
BucketPrincipal: '783225319266'
ap-south-1:
BucketPrincipal: '718504428378'
sa-east-1:
BucketPrincipal: '507241528517'
us-gov-west-1:
BucketPrincipal: '048591011584'
cn-north-1:
BucketPrincipal: '638102146993'
Resources:
###################################################
# IAM
###################################################
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: ecsTaskExecutionPolicy
PolicyDocument:
Statement:
Effect: Allow
Action:
- ecr:GetAuthorizationToken
- ecr:BatchCheckLayerAvailability
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
AutoScalingRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- application-autoscaling.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: ecsAutoScalingPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- application-autoscaling:*
- cloudwatch:DescribeAlarms
- cloudwatch:PutMetricAlarm
- ecs:DescribeServices
- ecs:UpdateService
Resource: '*'
###################################################
# SecurityGroup
###################################################
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
DependsOn: ALBSecurityGroup
Properties:
GroupDescription: ECS Security Group
VpcId: !Ref VpcId
ECSSecurityGroupHTTPinbound:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: !Sub ${ProjectName}-alb
GroupId: !Ref ECSSecurityGroup
IpProtocol: tcp
FromPort: !Ref ECSContainerPort
ToPort: !Ref ECSContainerPort
SourceSecurityGroupId: !Ref ALBSecurityGroup
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ECS Security Group
VpcId: !Ref VpcId
ALBSecurityGroupHTTPinbound:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: vpn ip
GroupId: !Ref ALBSecurityGroup
IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: !Ref ALBAllowCidrIp
###################################################
# ECS
###################################################
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${ProjectName}-cluster
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBHTTPListener
Properties:
ServiceName: !Sub ${ProjectName}-service
Cluster: !Ref ECSCluster
DesiredCount: !Ref ECSDesiredCapacity
LaunchType: FARGATE
LoadBalancers:
- ContainerName: !Ref ProjectName
ContainerPort: !Ref ECSContainerPort
TargetGroupArn: !Ref TargetGroup
TaskDefinition: !Ref ECSTaskDefinition
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSecurityGroup
Subnets: !Ref PrivateSubnetIds
ECSTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub ${ProjectName}
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: !Ref ProjectName
PortMappings:
- ContainerPort: !Ref ECSContainerPort
HostPort: !Ref ECSContainerPort
Protocol: tcp
Image:
Fn::ImportValue:
!Sub ${ProjectName}-ecr-repository-uri
Cpu: !Ref ECSTaskCpuMB
Memory: !Ref ECSTaskMemory
TaskRoleArn: !GetAtt ECSTaskExecutionRole.Arn
RequiresCompatibilities:
- FARGATE
NetworkMode: awsvpc
###################################################
# ALB
###################################################
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub ${ProjectName}-alb
Scheme: internet-facing
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: '30'
- Key: access_logs.s3.enabled
Value: 'true'
- Key: access_logs.s3.bucket
Value: !Sub ${ProjectName}-alb-logs
Subnets: !Ref PublicSubnetIds
SecurityGroups:
- !Ref ALBSecurityGroup
ALBHTTPListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
LoadBalancerArn: !Ref ALB
ALBHTTPListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
Conditions:
- Field: path-pattern
Values:
- '*'
ListenerArn: !Ref ALBHTTPListener
Priority: 1
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: ALB
Properties:
HealthCheckIntervalSeconds: 60
HealthCheckPath: !Ref TargetGroupHealthCheckPath
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Name: !Sub ${ProjectName}-tg
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
TargetType: ip
VpcId: !Ref VpcId
###################################################
# AutoScale
###################################################
AutoScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: !Ref ECSMaxCapacity
MinCapacity: 1
ResourceId:
Fn::Join:
- ''
- - service/
- !Ref ECSCluster
- /
- !GetAtt ECSService.Name
RoleARN: !GetAtt AutoScalingRole.Arn
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
AutoScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Sub ${ProjectName}-target-tracking-policy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref AutoScalingTarget
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
ScaleInCooldown: 300
ScaleOutCooldown: 300
TargetValue: 70
###################################################
# CloudWatch
###################################################
CloudwatchLogsGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /ecs/${ProjectName}
RetentionInDays: !Ref ECSContainerLogsRetentionInDays
###################################################
# S3
###################################################
ALBLogBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${ProjectName}-alb-logs
BucketPolicyELBLogBucket:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ALBLogBucket
PolicyDocument:
Id: !Sub ${ProjectName}-alb-logs-policy
Version: '2012-10-17'
Statement:
- Action:
- s3:PutObject
Effect: Allow
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- !Ref ALBLogBucket
- /AWSLogs/
- !Ref AWS::AccountId
- /*
Principal:
AWS:
Fn::FindInMap:
- S3Config
- !Ref AWS::Region
- BucketPrincipal
Route53 の設定
設定を追記
-
${ssl 証明書の arn}
,${Domain の hostzone ID}
,${Domain}
はご自身のものに書き換えてくださいparameters.ecs.json[ // 追記 { "ParameterKey": "ACMArn", "ParameterValue": "${ssl 証明書の arn}" }, { "ParameterKey": "HostZoneId", "ParameterValue": "${Domain の hostzone ID}" }, { "ParameterKey": "Domain", "ParameterValue": "${Domain}" } ]
template.ecs.ymlParameters: (中略) ACMArn: Type: String Description: Select Arn for ssl/tls certificate. HostZoneId: Type: AWS::Route53::HostedZone::Id Description: ID of the hosted zone. Domain: Type: String Description: Name of domain. (中略) Resources: (中略) ECSService: Type: AWS::ECS::Service DependsOn: - ALBHTTPListener - ALBHTTPSListener //追記 (中略) ALBSecurityGroupHTTPSinbound: Type: AWS::EC2::SecurityGroupIngress Properties: Description: vpn ip GroupId: !Ref ALBSecurityGroup IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: !Ref ALBAllowCidrIp ALBHTTPSListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: Port: 443 Protocol: HTTPS Certificates: - CertificateArn: !Ref ACMArn DefaultActions: - TargetGroupArn: !Ref TargetGroup Type: forward LoadBalancerArn: !Ref ALB ALBHTTPSListenerRule: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: Actions: - Type: forward TargetGroupArn: !Ref TargetGroup Conditions: - Field: path-pattern Values: - '*' ListenerArn: !Ref ALBHTTPSListener Priority: 1 DNSRecordSet: Type: AWS::Route53::RecordSetGroup Properties: HostedZoneId: !Ref HostZoneId RecordSets: - Name: !Ref Domain Type: A AliasTarget: HostedZoneId: !GetAtt ALB.CanonicalHostedZoneID DNSName: Fn::Join: - '.' - - dualstack - !GetAtt ALB.DNSName
Stack をアップデート
❯ aws cloudformation update-stack \
--profile cloudformation_sample \
--stack-name cloudformation-sample-ecs \
--capabilities CAPABILITY_IAM \
--template-body file://$(pwd)/template.ecs.yml \
--parameters file://$(pwd)/parameters.ecs.json
確認
指定したドメインにアクセスすると、nginx の画面が表示されればOK
参考
- https://qiita.com/yasuhiroki/items/8463eed1c78123313a6f
- https://dev.classmethod.jp/articles/cloudformation-fargate/
- https://qiita.com/tomokyu/items/d341ba1f4a1ad1149fe4
- https://note.com/dd_techblog/n/n109b59faa5f5
- https://aws.amazon.com/jp/blogs/devops/passing-parameters-to-cloudformation-stacks-with-the-aws-cli-and-powershell/