GitHub Actionsを使用したECSサービスの更新を試してみた
GitHub Actionsを利用したECSサービスの更新を試す機会があったので記録として残しておこうと思います。
やること
GitHub ActionsからECSクラスター内のサービスを更新します。
タスク定義の登録やECSサービスの更新はamazon-ecs-deploy-task-definitionを使用します。
AWSの構成は以下のようにします。
デプロイ後、ALBからアクセスしてWebページの内容が変わっていることを確認します。
設定
GitHubとAWSの連携
GitHub ActionsでAWSの認証情報を使用するためにOIDCプロバイダーの設定を行います。
こちらの設定を行うことでIAMロールを引き受け一時的な認証情報を使用したアクセスが可能となります。
以下のドキュメントの手順でプロバイダを追加することが可能ですが、今回はCloudFormationで作成してみました。
以下のCloudFormationテンプレートでOIDCプロバイダとIAMロールを作成しています。
IAMロールの使用するIAMポリシーではECSサービスの更新やECRへのpushができるようにしています。(今回は検証なので細かく絞っていませんが、必要があれば"Resource"でサービスを絞り込んだりすることをお勧めします)
AWSTemplateFormatVersion: '2010-09-09'
Description: OIDC settings for Github Actions.
Parameters:
EnvName:
Type: String
Default: dev
AllowedValues:
- dev
Description: Environment name
OrgID:
Type: String
Description: Github Organazation ID
RepoName:
Type: String
Default: RepositoryName
Description: Repository name
Resources:
OIDCProvider:
Type: AWS::IAM::OIDCProvider
Properties:
ClientIdList:
- 'sts.amazonaws.com'
Url: https://token.actions.githubusercontent.com
GithubActionsPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- 'ecr:UploadLayerPart'
- 'ecr:PutImage'
- 'ecr:InitiateLayerUpload'
- 'ecr:CompleteLayerUpload'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetAuthorizationToken'
- 'iam:PassRole'
- 'ecs:UpdateService'
- 'ecs:DescribeServices'
- 'ecs:DescribeTaskDefinition'
- 'ecs:RegisterTaskDefinition'
Resource: '*'
ManagedPolicyName: !Sub policy-${EnvName}-github-oidc-${RepoName}-001
OIDCProviderRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub 'role-${EnvName}-github-oidc-${RepoName}-001'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
Action: 'sts:AssumeRoleWithWebIdentity'
Condition:
StringLike:
'token.actions.githubusercontent.com:sub':
- !Sub 'repo:${OrgID}/${RepoName}:*'
ManagedPolicyArns:
- !Ref GithubActionsPolicy
デプロイは以下のコマンドで行います。
aws cloudformation create-stack --stack-name スタック名 --template-body file://CloudFormationテンプレートファイル名 --capabilities CAPABILITY_NAMED_IAM --parameters ParameterKey=OrgID,ParameterValue=オーナ名 ParameterKey=RepoName,ParameterValue=
リポジトリ名
ECSサービスなどの作成
ECSを作成する前にECRを作成して初期デプロイ時に使用するコンテナイメージを格納しておきます。
ECRは以下のCloudFormationテンプレートで作成します。
AWSTemplateFormatVersion: "2010-09-09"
Description: ECR
Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Parameters for env Name
Parameters:
- env
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
env:
Type: String
Default: dev
AllowedValues:
- dev
Resources:
# ------------------------------------------------------------#
# ECR
# ------------------------------------------------------------#
ECR:
Type: AWS::ECR::Repository
Properties:
EmptyOnDelete: true
EncryptionConfiguration:
EncryptionType: AES256
RepositoryName: !Sub ecr-${env}
ECRを作成したら以下のDockerfileを使用してコンテナイメージを作成します。
htmlファイルは好きなものを使用してください。
FROM public.ecr.aws/docker/library/httpd:2.4
COPY ./html/ /usr/local/apache2/htdocs/
ファイルを作成したら以下のコマンドでECRへコンテナイメージをpushしていきます。
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t ecr-dev .
docker tag ecr-dev:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-dev:latest
docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-dev:latest
ECRへコンテナイメージをpushしたら以下のCloudFormationテンプレートでECS周りのリソースを作成します。
AWSTemplateFormatVersion: "2010-09-09"
Description: ECS
Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Parameters for env Name
Parameters:
- env
- Label:
default: Parameters for Network
Parameters:
- VPCCIDR
- PublicSubnet01CIDR
- PublicSubnet02CIDR
- PrivateSubnet01CIDR
- PrivateSubnet02CIDR
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
env:
Type: String
Default: dev
AllowedValues:
- dev
VPCCIDR:
Default: 192.168.0.0/16
Type: String
PublicSubnet01CIDR:
Default: 192.168.0.0/24
Type: String
PublicSubnet02CIDR:
Default: 192.168.1.0/24
Type: String
PrivateSubnet01CIDR:
Default: 192.168.2.0/24
Type: String
PrivateSubnet02CIDR:
Default: 192.168.3.0/24
Type: String
Resources:
# ------------------------------------------------------------#
# CloudWatch Logs
# ------------------------------------------------------------#
ECSLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/ecs-${env}-log"
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------#
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
RoleName: !Sub iam-${env}-ecs-tast-execution-role
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub vpc-${env}
# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------#
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub igw-${env}
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------#
PublicSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PublicSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub1
VpcId: !Ref VPC
PublicSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PublicSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub2
VpcId: !Ref VPC
PrivateSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PrivateSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv1
VpcId: !Ref VPC
PrivateSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PrivateSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv2
VpcId: !Ref VPC
# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------#
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-pub
PublicRouteTableRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet01
PublicRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet02
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-prv
PrivateRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet01
PrivateRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet02
# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------#
ALBSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for alb
GroupName: !Sub securitygroup-${env}-alb
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 80
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-alb
VpcId: !Ref VPC
ECSSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for ecs
GroupName: !Sub securitygroup-${env}-ecs
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
SourceSecurityGroupId: !Ref ALBSG
ToPort: 80
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-ecs
VpcId: !Ref VPC
VPCEndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for vpc endpoint
GroupName: !Sub securitygroup-${env}-vpc-endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 443
IpProtocol: tcp
SourceSecurityGroupId: !Ref ECSSG
ToPort: 443
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-vpc-endpoint
VpcId: !Ref VPC
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
VpcId: !Ref VPC
ECRdkrEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
ECRapiEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
LogsEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
LoadBalancerAttributes:
- Key: deletion_protection.enabled
Value: false
Name: !Sub alb-${env}-ecs
Scheme: internet-facing
SecurityGroups:
- !Ref ALBSG
Subnets:
- !Ref PublicSubnet01
- !Ref PublicSubnet02
Tags:
- Key: Name
Value: !Sub alb-${env}-ecs
Type: application
TargetGroup1:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-01
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-01
TargetType: ip
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
ALBHTTPListener1:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup1
Type: forward
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
# ------------------------------------------------------------#
# ECS
# ------------------------------------------------------------#
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
CapacityProviders:
- FARGATE
ClusterName: !Sub ecs-${env}-cluster
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
ECSTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
- Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-${env}:latest
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref ECSLogGroup
awslogs-region: !Ref "AWS::Region"
awslogs-stream-prefix: !Sub ecs-${env}-log
Name: !Sub task-${env}
PortMappings:
- ContainerPort: 80
HostPort: 80
Cpu: 256
ExecutionRoleArn: !Ref TaskExecutionRole
Family: !Sub task-${env}
Memory: 512
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBHTTPListener1
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
LoadBalancers:
- ContainerName: !Sub task-${env}
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup1
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSG
Subnets:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
ServiceName: !Sub service-${env}
TaskDefinition: !Ref ECSTaskDef
PlatformVersion: LATEST
DeploymentConfiguration:
DeploymentCircuitBreaker:
Enable: true
Rollback: true
DeploymentController:
Type: ECS
デプロイが正常に完了するとALBのDNS名からWebページが確認できます。
GitHub Actions設定
AWSリソースの作成が完了したらGitHub Actionsで使用するワークフローファイルを作成します。
以下のREADMEに記載されている例を参考にファイルを作成しました。
name: ECS deploy
on:
pull_request:
branches:
- main
types: [closed]
env:
AWS_ACCOUNT_ID: AWSアカウントID
TASK_DEF: "task-dev"
ECS_SERVICE: "service-dev"
ECS_CLUSTER: "ecs-dev-cluster"
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
defaults:
run:
working-directory: ./
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: "arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/role-dev-github-oidc-ecs-deploy-001"
aws-region: "ap-northeast-1"
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
id: login-ecr
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: "ecr-dev"
IMAGE_TAG: ${{ github.sha }}
run: |
docker build . --tag ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}
docker push ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}
echo "image=${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}" >> $GITHUB_OUTPUT
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.TASK_DEF }} --query taskDefinition > task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.TASK_DEF }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
上記のワークフローファイルはmainブランチへマージした際に動き出します。
37行目から46行目でコンテナイメージをビルドしてECRへpushしています。
48行目から50行目で現在のタスク定義を取得してtask-definition.jsonというファイルに保存しています。
52行目から58行目で上記で取得したtask-definition.jsonのイメージURLを書き換えています。
イメージURLの書き換えにはamazon-ecs-render-task-definitionを使用しています。
60行目から66行目でECSサービスの更新を行っています。
フォルダの構成は以下のようにしてください。
.
├── Dockerfile
├── .github
│ └── workflows
│ └── deploy.yaml
└── html
└── index.html
動作確認
ファイルの作成ができたらGitHubリポジトリにpushを行ってmainブランチへマージを行います。
マージを行うとActionsタブからデプロイが開始していることを確認できます。
デプロイが完了すると緑色のチェックマークに変わります。
ALBのDNS名から再度アクセスすると変更したHTMLファイルが反映されていることが確認できます。
さいごに
今回はローリングアップデートでECSサービスを更新しました。
amazon-ecs-deploy-task-definitionはCodeDeployを使用することでBlue/Greenデプロイもできるようなので次回試してみたいと思いました。