14
3

More than 1 year has passed since last update.

CodePipelineからLambdaを使ってECSタスク定義を更新する

Last updated at Posted at 2022-07-21

概要

AWS CodePipelineを使うとECSに自動デプロイができるが、対応しているのはECSサービスに紐づいたタスク定義だけとなっている。
そこで、EventBridgeを使った定時バッチ用に都度コンテナを起動するタスク定義の更新をCodePipelineから行う方法を試した。

CodePipelineの流れ

今回作成するパイプラインは以下の通り。

  1. Sourceステージ:GitHubから指定したブランチのコードをpullする
  2. Buildステージ:buildspec.ymlの手順でビルド、イメージをECR登録
  3. Deployステージ:ビルドしたイメージを設定したタスク定義をECSにデプロイ

DeployステージのアクションプロバイダーをAmazon ECSにすると、Buildステージのアクションでアーティファクトに出力したイメージ定義ファイルを元に、ビルドしたイメージを使ってデプロイできる。
この方法はサービスに紐づいていないタスク定義ではできないので、今回はタスク定義の更新にLambda関数を使う。

Lambda関数でタスク定義を更新する流れ

  1. Buildステージでビルドしたイメージのタグを取得する
  2. DeployステージのLambda関数でイメージのタグを取得する
  3. 対象のタスク定義を更新する(前リビジョンの数値を使って新規作成)
  4. 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から渡されたデータが格納されている。

このコードは以下の処理を実施する。

  1. 対象のタスク定義を更新する(前リビジョンの数値を使って新規作成)
  2. 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()を使ってパイプラインに結果を返す必要がある。
これを実施しないとパイプラインが動き続けてしまう。

参考

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": "*"
        },

各アクションに対して必要最低限のリソースを設定するのがベストだが、
アクションによってはリソースが * 必須のものが多かった。

参考

テスト

毎回パイプラインを実行しなくともテストの「イベント 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"]
14
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
3