見出し画像

ECRへのレプリケーションをトリガーにECSのサービス更新を実行する

はじめに

こんにちは、SHIFT にて自動化アーキテクトとしてテスト自動化・DevOps 導入支援などをしていますKatayamaです。

バックエンドの Build 成果物(docker image)が Deploy 環境にレプリケートされた事をトリガーに、ECS のサービス更新を行う CD の構築を行ったので、それに関する Tips のようなものを書き残しておこうと思います。

(前回は Cloud Front のキャッシュ削除についての記事だったが、この記事は ECS のサービス更新についての記事になる。)

※マルチベンダーなどの場合、開発資材のやり取り(コピー・複製=レプリケーション)は必要になるので今回もその要望から始まった。

構成イメージ

何らかのトリガーで Code Build が Start すると、ECR に Build 成果物である docker image が push され、それをトリガーにレプリケートが走る。レプリケーション先の環境でレプリケート後に自動で Deploy(ECS のサービス更新)を実行させる。

図で示すと以下のようになる。

ECS のサービス更新

今回の Deploy は、Blue/Green Deploy ではなく、単純に ECS のサービス更新を行う方式で行った(検証環境という事でダウンタイム発生が許容されていたので)。

Deploy 自体は、レプリケーションで ECR に新規で image が push されるのでそれを Cloud Watch Events で検知してそれをトリガーに Lambda 関数を実行し、以下の AWS CLI コマンドで行っている事と同じ事を AWS SDK(今回は JavaScript)で実行させる、という感じで行う。

aws ecs update-service --cluster {cluster名} --service {service名}

※ --cluster は省略可能だがその場合デフォルトのクラスターになるので、UpdateService の API を実行する ARN が arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:service/default/{service 名} になるので注意。

※Cloud Watch Event にAmazon Elastic Container Registry イベントがあるので、設定する際はこれに思えるが、レプリケーションで ECR に image が push される場合は、 ReplicateImage という Cloud Trail に記録されるイベントになるので、これを Cloud Watch Event の AWS API Call via CloudTrail で捕捉させて Deploy を実行する必要がある。

・参考:aws.ecs.update-service

IAM

Lambda 関数を作成するために必要な IAM( lambda: や iam: )については、ここでは省略する(S3 へのレプリケーションをトリガーに Cloud Front のキャッシュ削除を実行するの方を参照)。ここでは Cloud Watch Event のルールを作成するための IAM について見ていく。

今回構築してみて分かった事だが、Management Console から設定する場合と、AWS CLI で設定する場合とでは、必要になる権限に大きな違いがあったのでそれぞれの設定方法で必要な権限について見ていく。

※Cloud Watch Events は cloudwatch: ではなく events: (EventBridge)なので注意。

Management Console からの設定

Management console からのルール作成の場合、参考に示したCloudWatch Events API オペレーションおよびアクションに必要な許可の権限全てがないと、以下のように「今すぐ始める」ボタンが非活性で作成できない。

したがって以下のような権限(インラインポリシー)が必要になる。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": [
        "events:PutEvents",
        "events:DeleteRule",
        "events:PutTargets",
        "events:DescribeRule",
        "events:EnableRule",
        "events:PutRule",
        "events:RemoveTargets",
        "events:ListTargetsByRule",
        "events:DisableRule"
      ],
      "Resource": [
        "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:rule/replication-deploy-by-console",
        "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:event-bus/*"
      ]
    },
    {
      "Sid": "VisualEditor1",
      "Effect": "Allow",
      "Action": [
        "events:ListRuleNamesByTarget",
        "events:ListRules",
        "events:TestEventPattern"
      ],
      "Resource": "*"
    }
  ]
}

・参考:CloudWatch Events API オペレーションおよびアクションに必要な許可

AWS CLI コマンドからの設定

AWS CLI コマンドでルールを作成する場合には、以下の IAM のみで OK で、Management Console から設定する時よりも最小権限で済む。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": ["events:PutTargets", "events:PutRule"],
      "Resource": "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:rule/replication-deploy-by-cli"
    }
  ]
}

