Lambdaをコンテナイメージから作る

# はじめに
AWSのLambdaはOSがAmazon Linux 2上で実行されるとのことです。ですがPowerShellの一部のモジュールなど、Amazon Linux 2上では動かないものもあったりします。

今回、別のOSで動かす必要があったため、自作のコンテナからLambdaを作成する方法が記載されいているページありましたので、それをやってみました。
普通になぞるだけでは味気なかったので、一部CloudFormationを使うようにしてみました。

# 参考
https://aws.amazon.com/jp/builders-flash/202103/new-lambda-container-development/?awsf.filter-name=*all


# 環境
- Cloud9
    - Docker
        - インストール済み
    - コンテナを扱うので、そこそこの容量は必要かも
        - 自分は50GBで行いました。

# やったこと

## ディレクトリ・ファイル作成

まずはプロジェクトのディレクトリを作成し、よく使う情報を環境変数にセットします。

```bash
# プロジェクトディレクトリ作成
$ mkdir sample-ecr-lambda
$ cd sample-ecr-lambda

# リージョンとアカウントIDを環境変数にセット
$ REGION="ap-northeast-1"
$ ACCOUNTID=$(aws sts get-caller-identity --output text --query Account)
```

次にLambdaで実行させるPythonスクリプトを作成します。
```bash
# pythonファイル作成
$ touch app.py
```
```python:app.py
import json

def handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "message": "hello world",
            }
        ),
    }
```

参考のページではまずZip圧縮ファイルを使ってLambdaを作成する手法を紹介しています。
私はZipで作るところは飛ばして、Dockerを作るところから続けています。

```bash
$ touch Dockerfile
```
```dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.8
COPY app.py   ./
CMD ["app.handler"]
```

Dockerイメージを作成します。
```bash
$ docker build -t func1 .
# func1 というイメージがあるか確認
$ docker images
```
## 一旦テスト
この時点でPythonが動くか確認します。
ベースとなるイメージがLambda用のものであれば、RIEというものが組み込まれており、それを使ってテストできます。

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

```bash
$ docker run --rm -p 9000:8080 func1:latest

# 別のターミナルから
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

# docker runしたターミナルで
Ctrl+C
```

## ECRリポジトリ作成・イメージPUSH
ECRをCFnで作成します。

```bash
# ECRリポジトリ作成CFn
$ touch createECRRepository.yaml
```
```yaml:createECRRepository.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Create ECR Stack Test

Resources:
  ########################################################
  ### ECR Repository
  ########################################################
  TestEcrPoc:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: func1
```
```bash
$ aws cloudformation create-stack --stack-name tempecrrepo --template-body file://createECRRepository.yaml --region ${REGION}
```

先程作成したイメージに、ECRのURIを参照先としてタグ付与します。
```bash
# タグ付与
$ docker tag func1:latest \
  ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1:latest
```

次にレジストリに対して Docker CLI を認証します。

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/getting-started-cli.html#cli-authenticate-registry

```bash
# 認証
$ aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com

Login Succeeded
```

作ったイメージをPUSHします。
```bash
# PUSH サイズは180MB程度
$ docker push ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1:latest
```

## Lambada作成
Lambadaを作成するCFnファイルを記述します。
```bash
# Lambda作るCFn
$ touch createLambda.yaml
```
たくさん書いてありますが、Lambda用のロググループとIAMロールを一緒に作っているだけです。
パラメータにはLambdaの名前とECRのイメージURIを指定するようにしています。
```yaml:createLambda.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Create ECR Stack Test

Parameters:
  LambdaFunctionName:
    Type: String
  ImageUri:
    Type: String
    
Resources:
  ########################################################
  ### Log Group
  ########################################################
  FunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaFunctionName}"
      RetentionInDays: 3653

  ########################################################
  ### IAM Role
  ########################################################
  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "for-lambdafunction-${LambdaFunctionName}"
      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/${LambdaFunctionName}:*"    

  ########################################################
  ### Lambda Function
  ########################################################
  TargetFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Ref LambdaFunctionName
      Role: !GetAtt FunctionRole.Arn
      PackageType: Image
      Code:
        ImageUri: !Ref ImageUri
```

パラメータに渡す文字列が長いため一度変数に格納し、Lambda作成時の引数に使います。
```bash
# CFnのパラメータに使う文字列を生成
$ DIGEST=$(aws ecr list-images --repository-name func1 --region ${REGION} --out text --query 'imageIds[?imageTag==`latest`].imageDigest')
$ IMAGEURI=${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1@${DIGEST}

# Lambda作成。IAM作るのでcapabilities指定
$ aws cloudformation create-stack --stack-name tempecrlambda \
  --template-body file://createLambda.yaml \
  --region ${REGION}  \
  --parameters \
    ParameterKey=LambdaFunctionName,ParameterValue=func1-container \
    ParameterKey=ImageUri,ParameterValue=${IMAGEURI} \
  --capabilities CAPABILITY_NAMED_IAM

# 実行
$ aws lambda invoke --function-name func1-container --region ${REGION} output ; cat output
```

# 片づけ
作ったものを削除していきます。CFnで作っているので簡単に削除できます。
```bash
# Lambda削除
$ aws cloudformation delete-stack --stack-name tempecrlambda --region ${REGION}

# ECR上イメージ削除
aws ecr batch-delete-image --repository-name func1 --region ${REGION} --image-ids imageDigest=${DIGEST}

# ECRリポジトリ削除
$ aws cloudformation delete-stack --stack-name tempecrrepo --region ${REGION}

# Dockerイメージを確認して、削除
$ docker images | grep func1
$ docker rmi <<IMAGENAME>>
```

# おわりに
今回はDockerイメージからLambdaを作成してみました。
Lambdaは普段SAMから作成していましたが、Dockerイメージから作成することで、Lambdaの用途がかなり広がると思います。
簡単に試すことができましたので、やったことない方は気軽に試してみてください。