CloudFormation 一撃で Fargate の Blue/Green Deployment 環境を構築する
はじめに
おはようございます、もきゅりんです。
皆さん、Blue/Green デプロイメントしてますか?
Blue/Green デプロイメントとは、はすでに理解されていることを前提として話を進めます。
本稿は、CloudFormationとAWS CLIでFargateのBlue/Green Deployment環境を構築する #Fargate | DevelopersIO の内容をカスタムリソースを利用して CloudFormation 一撃で構築するものになります。
構成図は以下です。
プレースホルダを用いて以下のBlue/Green デプロイメントを実現します。
プレースホルダ
とは?という方は、 チュートリアル: ソースと ECS と CodeDeploy 間のデプロイでパイプラインを作成するAmazon ECR - AWS CodePipeline をご参照下さい。
なお、本稿の別アプローチで AWS CloudFormation を使用して CodeDeploy による ECS ブルー/グリーンデプロイを実行する - AWS CloudFormation の方法でも CloudFormation 一撃で ECS の Blue/Green デプロイメント環境を構築することもできます。
これらの違いについては、[2パターン] CFn で Fargate の Blue/Green Deployment の CodePipeline を構築するで説明していますので、興味がある方はご参照下さい。
カスタムリソースとは
カスタムリソースとは?という人は カスタムリソース - AWS CloudFormation をご参照下さい。
カスタムリソースを使用すると、テンプレートにカスタムのプロビジョニングロジックを記述し、ユーザーがスタックを作成、更新(カスタムリソースを変更した場合)、削除するたびに AWS CloudFormation がそれを実行します。たとえば、AWS CloudFormation のリソースタイプとして使用できないリソースを含める必要があるとします。それらのリソースは、カスタム リソースを使用して含めることができます。この方法により、すべての関連リソースを 1 つのスタックで管理できます。
本稿では、AWS Lambda-backed カスタムリソース - AWS CloudFormation を使って、AWS CloudFormation とLambda 関数との組み合わせで環境を構築します。
なんでそんなことわざわざするの?
現状(2021/3/10)、CloudFormation による CodeDeploy の ECS Blue/Green Deployment Group 作成は非対応のためです。
CloudFormation と AWS CLI で作れますが、どうせなら CFn 一撃で作りたいよね、ということで作った次第です。
以下の図でいうと、赤枠の部分を Lambda のカスタムリソースが作成します。
前提条件
ベースは CloudformationでFargateを構築する | DevelopersIO のブログなので、こちらと同様です。
- 利用予定のアカウントで、AWS CLIが利用可能
- VPC、ALB Security Group、ECS Task Security Groupが構築済み
- Fargateを配置するサブネットは、NATゲートウェイ等経由でインターネット通信可能であること
ついでだったのでALBログS3バケットはリソースに加えています。
Cloudformationテンプレート
注記
Lambda 関数は Python で記載しているので、必要に応じて、CodeDeploy — Boto3 Docs 1.17.23 documentation を確認の上、パラメータを微調整してご利用頂ければと思います。
CodeDeployのデプロイメントグループのデプロイ設定は下記です。
- トラフィックを再ルーティングさせるのは5分後です。
- デプロイ設定は
CodeDeployDefault.ECSLinear10PercentEvery1Minutes
です。 - 元のリビジョンの終了は30分後です。
- デプロイが失敗したらロールバックします。
sample.yml
AWSTemplateFormatVersion: '2010-09-09' Description: Fargate and ALB and CodeDeploy for Blue/Green Create. Metadata: 'AWS::CloudFormation::Interface': ParameterGroups: - Label: default: 'Project Name Prefix' Parameters: - ProjectName - Label: default: 'InternetALB Configuration' Parameters: - InternetALBName - TargetGroupName1 - TargetGroupName2 - Label: default: 'Fargate for ECS Configuration' Parameters: - ECSClusterName - ECSTaskName - ECSTaskCPUUnit - ECSTaskMemory - ECSContainerName - ECSImageName - ECSServiceName - ECSTaskDesiredCount - Label: default: 'Netowork Configuration' Parameters: - VpcId - ALBSecurityGroupId - ALBSubnetId1 - ALBSubnetId2 - ECSSecurityGroupId - ECSSubnetId1 - ECSSubnetId2 - Label: default: 'Scaling Configuration' Parameters: - ServiceScaleEvaluationPeriods - ServiceCpuScaleOutThreshold - ServiceCpuScaleInThreshold - TaskMinContainerCount - TaskMaxContainerCount - Label: default: 'CodeDeploy Configuration' Parameters: - CodeDeployAppName - CodeDeployDeploymentGroupName ParameterLabels: InternetALBName: default: 'InternetALBName' TargetGroupName1: default: 'TargetGroupName1' TargetGroupName2: default: 'TargetGroupName2' ECSClusterName: default: 'ECSClusterName' ECSTaskName: default: 'ECSTaskName' ECSTaskCPUUnit: default: 'ECSTaskCPUUnit' ECSTaskMemory: default: 'ECSTaskMemory' ECSContainerName: default: 'ECSContainerName' ECSImageName: default: 'ECSImageName' ECSServiceName: default: 'ECSServiceName' ECSTaskDesiredCount: default: 'ECSTaskDesiredCount' CodeDeployAppName: default: 'CodeDeployAppName' CodeDeployDeploymentGroupName: default: 'CodeDeployDeploymentGroupName' # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: ProjectName: Default: sample-fargate Type: String #VPCID VpcId: Description: 'VPC ID' Type: AWS::EC2::VPC::Id #ALBSecurity Group ALBSecurityGroupId: Type: AWS::EC2::SecurityGroup::Id #ALBSubnet1 ALBSubnetId1: Description: 'ALB Subnet 1st' Type: AWS::EC2::Subnet::Id #ALBSubnet2 ALBSubnetId2: Description: 'ALB Subnet 2st' Type: AWS::EC2::Subnet::Id #ECSSecurity Group ECSSecurityGroupId: Type: AWS::EC2::SecurityGroup::Id #ECSSubnet1 ECSSubnetId1: Description: 'ECS Subnet 1st' Type: AWS::EC2::Subnet::Id #ECSSubnet2 ECSSubnetId2: Description: 'ECS Subnet 2st' Type: AWS::EC2::Subnet::Id #InternetALB InternetALBName: Type: String Default: 'alb' #TargetGroupName1 TargetGroupName1: Type: String Default: 'tg1' #TargetGroupName2 TargetGroupName2: Type: String Default: 'tg2' #ECSClusterName ECSClusterName: Type: String Default: 'cluster' #ECSTaskName ECSTaskName: Type: String Default: 'task' #ECSTaskCPUUnit ECSTaskCPUUnit: AllowedValues: [256, 512, 1024, 2048, 4096] Type: String Default: 256 #ECSTaskMemory ECSTaskMemory: AllowedValues: [256, 512, 1024, 2048, 4096] Type: String Default: 512 #ECSContainerName ECSContainerName: Type: String Default: 'container' #ECSImageName ECSImageName: Type: String Default: 'nginxdemos/hello:latest' #ECSServiceName ECSServiceName: Type: String Default: 'service' CodeDeployAppName: Type: String Default: 'app' CodeDeployDeploymentGroupName: Type: String Default: 'dg' #ECSTaskDesiredCount ECSTaskDesiredCount: Type: Number Default: 1 # Scaling params ServiceScaleEvaluationPeriods: Description: The number of periods over which data is compared to the specified threshold Type: Number Default: 2 MinValue: 2 ServiceCpuScaleOutThreshold: Type: Number Description: Average CPU value to trigger auto scaling out Default: 50 MinValue: 0 MaxValue: 100 ConstraintDescription: Value must be between 0 and 100 ServiceCpuScaleInThreshold: Type: Number Description: Average CPU value to trigger auto scaling in Default: 25 MinValue: 0 MaxValue: 100 ConstraintDescription: Value must be between 0 and 100 TaskMinContainerCount: Type: Number Description: Minimum number of containers to run for the service Default: 1 MinValue: 1 ConstraintDescription: Value must be at least one TaskMaxContainerCount: Type: Number Description: Maximum number of containers to run for the service when auto scaling out Default: 2 MinValue: 1 ConstraintDescription: Value must be at least one Resources: # ------------------------------------------------------------# # Target Group # ------------------------------------------------------------# TargetGroup1: Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' Properties: VpcId: !Ref VpcId Name: !Sub '${ProjectName}-${TargetGroupName1}' Protocol: HTTP Port: 80 TargetType: ip TargetGroup2: Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' Properties: VpcId: !Ref VpcId Name: !Sub '${ProjectName}-${TargetGroupName2}' Protocol: HTTP Port: 80 TargetType: ip # ------------------------------------------------------------# # Internet ALB # ------------------------------------------------------------# InternetALB: Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' Properties: Name: !Sub '${ProjectName}-${InternetALBName}' Tags: - Key: Name Value: !Sub '${ProjectName}-${InternetALBName}' Scheme: 'internet-facing' LoadBalancerAttributes: - Key: 'deletion_protection.enabled' Value: 'false' - Key: 'idle_timeout.timeout_seconds' Value: '60' - Key: 'access_logs.s3.enabled' Value: 'true' - Key: 'access_logs.s3.bucket' Value: !Sub 'alb-log-${AWS::AccountId}' SecurityGroups: - !Ref ALBSecurityGroupId Subnets: - !Ref ALBSubnetId1 - !Ref ALBSubnetId2 ALBListener1: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: - TargetGroupArn: !Ref TargetGroup1 Type: forward LoadBalancerArn: !Ref InternetALB Port: 80 Protocol: HTTP ALBListener2: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: - TargetGroupArn: !Ref TargetGroup2 Type: forward LoadBalancerArn: !Ref InternetALB Port: 8080 Protocol: HTTP # ------------------------------------------------------------# # ECS Cluster # ------------------------------------------------------------# ECSCluster: Type: 'AWS::ECS::Cluster' Properties: ClusterName: !Sub '${ProjectName}-${ECSClusterName}' # ------------------------------------------------------------# # ECS LogGroup # ------------------------------------------------------------# ECSLogGroup: Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Sub '/ecs/logs/${ProjectName}-ecs-group' # ------------------------------------------------------------# # ECS Task Execution Role # ------------------------------------------------------------# ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub '${ProjectName}-ECSTaskExecutionRolePolicy' Path: / 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 # ------------------------------------------------------------# # ECS TaskDefinition # ------------------------------------------------------------# ECSTaskDefinition: Type: 'AWS::ECS::TaskDefinition' Properties: Cpu: !Ref ECSTaskCPUUnit ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn Family: !Sub '${ProjectName}-${ECSTaskName}' Memory: !Ref ECSTaskMemory NetworkMode: awsvpc RequiresCompatibilities: - FARGATE #ContainerDefinitions ContainerDefinitions: - Name: !Sub '${ProjectName}-${ECSContainerName}' Image: !Ref ECSImageName LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref ECSLogGroup awslogs-region: !Ref 'AWS::Region' awslogs-stream-prefix: !Ref ProjectName MemoryReservation: 128 PortMappings: - HostPort: 80 Protocol: tcp ContainerPort: 80 # ------------------------------------------------------------# # ECS Service # ------------------------------------------------------------# ECSService: Type: AWS::ECS::Service DependsOn: ALBListener1 Properties: Cluster: !Ref ECSCluster DesiredCount: !Ref ECSTaskDesiredCount DeploymentController: Type: CODE_DEPLOY LaunchType: FARGATE LoadBalancers: - TargetGroupArn: !Ref TargetGroup1 ContainerPort: 80 ContainerName: !Sub '${ProjectName}-${ECSContainerName}' NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - !Ref ECSSecurityGroupId Subnets: - !Ref ECSSubnetId1 - !Ref ECSSubnetId2 ServiceName: !Sub '${ProjectName}-${ECSServiceName}' TaskDefinition: !Ref ECSTaskDefinition # ------------------------------------------------------------# # Auto Scaling Service # ------------------------------------------------------------# ServiceAutoScalingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: application-autoscaling.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: !Sub '${ProjectName}-${ECSContainerName}-autoscaling' PolicyDocument: Statement: - Effect: Allow Action: - application-autoscaling:* - cloudwatch:DescribeAlarms - cloudwatch:PutMetricAlarm - ecs:DescribeServices - ecs:UpdateService Resource: '*' ServiceScalingTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref TaskMinContainerCount MaxCapacity: !Ref TaskMaxContainerCount ResourceId: !Sub - service/${EcsClusterName}/${EcsDefaultServiceName} - EcsClusterName: !Ref ECSCluster EcsDefaultServiceName: !Sub '${ProjectName}-${ECSServiceName}' RoleARN: !GetAtt ServiceAutoScalingRole.Arn ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs DependsOn: - ECSService ServiceScaleOutPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Sub '${ProjectName}-${ECSServiceName}-ScaleOutPolicy' PolicyType: StepScaling ScalingTargetId: !Ref ServiceScalingTarget StepScalingPolicyConfiguration: AdjustmentType: ChangeInCapacity Cooldown: 60 MetricAggregationType: Average StepAdjustments: - ScalingAdjustment: 1 MetricIntervalLowerBound: 0 ServiceScaleInPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Sub '${ProjectName}-${ECSServiceName}-ScaleInPolicy' PolicyType: StepScaling ScalingTargetId: !Ref ServiceScalingTarget StepScalingPolicyConfiguration: AdjustmentType: ChangeInCapacity Cooldown: 60 MetricAggregationType: Average StepAdjustments: - ScalingAdjustment: -1 MetricIntervalUpperBound: 0 ServiceScaleOutAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: !Sub '${ProjectName}-${ECSServiceName}-ScaleOutAlarm' EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods Statistic: Average TreatMissingData: notBreaching Threshold: !Ref ServiceCpuScaleOutThreshold AlarmDescription: Alarm to add capacity if CPU is high Period: 60 AlarmActions: - !Ref ServiceScaleOutPolicy Namespace: AWS/ECS Dimensions: - Name: ClusterName Value: !Ref ECSCluster - Name: ServiceName Value: !Sub '${ProjectName}-${ECSServiceName}' ComparisonOperator: GreaterThanThreshold MetricName: CPUUtilization DependsOn: - ECSService ServiceScaleInAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: !Sub '${ProjectName}-${ECSServiceName}-ScaleInAlarm' EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods Statistic: Average TreatMissingData: notBreaching Threshold: !Ref ServiceCpuScaleInThreshold AlarmDescription: Alarm to reduce capacity if container CPU is low Period: 300 AlarmActions: - !Ref ServiceScaleInPolicy Namespace: AWS/ECS Dimensions: - Name: ClusterName Value: !Ref ECSCluster - Name: ServiceName Value: !Sub '${ProjectName}-${ECSServiceName}' ComparisonOperator: LessThanThreshold MetricName: CPUUtilization DependsOn: - ECSService # ------------------------------------------------------------# # BlueGreen CodeDeploy Role # ------------------------------------------------------------# CodeDeployRole: Type: AWS::IAM::Role Properties: RoleName: !Sub '${ProjectName}-CodeDeployRole' Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codedeploy.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaFunction: Type: 'AWS::Lambda::Function' DeletionPolicy: 'Delete' Properties: Code: ZipFile: | import boto3 import json import logging import cfnresponse from botocore.exceptions import ClientError logger = logging.getLogger() logger.setLevel(logging.INFO) client = boto3.client('codedeploy') def lambda_handler(event, context): appName = event['ResourceProperties']['appName'] deploymentGroup = event['ResourceProperties']['deploymentGroup'] clusterName = event['ResourceProperties']['ECSClusterName'] serviceName = event['ResourceProperties']['ECSServiceName'] print('REQUEST RECEIVED:\n' + json.dumps(event)) responseData = {} try: res = client.create_application( applicationName=appName, computePlatform='ECS' ) logger.info(res) logger.info("SUCCESS: CodeDeploy Application created.") res = client.create_deployment_group( applicationName=appName, deploymentGroupName=deploymentGroup, deploymentConfigName='CodeDeployDefault.ECSLinear10PercentEvery1Minutes', serviceRoleArn=event['ResourceProperties']['CodeDeployServiceRoleArn'], autoRollbackConfiguration={ 'enabled': True, 'events': [ 'DEPLOYMENT_FAILURE', ] }, deploymentStyle={ 'deploymentType': 'BLUE_GREEN', 'deploymentOption': 'WITH_TRAFFIC_CONTROL' }, blueGreenDeploymentConfiguration={ 'terminateBlueInstancesOnDeploymentSuccess': { 'action': 'TERMINATE', 'terminationWaitTimeInMinutes': 30 }, 'deploymentReadyOption': { 'actionOnTimeout': 'STOP_DEPLOYMENT', 'waitTimeInMinutes': 5 } }, loadBalancerInfo={ 'targetGroupPairInfoList': [ { 'targetGroups': [ { 'name': event['ResourceProperties']['TargetGroup1'] }, { 'name': event['ResourceProperties']['TargetGroup2'] }, ], 'prodTrafficRoute': { 'listenerArns': [ event['ResourceProperties']['ALBListener1'], ] }, 'testTrafficRoute': { 'listenerArns': [ event['ResourceProperties']['ALBListener2'], ] } }, ] }, ecsServices=[ { 'serviceName': serviceName, 'clusterName': clusterName }, ] ) except ClientError as e: logger.error("ERROR: Something error!") logger.error(e) responseData = {'error': str(e)} cfnresponse.send(event, context, cfnresponse.FAILED, responseData) else: logger.info(res) logger.info( "SUCCESS: CodeDeploy Application and DeploymentGroup created.") return cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) Handler: index.lambda_handler Role: !GetAtt LambdaRole.Arn Runtime: python3.8 Timeout: 10 # ------------------------------------------------------------# # Custom Resource # ------------------------------------------------------------# CreateCodeDeploy: Type: Custom::CreateCodeDeploy DependsOn: - ECSService Properties: ServiceToken: !GetAtt LambdaFunction.Arn Region: !Ref AWS::Region ECSClusterName: !Sub '${ProjectName}-${ECSClusterName}' ECSServiceName: !Sub '${ProjectName}-${ECSServiceName}' CodeDeployServiceRoleArn: !GetAtt CodeDeployRole.Arn TargetGroup1: !Sub '${ProjectName}-${TargetGroupName1}' TargetGroup2: !Sub '${ProjectName}-${TargetGroupName2}' ALBListener1: !Ref ALBListener1 ALBListener2: !Ref ALBListener2 appName: !Sub '${ProjectName}-${ECSClusterName}-${ECSServiceName}-${CodeDeployAppName}' deploymentGroup: !Sub '${ProjectName}-${ECSClusterName}-${ECSServiceName}-${CodeDeployDeploymentGroupName}' # ------------------------------------------------------------# # IAMRole For CustomResource Lambda # ------------------------------------------------------------# LambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSCodeDeployFullAccess LambdaPolicy: Type: AWS::IAM::Policy Properties: PolicyName: LambdaPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:* - logs:* Resource: '*' - Effect: Allow Resource: '*' Action: - iam:PassRole Condition: StringEqualsIfExists: iam:PassedToService: - codedeploy.amazonaws.com Roles: - !Ref LambdaRole # ------------------------------------------------------------# # Log Bucket # ------------------------------------------------------------# logsBacket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'alb-log-${AWS::AccountId}' AccessControl: LogDeliveryWrite BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: TRUE BlockPublicPolicy: TRUE IgnorePublicAcls: TRUE RestrictPublicBuckets: TRUE LifecycleConfiguration: Rules: - Id: 'Delete-After-400days' Status: Enabled ExpirationInDays: 400 logsBucketPolicy: Type: AWS::S3::BucketPolicy DependsOn: logsBacket Properties: Bucket: !Sub 'alb-log-${AWS::AccountId}' PolicyDocument: Statement: - Action: - 's3:PutObject' Effect: 'Allow' Resource: - Fn::Join: - '' - - 'arn:aws:s3:::' - !Sub 'alb-log-${AWS::AccountId}' - '/*' Principal: AWS: '582318560864' - Action: - 's3:PutObject' Effect: 'Allow' Resource: - Fn::Join: - '' - - 'arn:aws:s3:::' - !Sub 'alb-log-${AWS::AccountId}' - '/*' Principal: Service: 'delivery.logs.amazonaws.com' Condition: StringEquals: 's3:x-amz-acl': - 'bucket-owner-full-control' - Action: - 's3:GetBucketAcl' Effect: 'Allow' Resource: Fn::Join: - '' - - 'arn:aws:s3:::' - !Sub 'alb-log-${AWS::AccountId}' Principal: Service: 'delivery.logs.amazonaws.com'
CodePipeline 構築を合わせてどうぞ
[2パターン] CFn で Fargate の Blue/Green Deployment の CodePipeline を構築する では、CodeCommit を利用した プレースホルダを利用した ECS Blue/Green の CodePipeline を CloudFormation で構築しています。
(Fargateを構築するパラメータと多少異なるので調整が必要です。)
CodeCommit に必要な Dockerfile(+その他) , appspec.yaml , taskdef.jsonを格納して実行します。
手動承認ステージはコメントアウトしていますので、利用する場合は必要な設定を確認の上、ご利用下さい。
codepipeline.yml
AWSTemplateFormatVersion: 2010-09-09 Description: CodePipeline For ECS Fargate Blue/Green Deploy with PlaceHolder # ------------------------------------------------------------# # Parameters # ------------------------------------------------------------# Parameters: NameTagPrefix: Type: String Default: test Description: Prefix of Name tags. ServiceName: Type: String Default: myapp Description: Prefix of Service tags. CodeCommitRepositoryName: Type: String CodeDeployAppName: Type: String CodeDeployDGName: Type: String ContainerName: Type: String ECRName: Type: String # ------------------------------------------------------------# # Parameters # ------------------------------------------------------------# Resources: # ------------------------------------------------------------# # IAM Roles # ------------------------------------------------------------# # CodeWatchEventを実行できるIAMRole CloudwatchEventRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CloudWatchEventRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: CloudWatchEventsPipelineExecution PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: codepipeline:StartPipelineExecution Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline} # CodeBuildに適用するIAMRole CodeBuildServiceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodeBuildServiceNameRole Path: / AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: SampleCodeBuildAccess PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Resource: '*' Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Effect: Allow Resource: !Sub arn:aws:s3:::${ArtifactBucket}/* Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketAcl - s3:GetBucketLocation - Effect: Allow Action: - codebuild:CreateReportGroup - codebuild:CreateReport - codebuild:UpdateReport - codebuild:BatchPutTestCases - codebuild:BatchPutCodeCoverages Resource: '*' - Effect: Allow Action: - ecr:GetAuthorizationToken - ecr:BatchCheckLayerAvailability - ecr:GetDownloadUrlForLayer - ecr:GetRepositoryPolicy - ecr:DescribeRepositories - ecr:ListImages - ecr:DescribeImages - ecr:BatchGetImage - ecr:InitiateLayerUpload - ecr:UploadLayerPart - ecr:CompleteLayerUpload - ecr:PutImage Resource: '*' # CodePipelineに適用するIAMRole CodePipelineServiceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodePipelineServiceRole Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: SamplePipeline PolicyDocument: Version: 2012-10-17 Statement: - Action: - iam:PassRole Resource: '*' Effect: Allow Condition: StringEqualsIfExists: iam:PassedToService: - ecs-tasks.amazonaws.com - Resource: - !Sub arn:aws:s3:::${ArtifactBucket}/* Effect: Allow Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning - Action: - codecommit:CancelUploadArchive - codecommit:GetBranch - codecommit:GetCommit - codecommit:GetRepository - codecommit:GetUploadArchiveStatus - codecommit:UploadArchive Resource: '*' Effect: Allow - Action: - codedeploy:CreateDeployment - codedeploy:GetApplication - codedeploy:GetApplicationRevision - codedeploy:GetDeployment - codedeploy:GetDeploymentConfig - codedeploy:RegisterApplicationRevision - codedeploy:* Resource: '*' Effect: Allow - Action: - elasticbeanstalk:* - ec2:* - elasticloadbalancing:* - autoscaling:* - cloudwatch:* - sns:* - cloudformation:* - rds:* - sqs:* - ecs:* Resource: '*' Effect: Allow - Action: - codebuild:BatchGetBuilds - codebuild:StartBuild - codebuild:BatchGetBuildBatches - codebuild:StartBuildBatch Resource: '*' Effect: Allow # S3Bucket ArtifactBucket: Type: AWS::S3::Bucket Properties: PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True # CloudWatchEventの実行ルール AmazonCloudWatchEventRule: Type: AWS::Events::Rule Properties: EventPattern: source: - aws.codecommit detail-type: - CodeCommit Repository State Change resources: - Fn::Join: - '' - - 'arn:aws:codecommit:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - ':' - !Ref CodeCommitRepositoryName detail: event: - referenceCreated - referenceUpdated referenceType: - branch referenceName: - master Targets: - Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline} RoleArn: !GetAtt CloudwatchEventRole.Arn Id: codepipeline-AppPipeline # CodeBuild CodeBuildProject: Type: AWS::CodeBuild::Project Properties: ServiceRole: !Ref CodeBuildServiceRole Artifacts: Type: CODEPIPELINE Source: Type: CODEPIPELINE BuildSpec: | version: 0.2 phases: pre_build: commands: - echo Logging in to Amazon ECR... - $(aws ecr get-login --no-include-email) - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) build: commands: - echo Build started on `date` - echo Building the Docker image... - docker build -t $REPOSITORY_URI:$IMAGE_TAG . - docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG post_build: commands: - echo Build completed on `date` - echo Pushing the Docker images... - docker push $REPOSITORY_URI:$IMAGE_TAG - echo Writing imageDetail json... - echo "{\"name\":\"${ContainerName}\",\"ImageURI\":\"${REPOSITORY_URI}:${IMAGE_TAG}\"}" > imageDetail.json artifacts: files: imageDetail.json Environment: PrivilegedMode: true ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/standard:4.0 Type: LINUX_CONTAINER EnvironmentVariables: - Name: AWS_DEFAULT_REGION Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName} - Name: ContainerName Value: !Ref ContainerName - Name: DOCKER_BUILDKIT Value: '1' Name: !Ref AWS::StackName # ------------------------------------------------------------# # CodePipeline # ------------------------------------------------------------# Pipeline: Type: AWS::CodePipeline::Pipeline Properties: RoleArn: !GetAtt CodePipelineServiceRole.Arn Name: !Sub ${NameTagPrefix}-${ServiceName}-pipeline ArtifactStore: Type: S3 Location: !Ref ArtifactBucket Stages: - Name: Source Actions: - Name: SourceAction ActionTypeId: Category: Source Owner: AWS Version: '1' Provider: CodeCommit Configuration: RepositoryName: !Ref CodeCommitRepositoryName PollForSourceChanges: false BranchName: master RunOrder: 1 OutputArtifacts: - Name: App - Name: Build Actions: - Name: Build ActionTypeId: Category: Build Owner: AWS Version: '1' Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildProject RunOrder: 1 InputArtifacts: - Name: App OutputArtifacts: - Name: BuildOutput # - Name: Approval # Actions: # - Name: Manual_Approval # ActionTypeId: # Category: Approval # Owner: AWS # Version: '1' # Provider: Manual # Configuration: # CustomData: !Sub '${ServiceName} will be updated. Do you want to deploy it?' # NotificationArn: arn:aws:sns:ap-northeast-1:xxxxxxxx:hogehoge - Name: Deploy Actions: - Name: Deploy ActionTypeId: Category: Deploy Owner: AWS Version: '1' Provider: CodeDeployToECS Configuration: AppSpecTemplateArtifact: App AppSpecTemplatePath: appspec.yaml TaskDefinitionTemplateArtifact: App TaskDefinitionTemplatePath: taskdef.json ApplicationName: !Ref CodeDeployAppName DeploymentGroupName: !Ref CodeDeployDGName Image1ArtifactName: BuildOutput Image1ContainerName: IMAGE1_NAME RunOrder: 1 InputArtifacts: - Name: App - Name: BuildOutput Region: !Ref AWS::Region # ------------------------------------------------------------# # Outputs # ------------------------------------------------------------# Outputs: PipelinelogicalID: Description: logical ID. Value: !Ref Pipeline
最後に
前回カスタムリソースを使ったのは、今は AWS のアップデートによって不要となったCloudFormation一撃でAWS Transfer for SFTP のパブリックアクセスを特定IP に限定する | DevelopersIO でした。
このテンプレートもそのうち無用になるかと思っています。
とはいえ、個人的には機会があればカスタムリソースを使ってみたいと思います。
以上です。
どなたかのどなたかのお役に立てば幸いです。