CodePipelineの承認アクションで非同期にパイプラインを承認する

同じチームの方々がみんなre:Inventに行ってしまい、悲しい気分なもこ@札幌オフィスです。

CodePipelineのApprove Actionを利用して非同期的な処理をした後にパイプラインを承認してみたので、構築方法をご紹介します!

なぜCodePipelineのApproveを非同期にイベント駆動的に承認したいのか?

本記事のタイトルだけ見ても何をやりたいのかよくわからないと思います。

要するに、Code Pipelineの「Manual approval」のSNSイベントを利用して、SQS/DynamoDBにTokenを保存し、任意の非同期処理が終わったらSQS/DynamoDBに保存しているTokenを利用して承認してパイプラインを進める感じです。

実際にパイプラインに組み込む際はステージの遷移を止めたい場所にManual Approvalステージを挿入し、非同期でパイプラインが承認されてからステージを進める形です。

これができると何が嬉しいのかというと、

・EventBridge(CloudWatch Events)が発火されるまでパイプラインを止める

・CodeBuild / Lambdaを回すとコストのかかるジョブ

・CodePipelineのアクションプロバイダーで対応していない処理などをパイプライン上で待つ

などなど、かゆいところに手を伸ばすことができます。(ベストプラクティスなのかって話ですが...。)

早速CodePipelineのApprivalがどんなイベントを発火するのかを確認していきましょう。

前提

Manual approvalのSNSから出力されるイベントの中身

Manual approvalで出力されるJSONを確認したいため、下記のようなパイプラインを組み、SNSにメッセージを送信するようにしました。

取得できた結果は次の通りです。

{
  "region": "ap-northeast-1",
  "consoleLink": "https://console.aws.amazon.com/codepipeline/home?region=ap-northeast-1#/view/codepipeline",
  "approval": {
    "pipelineName": "codepipeline",
    "stageName": "Approve",
    "actionName": "Approval",
    "token": "6b7a1872-bed6-4ad1-9d9c-c2ba1321ec6d",
    "expires": "2019-11-26T02:25Z",
    "externalEntityLink": null,
    "approvalReviewLink": "https://console.aws.amazon.com/codepipeline/home?region=ap-northeast-1#/view/codepipeline/Approve/Approval/approve/6b7a1872-bed6-4ad1-9d9c-c2ba1321ec6d",
    "customData": null
  }
}

approval.tokenを使うと行けそうな気がします。

承認するLambda関数を作ってみる

先ほどのSNSに送られたメッセージを利用することを仮定して、approval.tokenを利用して、CodePipelineの進行を承認するLambda関数を書いてみましょう。

PutApprovalResult APIを利用するといけそうです。

PutApprovalResult https://docs.aws.amazon.com/codepipeline/latest/APIReference/API_PutApprovalResult.html

Node.jsでざっと書いてみました。

'use strict';
const AWS = require('aws-sdk');
const pipeline = new AWS.CodePipeline({region: 'ap-northeast-1'});

const data = {
  'region': 'ap-northeast-1',
  'consoleLink': 'https://console.aws.amazon.com/codepipeline/home?region=ap-northeast-1#/view/codepipeline',
  'approval': {
    'pipelineName': 'codepipeline',
    'stageName': 'Approve',
    'actionName': 'Approval',
    'token': '6b7a1872-bed6-4ad1-9d9c-c2ba1321ec6d',
    'expires': '2019-11-26T02:25Z',
    'externalEntityLink': null,
    'approvalReviewLink': 'https://console.aws.amazon.com/codepipeline/home?region=ap-northeast-1#/view/codepipeline/Approve/Approval/approve/6b7a1872-bed6-4ad1-9d9c-c2ba1321ec6d',
    'customData': null,
  },
};

exports.handler = async event => {
  const params = {
    actionName: data.approval.actionName,
    pipelineName: data.approval.pipelineName,
    result: {
      status: "Approved",
      summary: 'Approved by Lambda'
    },
    stageName: data.approval.stageName,
    token: data.approval.token
  };

  pipeline.putApprovalResult(params, (err, data) => {
    if(err) return console.log(err)
    console.log(data)
  });
};

これを実行した後にCode Pipelineを見ると、承認されてパイプラインが進んでいることを確認できます。

実際にSQSに入れて非同期的に承認してみる

本題です。Tokenを取得して承認できることを確認したので、このTokenを使いまわして、非同期的に承認してみましょう。

構成図に起こすとこんな感じのものを作っていきます。

(1)~(2)の段階ではSNS経由でSQSにイベントを書き込んでいます。

緑の矢印がパイプラインとは独立した非同期な部分です。

今回はテストとしてEventBrdige(CloudWatch Events)で1分置きにイベントを発火するように設定し、LambdaでSQSからトークンを取得してパイプラインを承認してみます。

AWS SAMで環境の構築

SNS, SQS, Lambda, ScheduleEventなどをまるっと構築する検証用のSAM Templateを作りました。

GitHubで公開しているので、今回はこちらをベースにやっていきます。

https://github.com/mokocm/codepipeline-async-approval

cloneして実行していきます。

git clone https://github.com/mokocm/codepipeline-async-approval.git

cd codepipeline-async-approval

# --s3-bucket は所有しているBucket名に変更
sam build && sam deploy  --stack-name codepipeline-approval-lambda --s3-bucket {BUCKET_NAME_HERE} --capabilities CAPABILITY_IAM

上記コマンドを実行し、Successfully created/updated stack - codepipeline-approval-lambda in ap-northeast-1と出れば完了です。

パイプラインに組み込む

SAMで出来上がったSNSのTopicを指定するように、パイプラインの任意の場所にManual approvalを追加します。

今回作ったパイプラインはこんな感じ。

動作確認

実際にパイプラインを動かしてみましょう。

Manual Approvalアクションで承認待ちとなります。

SNS経由でSQSにメッセージが送信されています。

パイプラインとは完全に独立した1分間に1回実行されるLambdaでSQSからポーリングし、キューのメッセージを元にパイプラインを承認します。

'use strict';
const AWS = require('aws-sdk');
const pipeline = new AWS.CodePipeline();
const sqs = new AWS.SQS();
const QueueUrl = process.env['QUEUE_URL'];

exports.handler = event => {

  sqs.receiveMessage({QueueUrl}, (err, data) => {
    if (err) return console.error(err);
    if (data.Messages === undefined) return;

    data.Messages.map(message => {
      const body = JSON.parse(message.Body);
      const queueMessage = JSON.parse(body.Message);
      console.log(queueMessage);
      const pipelineParams = {
        actionName: queueMessage.approval.actionName,
        pipelineName: queueMessage.approval.pipelineName,
        result: {
          status: 'Approved',
          summary: 'Approved by Lambda',
        },
        stageName: queueMessage.approval.stageName,
        token: queueMessage.approval.token,
      };

      pipeline.putApprovalResult(pipelineParams, (err, data) => {
        if (err) return console.error(err);
        console.log(data);

        sqs.deleteMessage({QueueUrl, ReceiptHandle: message.ReceiptHandle}, (err, data) => {
          if (err) return console.error(err);
          console.log(data);
        });
      });
    });
  });
};

しばらくするとパイプラインが承認されました!

まとめ

ご紹介してきました通り、CodePipelineの「Manual Approval」を利用することで、CodePipelineでは実現できないジョブを実行したり、EventBrdige(CloudWatch Events)を待ってからパイプラインを再開するなどの、細かな設定をすることが出来ました。(ベストプラクティスとは思えませんが。)

誰かのご参考になれば幸いです。

以上、もこでした。