クロスアカウントでLambdaの自動デプロイをしてみた

今回`Lambda`の自動デプロイをクロスアカウントで実行したので覚書として残しておきます。
`Lambda`のデプロイには`CloudFormation`で実施しています。
`Lambda`の`CloudFormatino`の設定については下記を参照してください。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html
完成図としては以下となります。
![アーキ図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556143/6240f03e-c320-48ea-b4d3-c0fc59d99d6b.png)




* ソースコードは開発アカウントの`CodeCommit`で管理します。
* masterブランチにソースコードがPUSHされると、開発環境の`CodePipline`が動作します。
* 開発アカウントの`Codebuild`上で``CloudFormatino`を利用して`Lambda`のパッケージ化とデプロイが行われます。
* 開発アカウント上のS3に`Lambda`のソースコードが配置されたのをトリガーに検証・本番アカウントのS3にコピーを行います。
* 検証アカウントでは自アカウント内のS3にファイルがアップロードされたのを契機に`CodePipline`を実行して`Lambda`のデプロイを行います。
* 本番アカウントでは未テストでデプロイされるの防ぐため、手動で`CodePipeline`を実行します。

# CodeCommit、CodePipelineの設定
クロスアカウントの`CodeCommit`の連携については下記を参考にしました。
https://dev.classmethod.jp/cloud/aws/angular-spa-auto-deploy-across-account/
ほとんどこのままなので以降は各環境での`CodeCommit`の連携、`CodePipline`のプロジェクトは作成できている前提で書いていきます。

# 開発アカウントでの設定
開発アカウントでは`CodeBuild`上の`buildspec.yaml`で`aws cli`を利用してソースコードのパッケージ化とデプロイを行います。

```buildspec.yaml
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
    commands:
      - apt-get update
      - apt-get install -y zip sed
  build:
    commands:
- aws cloudformation package --template-file [任意のパス]/[CloudFormatinoテンプレートファイル] --s3-bucket [S3バケット名] --s3-prefix [任意のプレフィックス] --output-template-file [任意のパス]/packaged-template.yml
- aws cloudformation deploy --template-file [任意のパス]/packaged-template.yml --stack-name [スタック名] --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
 
```
上記で開発アカウントでのデプロイは完了です。
`aws cloudformation package`でS3に`Lambda`のソースコードが配置されるので、それをトリガーに設定したLambda関数を作成します。

```index.js
var AWS = require('aws-sdk');
var s3 = new AWS.S3();

exports.handler = async (event) => {
    const copyParams = {
    Bucket: [検証アカウントバケット名],
    CopySource: event.Records[0].s3.bucket.name + '/' + event.Records[0].s3.object.key,
    Key: event.Records[0].s3.object.key,
    ACL: 'bucket-owner-full-control'
    };
    
    await s3.copyObject(copyParams, function(err, data) {
        console.log('検証コピースタート');
        if (err) {
            console.log('失敗！');
            console.log(err);
        } else {
            console.log('成功！');
        }
    }).promise();
    
    const copyParamsForProd = {
    Bucket: [本番アカウントバケット名],
    CopySource: event.Records[0].s3.bucket.name + '/' + event.Records[0].s3.object.key,
    Key: event.Records[0].s3.object.key,
    ACL: 'bucket-owner-full-control'
    };
    
    await s3.copyObject(copyParamsForProd, function(err, data) {
        console.log('本番コピースタート');
        if (err) {
            console.log('失敗！');
            console.log(err);
        } else {
            console.log('成功！');
        }
    }).promise();

};

```
コピーしたファイルは、コピー先のアカウントで自由に扱えるように`bucket-owner-full-control`を設定しています。
以上で、開発アカウントでの設定は完了です。

# 検証アカウントでの設定
開発アカウントで作成したLambda関数が正しく動作していれば、検証アカウントのS3にファイルがアップロードされるので、それをトリガーに設定したLambda関数を作成します。
※開発環境の`CodeCommit`と連携しているので、masterにPUSHされた時点でCodePiplineを実行することも可能ですが、なるべく開発環境と同一のファイルを利用してデプロイを行いたいので今回は明示的にその設定をOFFとしています。

```yaml
PollForSourceChanges: false
```

```index.js
var AWS = require('aws-sdk');
var s3 = new AWS.S3();
var codepipeline = new AWS.CodePipeline();

