🎃

Serverless FrameworkとCustom Docker(Go)でAPI GatewayとLambdaを作成する

2022/01/29に公開

概要

Serverless FrameworkとCustom Dockerを用いて、API GatewayとLambdaを作成する方法を記載します。言語はGoを使用しています。

背景

SlackからBot経由で、ユーザの入力に対する処理をする仕組みを作成しようとしていました。
イベントを受け付けるためにAPI Gatewayを用意し、処理をLambdaで実装することにしました。
このLambdaの処理内でいくつか使用したいパッケージがありましたが、処理を実行するOSの中に含まれていなかったため、Custom Docker内でインストールする必要がありました。

作成手順

  1. サーバーレスで雛形の作成
  2. Custom Dockerの作成
  3. AWSリソースの用意
  4. serverless.ymlの設定
  5. デプロイと動作確認

サーバーレスで雛形の作成

まずはディレクトリを作成します。

mkdir -p serverless-with-custom-docker-example && cd $_

次にserverlessをインストールします。

npm install serverless

次にaws-go-modを用いて、雛形を作成します。

sls create --template aws-go

Custom Dockerの作成

基本的には、AWSの公式ドキュメントにあるコンテナイメージを使用して Go Lambda 関数をデプロイするを見て、必要な箇所を記載します。

Dockerfileの準備

まずはDockerfileを記載します。

Dockerfile
FROM public.ecr.aws/lambda/provided:al2

COPY bin/hello /var/runtime/bootstrap

CMD ["/var/runtime/bootstrap"]

イメージはAWSが提供しているpublic.ecr.aws/lambda/provided:al2を使用します。

バイナリは/var/runtime/bootstrapに配置します。

これは、provided:al2のENTRYPOINTに/lambda-entrypoint.shが指定され、この中で/var/runtime/bootstrapがRUNTIME_ENTRYPOINTとして固定されているからです。

provided:al2ののENTRYPOINTは以下のように確認できます。

$ docker inspect public.ecr.aws/lambda/provided:al2 | jq -c ".[].Config.Entrypoint"
["/lambda-entrypoint.sh"]

/lambda-entrypoint.shは以下のように確認できます。

$ docker run --rm --entrypoint "cat" public.ecr.aws/lambda/provided:al2 /lambda-entrypoint.sh

#!/bin/sh
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.

