コンテナイメージから作るLambdaをSAMでデプロイ

# はじめに

以前、Microsoft365の監査ログを取得するLambdaをコンテナで作りました。

https://qiita.com/a_b_/items/84d6ae3cf8a21b8b332c

これを以下のページを参考に、Cloud9上でAWS SAM CLIでのデプロイにチャレンジしてみました。

https://aws.amazon.com/jp/builders-flash/202107/new-lambda-container-development-4/?awsf.filter-name=*all

# 所感

- AWS SAM CLIを用いると、デプロイまでの手間が減る
    - 特に、ECRの作成や管理を考えなくてよいのが大変助かる
        - 上記ページでは作っていますが、現時点では不要のようです
- (Cloud9上の)AWS Toolkitのウィザードでも試しましたが、launch.json を使ったデバッグ実行がうまくいかなかった
    - AWS SAM CLIのナビゲーションで十分便利なので、GUIのウィザードを使わなくてもよいかも

# 環境
- Cloud9
    - コンテナを使うのでそこそこのものを
    - Lambdaのコンテナを作ったものと同じ環境です
  
 https://qiita.com/a_b_/items/d9398fd6efe0f66b6c6f#%E7%92%B0%E5%A2%83

# やったこと

## 前提
必要なスクリプト等はすべて作成済みとします。以前の記事で`docker build`の前の状態までになります。

https://qiita.com/a_b_/items/84d6ae3cf8a21b8b332c#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88dockerfile

## プロジェクト作成

いきなり`sam init`から始めます。指示の中でProject nameを問われ、そのディレクトリ以下に展開されます。

```bash
$ sam init

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
        2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 2

Which base image would you like to use?
        1 - amazon/nodejs14.x-base
        2 - amazon/nodejs12.x-base
        3 - amazon/nodejs10.x-base
        4 - amazon/python3.9-base
        5 - amazon/python3.8-base
        6 - amazon/python3.7-base
        7 - amazon/python3.6-base
        8 - amazon/python2.7-base
        9 - amazon/ruby2.7-base
        10 - amazon/ruby2.5-base
        11 - amazon/go1.x-base
        12 - amazon/java11-base
        13 - amazon/java8.al2-base
        14 - amazon/java8-base
        15 - amazon/dotnet5.0-base
        16 - amazon/dotnetcore3.1-base
        17 - amazon/dotnetcore2.1-base
Base image: 4

Project name [sam-app]: get-ms365auditlog      

Cloning from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
        1 - Hello World Lambda Image Example
        2 - PyTorch Machine Learning Inference API
        3 - Scikit-learn Machine Learning Inference API
        4 - Tensorflow Machine Learning Inference API
        5 - XGBoost Machine Learning Inference API
Template selection: 1
```
作成されたプロジェクトディレクトリは以下の用にファイルが配置されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/32473881-03b0-fa5e-d79b-f0319c589b7c.png)

参考ページにあるとおりにほとんど削除し、必要なものをコピーして以下の様にしました。(RIEは今回使いません。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/2685284e-2573-7e34-b9a8-e68f8fdd5b99.png)

## sam build

template.yamlを以下のように書き換えました。
Outputsを指定していますが、今回は使いません（いずれ使うことがあるかと思い記載しています）。

<details><summary>クリックで表示</summary><div>

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Get Audit Log from MS365

Parameters:
  GetAuditLogFunctionName:
    Type: String
  TargetBucket:
    Type: String
  TargetParameterStore:
    Type: String

Globals:
  Function:
    Timeout: 900

Resources:

  ########################################################
  ### GetAuditLogFunction
  ########################################################
  
  # Log Group
  GetAuditLogFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${GetAuditLogFunctionName}"
      RetentionInDays: 3653

  # IAM Role
  GetAuditLogFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "for-lambdafunction-${GetAuditLogFunctionName}"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: '/service-role/'
      Policies:
        # CloudWatch
        - PolicyName: write-cloudwatchlogs
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${GetAuditLogFunctionName}:*"    
        # S3 
        - PolicyName: write-s3object
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - 's3:PutObject'
                Resource: !Sub "arn:aws:s3:::${TargetBucket}/*"    
        # SSM Parameter Store
        - PolicyName: get-ssmparameter
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - 'ssm:GetParameter'
                Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${TargetParameterStore}"  

  # Lambda Function
  GetAuditLogFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref GetAuditLogFunctionName
      Role: !GetAtt GetAuditLogFunctionRole.Arn
      MemorySize: 384
      PackageType: Image
      Architectures:
        - x86_64
      Environment:
        Variables:
          SSMPARAMETERNAME: !Ref TargetParameterStore
          
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./src/
      DockerTag: get-ms365auditlog

Outputs:
  GetAuditLogFunction:
    Description: "Get Audit Log Lambda Function ARN"
    Value: !GetAtt GetAuditLogFunction.Arn
  # GetAuditLogFunctionIamRole:
  #   Description: "Implicit IAM Role created for GetAuditLogFunction"
  #   Value: !GetAtt GetAuditLogFunctionRole.Arn