・参考:aws.events.put-rule

Lambda 関数のトリガーを設定する

ここでも Management Console から設定する場合と、AWS CLI コマンドで設定する場合の 2 パターンを見ていく。

Management Console からの設定

Management Console 上で設定すると以下のような感じ。

{
  "source": ["aws.ecr"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["ecr.amazonaws.com"],
    "eventName": ["ReplicateImage"]
  }
}

設定が完了すると Lambda のトリガーに以下のように表示される。

AWS CLI コマンドからの設定

以下のコマンドを順番に実行していけば設定できる。

aws events put-rule \
--name "replication-deploy-by-cli" \
--event-pattern file://event-json.json \
--description "AWS CLIから作成したreplication-deployのルール"

aws lambda add-permission \
--function-name replication-deploy-lambda \
--statement-id replication-deploy-by-cli \
--action 'lambda:InvokeFunction' \
--principal events.amazonaws.com --source-arn arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:rule/replication-deploy-by-cli

aws events put-targets \
--rule replication-deploy-by-cli \
--targets "Id"="1","Arn"="arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:replication-deploy-lambda"

コマンドについて少し補足をすると、

1, aws events put-rule を実行すると、ターゲットのないイベントルールが作成される

2, aws lambda add-permission を実行すると、以下の画像のように EventBridge からのアクセスを信頼し Lambda のトリガーに EventBridge(Cloud Watch Event)を設定できるようになる(ルールのターゲットが未設定なので赤字のエラーが出ている)

3, aws events put-targets を実行すると、ルールにターゲットが設定されトリガーとしての設定が完了する(設定完了後の Lambda のトリガーの状況は上記のManagement console からの画像を参照)

※event-json.json の中身はManagement Console からの設定の JSON と同じ。

※add permission コマンドで events:(EventBridge)からの Lambda へのアクセス・設定を信頼するのがポイントで、これをしないとちゃんと設定できない。

・参考:ステップ 2: ルールを作成する

Lambda 関数の実装

イベントトリガーの場合には Lambda 関数に Event オブジェクトが渡ってくるが、まずは ECR へのレプリケーションで image が push される時に Cloud Trail に記録され、Cloud Watch Event で検知するイベントの Event オブジェクトがどのようなものか?を見てみる。

その後、その Event オブジェクトの中身から Deploy(ECS UpdateService)を行うのに必要な情報を抜き出し実際にそれを実行するコードを考える。

Lambda に渡ってくる Event オブジェクトの中身

まず、Cloud Trail に記録されるオブジェクトだが、以下のようなオブジェクトが記録される(関係のある部分のみに省略して書いている)。

{
    "eventVersion": "1.04",
    "eventSource": "ecr.amazonaws.com",
    "eventName": "ReplicateImage",
    "awsRegion": "us-east-2",
    "requestParameters": {
        ...
    },
    "responseElements": {
        ...
    },
    "requestID": "cb8c167e-EXAMPLE",
    "eventID": "e3c6f4ce-EXAMPLE",
    "resources": [
        {
            "ARN": "arn:aws:ecr:us-east-2:123456789012:repository/testrepo",
            "accountId": "123456789012"
        }
    ],
    "eventType": "AwsApiCall",
}

そしてこの Cloud Trail に記録されるイベントを Cloud Watch Events の AWS API Call via CloudTrail で捕捉するので、Lambda 関数に渡ってくる Event オブジェクトの中身としては以下のような JSON になる(一部省略している)。

ポイントは、 Cloud Trail に記録されるイベントを Cloud Watch Events で捕捉する場合、CloudWatch イベントのイベントパターンで書かれている JSON の detail-type が AWS API Call via CloudTrail になり、detail が Cloud Trail に記録されるイベントのオブジェクト(上記)になるという事。