if [ $# -ne 1 ]; then
  echo "entrypoint requires the handler name to be the first argument" 1>&2
  exit 142
fi
export _HANDLER="$1"

RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
else
  exec $RUNTIME_ENTRYPOINT
fi

/usr/local/bin/aws-lambda-rieは、Lambda コンテナイメージをローカルでテストするためのエミュレータです。詳しく情報はLambda コンテナイメージをローカルでテストするに記載されています。

もしRUNTIME_ENTRYPOINTを変更したい場合、下記のようにentry.shを用意し、DockerfileのENTRYPOINTを変えてあげると動作します。

entry.sh
#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie "$@"
else
  exec "$@"
fi
Dockerfile
FROM public.ecr.aws/lambda/provided:al2

COPY entry.sh /

RUN chmod 755 /entry.sh

COPY bin/hello /main

ENTRYPOINT ["/entry.sh"]

CMD ["/main"]

ローカルでLambdaの動作確認

上記Dockerfileを作成したら、次はローカルでLambdaの動作確認を行います。

まずは、goの環境を用意します。

go mod init github.com/kazune-br/serverless-with-custom-docker-example
go mod tidy

次に、ソースコードとイメージをビルドします。

env GOARCH=amd64 GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go
docker build --platform linux/x86_64 -t serverless-with-custom-docker-example:latest .

準備が出来たらdockerを起動します。

docker run --rm -p 9000:8080 serverless-with-custom-docker-example:latest

下記のcurlを実行します。成功すると記載したものと同じレスポンスが返ってきます。

curl -s "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' | jq
{
  "statusCode": 200,
  "headers": {
    "Content-Type": "application/json",
    "X-MyCompany-Func-Reply": "hello-handler"
  },
  "multiValueHeaders": null,
  "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\"}"
}

AWSリソースの用意

lambdaのデプロイメントバケットとECRの用意をします。
*普段はterraformで作成していますが、この記事だけを見てさっと出来るようにaws cliから作成しています。

S3バケットの作成

下記コマンドでバケットを作成します。

aws s3 mb s3://your-bucket-name

serverless-deployment-bucketを使用して作る方法もあります。

ECRの作成

下記コマンドでECRを作成します。

aws ecr create-repository \
	--repository-name serverless-with-custom-docker-example \
	--image-scanning-configuration scanOnPush=true

*docker-image-deployments-in-ecrを使用して作る方法もあります。

serverless.ymlの設定

全体像

serverless.yml
serverless.yml
service: serverless-with-custom-docker-example

frameworkVersion: '3'

provider:
  name: aws
  runtime: go1.x
  stage: ${opt:stage, self:custom.defaultStage}
  region: "ap-northeast-1"
  deploymentBucket: "your bucket name"
  lambdaHashingVersion: 20201221
  timeout: 29
  memorySize: 128
  stackTags:
    env: ${opt:stage, self:custom.defaultStage}
  logs:
    httpApi: true

custom:
  defaultStage: stg
  customDomain:
    certificateName: "paste your certificate name"
    domainName: "paste your desired domain name"
    stage: ${self:provider.stage}
    createRoute53Record: true
    endpointType: regional
    securityPolicy: tls_1_2
    apiType: http

plugins:
  - serverless-domain-manager

package:
  patterns:
    - '!./**'

functions:
  hello:
    image:
      uri: "paste your ecr uri"
# If you want to override command and entryPoint, you cat specify as below.
#      command:
#        - "/main"
#      entryPoint:
#        - "/entry.sh"
    timeout: 29
    role: ServerlessWithCustomDockerExampleRole
    events:
      - httpApi:
          path: /hello
          method: get

resources:
  Resources:
    ServerlessWithCustomDockerExampleRole:
      Type: AWS::IAM::Role
      Properties:
        Path: /
        RoleName: serverless-with-custom-docker-example-${self:provider.stage}-role
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Policies:
          - PolicyName: serverless-with-custom-docker-example-${self:provider.stage}-policy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: "Allow"
                  Action:
                    - "logs:CreateLogGroup"
                    - "logs:CreateLogStream"
                    - "logs:PutLogEvents"
                  Resource:
                    - "arn:aws:logs:*:*:/aws/lambda/*"
``` 

Custom Dockerの設定

基本的には、serverlessのblogにあるContainer Image Support for AWS Lambdaを見て、必要な箇所を記載します。

変更箇所はfunctionsディレクティブだけです。
テンプレートから作成したときは下記のように記載されています。

serverless.yml
functions:
  hello:
    handler: bin/hello
    events:
      - httpApi:
          path: /hello
          method: get

handlerの部分を、imageに書き換え、uriに先程作成したecrのuriを記載するだけです。
commandやentryPointを変更したい場合は、下記のコメントアウトを外してあげると指定することが出来ます。

serverless.yml
functions:
  hello:
    image:
      uri: "paste your ecr uri"
# If you want to override command and entryPoint, you can specify as below.
#      command:
#        - "/main"
#      entryPoint:
#        - "/entry.sh"
    events:
      - httpApi:
          path: /hello
          method: get

デプロイと動作確認

まずはECRに作成したImageをPushします。

account_id=$(aws sts get-caller-identity | jq .Account)
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com

service_name=serverless-with-custom-docker-example

docker tag ${service_name}:latest ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/${service_name}:latest

docker push ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/${service_name}:latest

次にserverlessでAPI GatewayとLambdaを作成します。

sls deploy --stage stg

デプロイ完了後、ログにURLが表示されます。
そこにリクエストを送り、ローカルで試したときと同じ結果が得られれば完了です。

Discussion