```
</div></details>

一度に全て書いていますが、小さい部分から少しずつ作成する場合は、”Lambdaで環境変数を使用する場合はその旨明示しておき、Buildする”必要があります。

https://blog.deresuke.org/2018-01-05/

明示しないままだと、この後のlocal invokeにて環境変数が指定できません。

問題なければBuildします。docker buildと同様に、イメージが作られます。
```
sam build
```

## sam local invoke
作られたイメージを使って、SAM CLIのローカル環境での実行機能から実行してみます。
与えるイベントや環境変数はファイルに作成して、実行時のコマンドで渡す形になります。

```json:events\event.json
{
 "StartDate":"2022-08-24 15:00:00",
 "EndDate":"2022-08-24 15:59:59",
 "BucketName":"<<BUCKET_NAME>>",
 "Key":"testwrite/20220824150000.json"
}
```

環境変数を指定するファイルの記載は公式にあります。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-using-invoke.html

今回はtemplate.yamlのLambda関数の論理IDを指定しています。


```json:vars.json
{
  "GetAuditLogFunction": {
    "SSMPARAMETERNAME": "GetMs365AuditLog-ExchangeOnlineParameters"
  }
}
```

実行は以下のコマンドになります。

```
sam local invoke -n vars.json -e events/event.json 
```


## sam deploy

問題なければDeployです。事前にECRを作っておく必要はありません。
今回はIAM roleも作るので、`--capabilities CAPABILITY_NAMED_IAM`が必要になります。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html

```
sam deploy --guided --capabilities CAPABILITY_NAMED_IAM
```

あとはナビゲーション通りに入力するだけです。

作成されたCFn。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b37d46e7-c71a-627d-fc88-cc1eb06ed780.png)

自動作成されたECR。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/f6d86a4f-4513-b800-ca59-5661bae52c0e.png)

## 実行

以前と同様に実行できます。

```bash
# リージョンとアカウントIDを環境変数にセット
REGION="ap-northeast-1"
BUCKETNAME="<<BUCKET_NAME>>"

# 実行
aws lambda invoke \
  --function-name get-ms365auditlog \
   --payload "{
    \"StartDate\":\"2022-08-24 15:00:00\",
    \"EndDate\":\"2022-08-24 15:59:59\",
    \"BucketName\":\"${BUCKETNAME}\",
    \"Key\":\"testwrite/20220824150000.json\"
    }"\
   --region ${REGION} output ; cat output
```

## 変更
コードを変更して反映するまでを試してみました。
ログがうるさいのでSTDOUTは出さないようにしてみました。

```python:function\app.py
(前略)

  cmd = ['pwsh', '-Command', f'/script/GetMs365AuditLog.ps1 {encoded_event_and_context}']
  o = subprocess.run(cmd, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  # stdout = o.stdout.strip()
  stdout = ""
  stderr = o.stderr.strip()
  logger.info( json.dumps(stdout, ensure_ascii=False, indent=2) )
  logger.info( json.dumps(stderr, ensure_ascii=False, indent=2) )

(後略)

```

コマンドはBuildして、（必要に応じてローカル実行して、）Deployするだけになります。
Deployの設定は初回の内容をファイル保存していれば、それがデフォルト値になり、Enterしているだけで済みました。

```bash
sam build

# 必要であれば
sam local invoke -n vars.json -e events/event.json

# 初回の設定がデフォルト値に。基本Enter連打
sam deploy --guided --capabilities CAPABILITY_NAMED_IAM

aws lambda invoke \
  --function-name get-ms365auditlog \
   --payload "{
    \"StartDate\":\"2022-08-24 15:00:00\",
    \"EndDate\":\"2022-08-24 15:59:59\",
    \"BucketName\":\"${BUCKETNAME}\",
    \"Key\":\"testwrite/20220824150000.json\"
    }"\
   --region ${REGION} output ; cat output
```

## 片づけ

`sam delete`一つで、CFnや自動作成されたECRまで消してくれます。

```
 $ sam delete
        Are you sure you want to delete the stack teststack20220825 in the region ap-northeast-1 ? [y/N]: y
        Are you sure you want to delete the folder teststack20220825 in S3 which contains the artifacts? [y/N]: y
        Found ECR Companion Stack teststack20220825-1c6a0a04-CompanionStack
        Do you you want to delete the ECR companion stack teststack20220825-1c6a0a04-CompanionStack in the region ap-northeast-1 ? [y/N]: y
        ECR repository <<ECRリポジトリ名>> may not be empty. Do you want to delete the repository and all the images in it ? [y/N]: y
```

消されたCFn。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/e84b0009-376c-7aef-f66a-89a0628f7cc8.png)

ロールもロググループもLambdaも削除される（Lambdaだけ画像無し）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/bdb84ebf-1ae8-8a8e-ec2c-c86af81fbb0c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/1b714d32-94de-142b-c2bf-07f7a95a4918.png)

ECRのリポジトリも消えていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/1b101ecd-ea54-681d-de11-9170d4bd69f6.png)

あとは`docker images`と`docker rmi`で対象のイメージを削除していけば全部消えます。


# おわりに
今回はコンテナLambdaをSAMでデプロイする方法を試してみました。
以前のbashのコマンドによる方法と比べて、かなり簡単に作成・変更ができました。

大変便利なので、本番ではこれを使って運用していきたいと思います。