複数AWSアカウントにlambdaをデプロイする方法

# 要約
最終的に↓のようなことがやりたいので
![AWS構成-Page-10(2).png](https://qiita-image-store.s3.amazonaws.com/0/219595/df15dc65-7a9e-779d-3ebc-bc159fe4e079.png)


この記事では↓をやるための設定を書きます。
※ここまでできれば、あとは作り込めばやりたいことはできるはず
![AWS構成-Copy of Page-10.png](https://qiita-image-store.s3.amazonaws.com/0/219595/1a251e68-459a-5f48-7a74-4f5d6d009473.png)

# 作業内容
以下の順番で設定をします。なお、以降ではアカウントAを管理者アカウント、アカウントBをターゲットアカウントと言います。


* 管理者アカウントにてCloudFormationでスタックを作成する
* 作成したスタックインスタンスを削除する(optional)
* スタックインスタンスの作成とlambdaのデプロイをするCodeBuildを作成する

## 管理者アカウントにてCloudFormationでスタックを作成する
以下のようなlambdaを作成するスタックを、CloudformationのStackSetsで作成します。
なお、StackSetsの初回の設定で、1つ以上のスタックインスタンスを作る必要があるため、ターゲットアカウントの指定が必要となります。
最終的には、CodeBuildを実行するたびにスタックインスタンスを指定し、ターゲットアカウントの環境構築をするため、この設定は意味がないですが、UI的に指定しないとスタックを作成することができないので、ターゲットアカウントの設定も行います。
設定については
[前提条件: スタックセットオペレーションのアクセス権限の付与](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html)
を参考にして下さい。
以下の図を1セットだけ作るイメージです。
![stacksets_perms_master_target.png](https://qiita-image-store.s3.amazonaws.com/0/219595/ffbb00e0-efb2-7a01-db23-91ee5dcadacb.png)

権限設定が終わったら、以下のテンプレートでスタックを作成します。
空のpythonのLambdaと、Lambdaの実行権限だけ追加したIAMが、ターゲットアカウントに追加されるテンプレートです。

```
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "sample of cloudformation Stackset Template.",
  "Resources" : { 
    "SampleLambdaStackSet": {
      "Type" : "AWS::Lambda::Function",
      "Properties" : { 
        "Code": {
          "ZipFile": { "Fn::Join": ["", [
            "nothing"
          ]]} 
        },  
        "FunctionName": "SampleLambdaStackSet",
        "Runtime": "python3.6",
        "Role" : { "Fn::GetAtt" : ["LambdaExecutionRole", "Arn"] },
        "Handler": "lambda_function.lambda_handler",
        "Description": "", 
        "Timeout": 3,
        "MemorySize": 128 
      }   
    },                                                                                                                                                                                                      
    "LambdaExecutionRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }]
        }   
      }   
    }   
  }
}

```

スタックを作成し、成功すると、ターゲットアカウントに「SampleLambdaStackSet」と「LambdaExecutionRole」が追加されているはずです。

# 作成したスタックインスタンスを削除する(optional)
最終的に、スタックインスタンスは動的に増やせるようにしたいので、CodeBuildで12桁のアカウントIDを指定してスタックインスタンスを作成します。
ですので、スタックにスタックインスタンスがない状態としておきます。
上記で作成したスタックセットの、「スタックセットの管理」から「スタックの削除」を行い、一番最初に指定したターゲットアカウントを指定して、スタックインスタンスを削除します。
この作業をoptionalとした理由は、cloudformationで作成される環境は冪等なので、該当のターゲットアカウントにスタックインスタンスが「存在している状態でもう一度クリエイトをする」のと「存在しない状態でクリエイトする」のは、結果が同じになるためです。

# スタックインスタンスの作成とlambdaのデプロイをするCodeBuildを作成する
以下のbuildspec.ymlを利用して、管理者アカウントでCodeBuildをします。

* codebuildのEnvironmentはubuntuのpython
* ターゲットアカウントのアカウントIDは123456789012
* Cloudformationのスタックセット名はcloudformation-stackset
* codecommitにはlambdaというディレクトリがあり、これをzip圧縮してlambdaにデプロイする

こととしています。

```
version: 0.2                                                                                                                                                                                                

phases:
  install:
    commands:
      - echo 'install phase'
      - apt update -y
      - apt install -y zip 
      - apt -y install jq
      - mkdir ~/.aws
      - echo  '[123456789012]' > ~/.aws/credentials
      - echo  'role_arn = arn:aws:iam::123456789012:role/AWSCloudFormationStackSetExecutionRole' >> ~/.aws/credentials
      - echo  'credential_source = EcsContainer' >> ~/.aws/credentials

  build:
    commands:
      - echo 'build phase'
      - zip -r lambda.zip ./lambda/*
      - operationId=`aws cloudformation create-stack-instances --stack-set-name cloudformation-stackset --accounts '["123456789012"]' --regions '["ap-northeast-1"]' --operation-preferences FailureToleranceCount=0,MaxConcurrentCount=1 | jq -r '.OperationId'`
      - echo $operationId
      - sleep 20s 
      - status=`aws cloudformation describe-stack-set-operation --stack-set-name cloudformation-stackset --operation-id $operationId | jq -r '.StackSetOperation.Status'`
      - echo $status
      - aws lambda update-function-code --profile 123456789012 --function-name arn:aws:lambda:ap-northeast-1:123456789012:function:SampleLambdaStackSet --zip-file fileb://lambda.zip --publish

```
上記が成功すると、ターゲットアカウントにlambdaとIAMが作成され、lambdaのソースコードがデプロイされます。

## 注意点
・codebuildの実行ロールについて
「CodeBuildのデフォルトロールのポリシー」「AWSCloudFormationStackSetAdministrationRoleと同じポリシー(AWSCloudFormationStackSetExecutionRoleをsts:AssumeRoleするやつ)」と「cloudformation:CreateStackInstances」と「cloudformation:DescribeStackSetOperation」が設定してあります。

・スタックインスタンスの完了待機について
本来はcreate-stack-instancesでのスタックインスタンスの作成が完了するまで待つ必要がありますが、ここでは20秒待機だけしています。具体的にはdescribe-stack-set-operationで取得した$statusが「RUNNING」の間はポーリングが必要で「SUCCESS」以外に変わった場合はエラーにしたら良いと思います。

・複数のAWSターゲットアカウントにデプロイする場合
該当アカウントへAWSCloudFormationStackSetExecutionRoleの作成と、buildspec.ymlのcredentialsの追記、各cloudformationのコマンド実行を繰り返す必要があります。

・lambdaをデプロイするためのロールについて
ロール(profile 123456789012)では、AWSCloudFormationStackSetExecutionRoleを使っていますが、lambdaをデプロイするためだけの場合は過剰です。気になるならターゲットアカウントにロールを作成し、管理者アカウントのcodebuildを行うロールにsts:AssumeRoleを設定しましょう。

・ソースコードのデプロイについて
ソースコードをデプロイするだけの場合、いちいちスタックインスタンスの作成しなおしが入るので、若干無駄です。環境構築と資産のデプロイし直しは、別のビルドにしたほうが良いかもしれないです。

・もっといい方法ないの？
いっそのことgithubとかにソースを持っといて、各環境にcodebuildしたほうが楽かもしれない、それかビルドでS3にzip保存だけやって、ターゲットアカウントからzipを取りに来るとか・・・？