Published
- 10 min read
CloudFormationで、ECSのCI/CD環境を構築した際のハマりどころ 〜CodePipeline,CodeBuild,KMSも添えて〜
Classiアドベントカレンダー4日目です。
本日は、ECSを利用して、AWS上でAWSどっぷりのCI/CD環境を準備したときのお話になります。
今年のre:InventでEKSとFargateがリリースされましたが、東京リージョンに来てなかったり、プレビュー段階だったりで、
まだしばらくは参考になる部分はありそうかなと^^;
1.背景
- AWS公式ブログ
AWS CodePipeline, AWS CodeBuild, Amazon ECR, AWS CloudFormationを利用したAmazon ECSへの継続的デプロイメント
コンテナやサーバレスアプリのデプロイツールとしてのAWS CloudFormation - Developers.io
CodePipeline, CodeBuildを使ってAmazon ECSへの継続的デプロイメントを試してみた
などで、AWS公式でもECS環境下のCloudFormation(以下、CFn)を使ったデプロイ方法が紹介されています。
とはいえ、現実の要件でCFnで実装しようとすると、デフォルト設定だと失敗したり、
ドキュメントだけだと、GUIで設定できる部分がCFnでの書き方がわからかったりして、いくつかハマった内容があったので、
3種類ぐらいの特徴を抜粋して書いてみようと思います。
2.TL;DR
ECSを使うなら、
- ALBとECSの動的ポート機能を組み合わせる
- IAM Role,KMS,SSMパラメータストアを組み合わせる
- CodePipelineで複数リポジトリからのコード取得を行う
これらの機能を全部CFnでやろうとすると、一部aws-cliなどを使う必要がありますが、
ひとまずDevとOpsでうまく権限を分担したCI/CD環境を構築できるのではないかなと思います。
3.特徴解説
3-1. ALBとECSの動的ポート機能の組み合わせ
EC2へ割り当てるSecurityGroupは、ECSの動的ポート機能を利用するため、インバウンドのTCPポートを開放しておきます。
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref VpcId
GroupName: sample
GroupDescription: "ALB Serurity Group"
SecurityGroupIngress:
-
CidrIp: 0.0.0.0/0
IpProtocol: tcp
FromPort: 443
ToPort: 443
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref VpcId
GroupName: sample
GroupDescription: "EC2 Serurity Group"
SecurityGroupIngress:
-
SourceSecurityGroupId: !Ref ALBSecurityGroup
IpProtocol: tcp
FromPort: 0
ToPort: 65535
ECSの動的ポートを有効にするため、PortMappingsの設定でホストのポートを0に設定します。
ECSTask:
Type: 'AWS::ECS::TaskDefinition'
Properties:
Family: sample
NetworkMode: bridge
ContainerDefinitions:
- Name: sample
Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}:${ImageTag}'
Cpu: 2
Memory: 128
PortMappings:
- ContainerPort: 80
HostPort: 0
Essential: true
Ulimits:
- Name: nofile
SoftLimit: 65535
HardLimit: 65535
Environment:
- Name: TZ
Value: Asia/Tokyo
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: sample
awslogs-region: !Sub ${AWS::Region}
awslogs-stream-prefix: !Ref ImageTag
Service:
Type: 'AWS::ECS::Service'
Properties:
ServiceName: sample
Cluster: !Ref ECSCluster
DesiredCount: 1
TaskDefinition: !Ref ECSTask
Role: !Ref ECSServiceRole
PlacementStrategies:
- Type: spread
Field: instanceId
LoadBalancers:
- ContainerName: sample
ContainerPort: 80
TargetGroupArn: !Ref ALBTargetGroup
注意点
複数のEC2でECSを運用するのであれば、PlacementStrategiesの設定を行っておかないと、
random配置ECSのタスクが一つのホストだけに偏ってしまったりすることがあります。
3-2. DevとOpsで別gitリポジトリを運用しつつ、CodePipelineのデプロイフェーズでCFnのChangeSetを使う
デプロイにCFnを利用することで、デプロイの実行記録の管理やCFnで記載された部分のインフラ部分のテストを行いつつ、デプロイをすることが可能になります。
また、Sourceフェーズで、CFnの内容やEC2のASGやAMI設定の管理を行うOps管轄リポジトリと、Dockerコンテナ化するアプリロジックが含まれているDev管轄リポジトリを分割することで、運用フェーズに入ったときにDevとOpsで独立して、デプロイを行うことができます。
CodePipeline:
Type: "AWS::CodePipeline::Pipeline"
Properties:
Name: sample
ArtifactStore:
Type: S3
Location: sample
RoleArn: !Ref BuildRole
Stages:
-
Name: Source
Actions:
-
Name: AppSource
RunOrder: 1
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
Configuration:
Owner: !Ref GithubOwner
Repo: !Ref GithubAppRepo
Branch: !Ref GithubAppBranch
OAuthToken: !Ref GithubToken
OutputArtifacts:
- Name: AppSource
-
Name: InfraSource
RunOrder: 1
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
Configuration:
Owner: !Ref GithubOwner
Repo: !Ref GithubInfraRepo
Branch: !Ref GithubInfraBranch
OAuthToken: !Ref GithubToken
OutputArtifacts:
- Name: InfraSource
-
Name: Build
Actions:
-
Name: CodeBuild
RunOrder: 1
InputArtifacts:
- Name: AppSource
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuild
OutputArtifacts:
- Name: Build
-
Name: CreateChangeSet
Actions:
-
Name: CreateChangeSet
RunOrder: 1
InputArtifacts:
- Name: InfraSource
- Name: Build
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
Configuration:
ChangeSetName: Deploy
ActionMode: CHANGE_SET_REPLACE
StackName: !Sub ${AWS::StackName}
Capabilities: CAPABILITY_NAMED_IAM
TemplatePath: !Sub "Source::sample.yml"
ChangeSetName: !Ref CFnChangeSetName
RoleArn: !Ref BuildRole
ParameterOverrides: !Sub |
{
"ImageTag": { "Fn::GetParam" : [ "Build", "build.json", "tag" ] },
"AppName": "${AppName}",
"OwnerName": "${OwnerName}",
"RoleName": "${RoleName}",
"StageName": "${StageName}",
"VpcId": "${VpcId}"
}
-
Name: Deploy
Actions:
-
Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
Configuration:
ActionMode: CHANGE_SET_EXECUTE
ChangeSetName: !Ref CFnChangeSetName
RoleArn: !Ref BuildRole
StackName: !Sub ${AWS::StackName}
注意点
- CodePipelineのキックは、PRがマージされたタイミングなので、(一応、CodePipelineにはTestフェーズもあるが)マージ前のテストなどはCircleCIとかに任せた方がよいかも
- ParameterOverridesで上書きするパラメータは、CFnのParametersに設定している項目に応じて設定する
- Sourceフェーズで持ってこれるリポジトリは2つまで。コンテナビルドに持ってくるのがもっとある場合、CodeBuild内でこちらの記事のように、githubから引っ張ってきて、ビルドするなどの対応が必要になりそう
3-3. CodeBuildでDockerイメージを作る際、KMSとSSMパラメータストアを利用する
このあたりはAWSの恩恵をフルに受けている部分かなと。
RDSのパスワードや秘密鍵など、gitリポジトリ内で管理したくない情報は、SSMパラメータストアを使って、Dockerイメージを作成するときに環境変数を埋め込みます。
CodeBuild:
Type: AWS::CodeBuild::Project
Properties:
Name: sample
Source:
Type: CODEPIPELINE
ServiceRole: !Ref BuildRole
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: 'aws/codebuild/docker:1.12.1'
EnvironmentVariables:
- Name: AWS_DEFAULT_REGION
Value: !Sub ${AWS::Region}
- Name: AWS_ACCOUNT_ID
Value: !Sub ${AWS::AccountId}
- Name: IMAGE_REPO_NAME
Value: !Ref ECRRepoName
docker buildするときに、—build-argに秘匿情報として環境変数を引き渡し、できあがったイメージをECRにpushする。
version: 0.2
phases:
pre_build:
commands:
- $(aws ecr get-login --region $AWS_DEFAULT_REGION)
- IMAGE_TAG="${CODEBUILD_RESOLVED_SOURCE_VERSION}"
- DB_PASSWORD=$(aws ssm get-parameters --names rds_pass --with-decryption --query "Parameters[0].Value" --output text)
build:
commands:
- docker build --build-arg DB_PASSWORD="${DB_PASSWORD}" -t "${IMAGE_REPO_NAME}:${IMAGE_TAG}" .
- docker tag "${IMAGE_REPO_NAME}:${IMAGE_TAG}" "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
post_build:
commands:
- docker push "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
- printf '{"tag":"%s"}' "${IMAGE_TAG}" > build.json
artifacts:
files:
- build.json
discard-paths: yes
(snip)
ARG DB_PASSWORD
ENV DB_PASSWORD=${DB_PASSWORD}
(snip)
実運用する際は、IAM Roleを使う権限も意識して、KMSのKeyを利用するIAM UserやIAM Roleを設定する。
KMSKey:
Type: 'AWS::KMS::Key'
Properties:
Description: sample-key
KeyPolicy:
Version: '2012-10-17'
Id: 'key-default-1'
Statement:
- Sid: 'Allow use of the key'
Effect: 'Allow'
Principal:
AWS: !GetAtt BuildRole.Arn
Action:
- 'kms:DescribeKey'
- 'kms:Decrypt'
Resource: '*'
注意点
- SSMパラメータにおける、SecureString型の値登録
3-3.でSSMパラメータストアで暗号化する際、SecureString型はCFnに対応していない。
そのため、aws-cliで設定することにした。TerraformはSecureString型に対応しているので、CFn側でも対応して欲しいところ…
$aws ssm put-parameter --name rds-pass --value PASSWORD --type SecureString --key-id hogehoge
4. その他の雑多なハマりどころ
4-1. ECSのAMIのデフォルト設定
- EBSのストレージタイプのデフォルトがHDD
LaunchConfigurationのBlockDeviceMappingsで、gp2を明示的に指定してあげる。 - WillReplace用のシグナルを送るcfn-signalが未インストール
UserDataの中で記載しておく。シグナルを送るタイミングは、どこまでAMIに手を入れるかによって変更する。
LaunchConfig:
Type: 'AWS::AutoScaling::LaunchConfiguration'
Properties:
AssociatePublicIpAddress: true
KeyName: sample
IamInstanceProfile: sample
ImageId: ami-e4657283
SecurityGroups:
- !Ref SecurityGroup
InstanceType: t2.micro
BlockDeviceMappings:
- DeviceName: '/dev/xvda'
Ebs:
VolumeType: gp2
VolumeSize: 30
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo ECS_CLUSTER=${ECSClusterName} >> /etc/ecs/ecs.config
sudo yum install -y aws-cfn-bootstrap
sleep 60
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource AutoScalingGroup --region ${AWS::Region}
AutoScalingGroup:
Type: 'AWS::AutoScaling::AutoScalingGroup'
Properties:
LaunchConfigurationName: sample
DesiredCapacity: 2
MaxSize: 3
MinSize: 2
VPCZoneIdentifier:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
CreationPolicy:
ResourceSignal:
Count: 1
Timeout: PT5M
UpdatePolicy:
AutoScalingReplacingUpdate:
WillReplace: true
5.まとめ
もう少しきれいな書き方がありそうだけど、実運用でよくある要件の参考程度になれば幸いです。
EC2のASGまわりの設定は、従来のECSだとこのような形で大分インフラ側を意識しないといけない構成です。
今後、re:Inventで発表されたEKSやFargateなどとも比べながら、本環境をアップデートしていければよいなと思います。