{
  "version": "0",
  "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718",
  "detail-type": "AWS API Call via CloudTrail",
  "source": "aws.ecr",
  "region": "us-east-2",
  "resources": [],
  "detail": {
    "eventVersion": "1.04",
    "eventSource": "ecr.amazonaws.com",
    "eventName": "ReplicateImage",
    "awsRegion": "us-east-2",
    "requestParameters": {
        ...
    },
    "responseElements": {
        ...
    },
    "requestID": "cb8c167e-EXAMPLE",
    "eventID": "e3c6f4ce-EXAMPLE",
    "resources": [
        {
            "ARN": "arn:aws:ecr:us-east-2:123456789012:repository/testrepo",
            "accountId": "123456789012"
        }
    ],
    "eventType": "AwsApiCall",
  }
}

・参考:Amazon ECR ログファイルエントリの概要
・参考:CloudTrail ログイベントリファレンス
・参考:CloudWatch イベントのイベントパターン

Lambda 関数(index.handler)の実装

今回は Node.js の Lambda 関数で Deploy を実行させるので、実装としては以下のようした。

const { ECSClient, UpdateServiceCommand } = require("@aws-sdk/client-ecs");
const client = new ECSClient({ region: process.env.REGION });

const serviceRegex = new RegExp(process.env.ECR_SERVICE_REGEX);
const clustreRegex = new RegExp(process.env.ECR_CLUSTER_REGEX);

exports.handler = async (event) => {
  try {
    const service = event.detail.resources[0].ARN.match(serviceRegex)
      ? event.detail.resources[0].ARN.match(serviceRegex)[1]
      : undefined;
    const cluster = event.detail.resources[0].ARN.match(clustreRegex)
      ? event.detail.resources[0].ARN.match(clustreRegex)[1]
      : undefined;

    if (!service && !cluster) {
      console.log("not match regex", event.detail.resources[0].ARN);
      return "";
    }

    const input = {
      service, // service: service の ES6省略形
      cluster, // cluster: cluster の ES6省略形
      forceNewDeployment: true,
    };
    console.log("input", input);

    const command = new UpdateServiceCommand(input);
    const response = await client.send(command);

    console.log("status", response.$metadata.httpStatusCode);
    console.log("serviceArn", response.service.serviceArn);

    return "";
  } catch (error) {
    return errorHandler(error);
  }
};

const errorHandler = (error) => {
  const obj = {};
  obj["status"] = 500;
  obj["message"] = error.message;
  obj["stack"] = error.stack;
  obj["result"] = "ng";
  if (error.$metadata) {
    obj["status"] = error.$metadata.httpStatusCode;
  }

  console.log("errorHandler", obj);
  return "";
};

※実装について何点か補足すると、

・if (!service && !cluster)
 三項演算子で undefined にせず、if()の中に event.detail.resources[0].ARN.match(serviceRegex) を書く事もできるが長いのであえて上記のような実装にした

・console.log()
 今回は Cloud Watch Events(EventBridge)が Lambda 関数のトリガーであり、return された結果を人が見る事ができないので、何が起きているか分かるように敢えて console.log()で log に出力させるようにした

・return "";
 これも今回 S3 がトリガーで return された結果を人が見れないので""(空文字)を返すようにした(何か JSON を返しても意味ないと判断)。

・参考:ECS Client - AWS SDK for JavaScript v3
・参考:Class UpdateServiceCommand

Lambda 関数の実行ロールの設定

Lambda 関数が他の AWS サービスに対して何らかの操作を行うには、実行ロールでその権限を設定する必要がある。今回は ECS の UpdateService が実行できればいいので以下のようなインラインポリシーを実行ロールに追加でアタッチするれば OK。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": "ecs:UpdateService",
      "Resource": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:service/{cluster名}/{service名}"
    }
  ]
}

まとめとして

前回は Cloud Front のキャッシュ削除について扱ったが、ECS についても同じような考え方でレプリケーション後にそれをトリガーにして Deploy を実行させる事ができた。

■このライターの他の記事を読む

__________________________________

執筆者プロフィール:Katayama Yuta
SaaS ERPパッケージベンダーにて開発を2年経験。 SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、最近ではDevOpsの一環でCICD導入支援をする案件にも従事。 座学で読み物を読むより、色々手を動かして試したり学んだりするのが好きなタイプ。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/