exports.handler = async (event) => {
    // オブジェクトキーの取得
    const key = event.Records[0].s3.object.key;
    const arrKey = key.split('/');
    
    // 要素の末尾であるファイル名を削除
    arrKey.pop();
    
    // pipelineのファンクション名となるように'-'区切りで文字列結合
    const pipelineName = arrKey.join('-');
    
    const pipelineParams = {
        name: `${pipelineName}`
    };
    // codepipelineの実行フラグ。
    var pipelineExecFlg = true;
    
    // codepipelineの実行ステータスを取得。
    const data = await codepipeline.getPipelineState(pipelineParams).promise();
    console.log(data);
    
    // 実行中であればcodepipelineの実行フラグをOFFにする。
    if (data.stageStates[0].latestExecution.status == 'InProgress' || data.stageStates[1].latestExecution.status == 'InProgress') {
            pipelineExecFlg = false;
        }
    // codepipeline実行
    if (pipelineExecFlg) {
        await codepipeline.startPipelineExecution(pipelineParams, function(err, data) {
        if (err) console.log(err, err.stack);
        else console.log(data);
    }).promise();   
    }
};

```
今回は`CodePipeline`のプロジェクト名がS3にアップロードされる際のプレフィックス名となるように合わせています。ここは実行できれば何でもいいです。

上記のLambdaが正しく動けば、`CodePipeline`が実行されます。
`codebuild`で実行する際のbuildspec.ymlは以下のように設定しています。

```buildspec.yaml
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
    commands:
      - apt-get update
      - apt-get install -y zip sed
  build:
    commands:
      ############################## deploy lambda
      ## 開発アカウントからコピーされたオブジェクトが配置されているバケット名を取得する（本番環境でもこのファイルを使用するため、SSMパラメータから取得するように設定）
      - BUCKET_NAME=`aws ssm get-parameters --names [SSMパラメータ名] | jq -r .Parameters[0].Value`
      ## 最新のオブジェクトを取得
      - OBJECKT_KEY=`aws s3api list-objects --bucket $BUCKET_NAME --prefix [オブジェクト配置プレフィックス] | jq -r '.Contents | sort_by(.LastModified) | reverse | .[0] | .Key'`
      ## テンプレートファイル内で設定したソースコード配置先を上記2つで取得した値に置換する。
      - sed -i -e "s@src/@`echo s3://$BUCKET_NAME/$OBJECKT_KEY`@" [CloudFormationテンプレートファイル]
      ## 本番環境との差異をなくすため、テンプレート内で使用するパラメータの値を取得する。
      - CloudFormationParameter=`aws ssm get-parameters --names [SSMパラメータ名] | jq -r .Parameters[0].Value`
      ## デプロイ実行
      - aws cloudformation deploy --template-file [CloudFormationテンプレートファイル] --stack-name [スタック名] --no-fail-on-empty-changeset --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM --parameter-overrides CloudFormationParameter=$CloudFormationParameter
      
```
* なるべく開発環境で設定したファイルと同一のファイルをしたいため、検証・本番アカウントでは`aws cloudformation package`は使用せずに、コピーされたファイルを利用して`aws cloudformation deploy`のみを行います。
* 本番環境でも同一の`buildspec.yaml`を使用するため、`CloudFormation`のパラメータやS3バケットは`SSMパラメータ`から取得してくるように設定します。
 
# 本番アカウントでの設定
本番アカウントは基本的に検証アカウントと同一です。違いは以下の一点です。
本番アカウントでは手動でのテスト前に自動でデプロイされると困るので、`CodePipeline`を実行する`Lambda`は作成しません。
検証環境でのテストも問題なければ、AWSコンソール上で`CodePipline`を開き「変更をリリースする」を押下すれば検証環境と同様にデプロイが行われます。