概要
AWS CodePipelineを使うとECSに自動デプロイができるが、対応しているのはECSサービスに紐づいたタスク定義だけとなっている。
そこで、EventBridgeを使った定時バッチ用に都度コンテナを起動するタスク定義の更新をCodePipelineから行う方法を試した。
CodePipelineの流れ
今回作成するパイプラインは以下の通り。
- Sourceステージ:GitHubから指定したブランチのコードをpullする
- Buildステージ:buildspec.ymlの手順でビルド、イメージをECR登録
- Deployステージ:ビルドしたイメージを設定したタスク定義をECSにデプロイ
DeployステージのアクションプロバイダーをAmazon ECSにすると、Buildステージのアクションでアーティファクトに出力したイメージ定義ファイルを元に、ビルドしたイメージを使ってデプロイできる。
この方法はサービスに紐づいていないタスク定義ではできないので、今回はタスク定義の更新にLambda関数を使う。
Lambda関数でタスク定義を更新する流れ
- Buildステージでビルドしたイメージのタグを取得する
- DeployステージのLambda関数でイメージのタグを取得する
- 対象のタスク定義を更新する(前リビジョンの数値を使って新規作成)
- EventBridgeなど対象のタスク定義を使うサービスのターゲットを変更する
Buildステージでビルドしたイメージのタグを取得する
方法は2通りある。今回は最初の方法を使う。
- Buildステージでイメージのタグを変数として出力する
- アーティファクトに保存したイメージ定義ファイルからタグを取得する
- 既にECSへの自動デプロイを実装しているならbuildspec.ymlを変更する必要がない
Buildステージでイメージのタグを変数として出力する
exported-variables
を設定することで別のステージで該当変数を扱うことができる。
https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec.env.exported-variables
buildspec.yml
env:
exported-variables:
- IMAGE_TAG_HASH
phases:
# buildspec内で指定したイメージのタグをexportする
export IMAGE_TAG_HASH="HOGEFUGA"
Buildステージのアクション「変数の名前空間」で BuildVariables
と指定すれば、
以降のステージで #{BuildVariables.IMAGE_TAG_HASH}}
と参照できる。
DeployステージのLambda関数でイメージのタグを取得する
CodePipelineの設定
「アクションプロバイダー」はAWS Lambda、「関数名」は作成済みのLambda関数の名前を選択する。
デフォルトでは先ほどの IMAGE_TAG_HASH
はLambda関数に渡されないので、
Lambda用アクションの「ユーザーパラメーター」で #{BuildVariables.IMAGE_TAG_HASH}}
を指定する。
Lambda関数の設定
lambda_handler
の引数 event
にCodePipelineから渡されたデータが格納されている。
このコードは以下の処理を実施する。
- 対象のタスク定義を更新する(前リビジョンの数値を使って新規作成)
- EventBridgeなど対象のタスク定義を使うサービスのターゲットを変更する
import os
import traceback
import boto3
TASK_DEFINITION_NAME = os.environ['taskDefinitionName']
ENV_NAME = os.environ['EnvName']
rule_list = [
'appname-%s-batch-01' % (ENV_NAME),
'appname-%s-batch-02' % (ENV_NAME),
]
code_pipeline = boto3.client('codepipeline')
def put_job_success(job, message):
print('Putting job success')
print(message)
code_pipeline.put_job_success_result(jobId=job)
def put_job_failure(job, message):
print('Putting job failure')
print(message)
code_pipeline.put_job_failure_result(jobId=job, failureDetails={'message': message, 'type': 'JobFailed'})
def update_scedule(job_id, rule_name, task_definition_arn):
eb = boto3.client('events')
response = eb.list_targets_by_rule(Rule=rule_name)
target_contents = response['Targets']
target_contents[0]['EcsParameters']['TaskDefinitionArn'] = task_definition_arn
ret = eb.put_targets(Rule=rule_name, Targets=target_contents)
if ret['FailedEntryCount'] > 0:
put_job_failure(job_id, 'Update Failure.')
print('Update Success: ' + rule_name)
def create_task_definition(image_tag_hash, account_id):
ecs = boto3.client('ecs')
response = ecs.list_task_definitions(
familyPrefix=TASK_DEFINITION_NAME,
status='ACTIVE',
sort='DESC',
maxResults=1
)
arn = response['taskDefinitionArns'][0]
response = ecs.describe_task_definition(taskDefinition=arn, include=['TAGS'])
td = response['taskDefinition']
cd = td['containerDefinitions'][0]
cd['image'] = '%s.dkr.ecr.ap-northeast-1.amazonaws.com/app:%s' % (account_id, image_tag_hash)
response = ecs.register_task_definition(
family=TASK_DEFINITION_NAME,
taskRoleArn=td['taskRoleArn'],
executionRoleArn=td['executionRoleArn'],
networkMode='awsvpc',
containerDefinitions=[cd],
requiresCompatibilities=td['requiresCompatibilities'],
cpu=td['cpu'],
memory=td['memory'],
tags=response['tags']
)
return response['taskDefinition']['taskDefinitionArn']
def lambda_handler(event, context):
rule_name = ''
try:
job_id = event['CodePipeline.job']['id']
account_id = event['CodePipeline.job']['accountId']
image_tag_hash = event['CodePipeline.job']['data']['actionConfiguration']['configuration']['UserParameters']
task_definition_arn = create_task_definition(image_tag_hash, account_id)
print('Create Success: ' + task_definition_arn)
for rule_name in rule_list:
update_scedule(job_id, rule_name, task_definition_arn)
except Exception as e:
print('Function failed due to exception. ' + rule_name)
print(e)
traceback.print_exc()
put_job_failure(job_id, 'Function exception: ' + str(e))
put_job_success(job_id, 'All Update Success.')
print('Function complete.')
return "Complete."
タスク定義は更新のAPIがないので、最新リビジョンの詳細を取得し、そのデータを以って新規リビジョンを作成する必要がある。
この関数はCodePipelineから呼び出すので、単純に結果をreturnするのではなく、put_job_success()とput_job_failure()を使ってパイプラインに結果を返す必要がある。
これを実施しないとパイプラインが動き続けてしまう。
参考
- https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html
Lambda関数のロール
デフォルトのロールだとLambda関数からCodePipelineとECSにアクセスできない。
そこで以下の様なポリシーを付与する。
{
"Sid": "ECS",
"Effect": "Allow",
"Action": [
"iam:PassRole",
"ecs:ListTaskDefinitions",
"ecs:DescribeTaskDefinition",
"ecs:RegisterTaskDefinition"
],
"Resource": "*"
},
{
"Sid": "EventBridge",
"Effect": "Allow",
"Action": [
"events:ListRules",
"events:PutTargets",
"events:ListTargetsByRule"
],
"Resource": "arn:aws:events:ap-northeast-1:000000000000:rule/batch-*"
},
{
"Sid": "CodePipeline",
"Effect": "Allow",
"Action": [
"codepipeline:PutJobFailureResult",
"codepipeline:PutJobSuccessResult"
],
"Resource": "*"
},
各アクションに対して必要最低限のリソースを設定するのがベストだが、
アクションによってはリソースが *
必須のものが多かった。
参考
- https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/permissions-reference.html
- https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonelasticcontainerservice.html
テスト
毎回パイプラインを実行しなくともテストの「イベント JSON」に以下のような設定をすれば動作確認ができる。
{
"CodePipeline.job": {
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "000000000000",
"data": {
"actionConfiguration": {
"configuration": {
"FunctionName": "app-deploy-batch",
"UserParameters": "0000000000000000000000000000000000000000"
}
}
}
}
}
余談
ちなみにイメージ定義ファイルからタグを取得する場合は、
Lambda関数のコードで以下の様にアクセスできる inputArtifacts
のアーティファクトの情報を使う。
このデータからS3クライアントを使ってイメージ定義ファイルを読み込みjsonをパースしてイメージのタグを取得できる。
# Extract the Job Data
job_data = event['CodePipeline.job']['data']
# Get the list of artifacts passed to the function
artifacts = job_data['inputArtifacts']
artifacts["location"]["s3Location"]["bucketName"]
artifacts["location"]["s3Location"]["objectKey"]