Traffine I/O

日本語

2023-03-13

AWS LambdaをAWS CLIを使ってデプロイ

はじめに

この記事では、AWS LambdaをAWS CLIを使ってデプロイするコードを紹介します。紹介するコードを使うと以下のことを実現することができます。

ディレクトリ構成

以下のディレクトリ構成に従ってコードを記述していきます。

├── Dockerfile
├── Makefile
├── deploy
│   ├── config
│   │   ├── policy.json
│   │   └── role.json
│   ├── push-ecr.sh
│   ├── provision-api-gateway.sh
│   ├── provision-ecr.sh
│   ├── provision-lambda.sh
│   └── update-lambda.sh
└── src/ # source code

環境変数の設定

以下の環境変数を設定します。

  • AWS_ACCOUNT_ID
  • AWS_REGION
  • IMAGE_NAME
  • LAMBDA_ALIAS
  • PROVISIONED_CONCURRENCY
# AWS account ID
$ export AWS_ACCOUNT_ID=123456789123

# AWS region
$ export AWS_REGION=ap-northeast-1

# ECR image and lambda name
$ export IMAGE_NAME=my-program

# Lambda alias
$ export LAMBDA_ALIAS=prod

# Number of Lambda Provisioned Concurrency
$ export PROVISIONED_CONCURRENCY=1

ECR関連コード

ECRのレポジトリを作成するシェルスクリプトdeploy/provision-ecr.shを作成します。

deploy/provision-ecr.sh
#!/bin/sh

# Create ECR repository
aws ecr create-repository --repository-name ${IMAGE_NAME} --region ${AWS_REGION}

そして、以下を実行するシェルスクリプトdeploy/push-ecr.shを作成します。

  1. ECRにログイン
  2. DockerfileからDockerイメージをビルド
  3. ビルドしたDockerイメージをECRにプッシュ
deploy/push-ecr
#!/bin/bash

# ECR repository URI
ECR_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:latest

# ECR login
aws ecr get-login-password | docker login --username AWS --password-stdin https://$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

# Build docker image
docker build -t $ECR_URI .

# Push docker image to ECR
docker push $ECR_URI

Lambda関連コード

Lambdaに権限を付与するためのIAM RoleとPolicyのJSONファイルを作成します。

deploy/config/policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
deploy/config/role.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

そして、以下を実行するシェルスクリプトprovision-lambda.shを作成します。

  1. Lambda用のIAM Policyをdeploy/config/policy.jsonから作成
  2. Lambda用のIAM Roleをdeploy/config/role.jsonから作成
  3. RoleにPolicyをアタッチ
  4. Lambdaを作成(ソースはECRのイメージ)
  5. Lambdaのバージョンとエイリアスを作成(Provisioned Concurrencyで使うため)
  6. LambdaにProvisioned Concurrencyをセット
deploy/provision-lambda.sh
#!/bin/bash

# Create IAM policy for lambda
aws iam create-policy --policy-name AWSLambdaBasicExecutionRole-${IMAGE_NAME} --policy-document file://deploy/config/policy.json

# Create IAM role for lambda
aws iam create-role --role-name ${IMAGE_NAME}-lambda-role --assume-role-policy-document file://deploy/config/role.json

# Sleep for waiting provision of policy and role creation
sleep 10

# Attach policy to role
aws iam attach-role-policy \
--role-name ${IMAGE_NAME}-lambda-role \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/AWSLambdaBasicExecutionRole-${IMAGE_NAME}

# Sleep for waiting attachment
sleep 10

# Create lambda
aws lambda create-function  \
  --function-name ${IMAGE_NAME} \
  --package-type Image \
  --code ImageUri=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMAGE_NAME}:latest \
  --timeout 600 \
  --memory-size 10240 \
  --ephemeral-storage Size=2048 \
  --role arn:aws:iam::${AWS_ACCOUNT_ID}:role/${IMAGE_NAME}-lambda-role


# Wait for Lambda function to be created
while true; do
  LAMBDA_STATUS=$(aws lambda get-function --function-name ${IMAGE_NAME} --query "Configuration.State" --output text)

  if [[ ${LAMBDA_STATUS} == "Active" ]]; then
    break
  elif [[ ${LAMBDA_STATUS} == "Failed" ]]; then
    echo "Lambda creation failed" >&2
    exit 1
  else
    echo "Waiting for Lambda function to be created..."
    sleep 10
  fi
done

echo "Lambda function created."

# Create lambda version and alias
LAMBDA_VERSION=$(aws lambda publish-version --function-name ${IMAGE_NAME} --region ${AWS_REGION} --query 'Version' --output text)

aws lambda create-alias \
  --function-name ${IMAGE_NAME} \
  --name ${LAMBDA_ALIAS} \
  --function-version ${LAMBDA_VERSION} \
  --region ${AWS_REGION}

# Add provisioned concurrency to lambda
aws lambda put-provisioned-concurrency-config \
  --function-name ${IMAGE_NAME} \
  --qualifier ${LAMBDA_ALIAS} \
  --provisioned-concurrent-executions "$PROVISIONED_CONCURRENCY" \
  --region ${AWS_REGION}

# Wait for provisioned concurrency to be configured
while true; do
  CONCURRENCY_STATUS=$(aws lambda get-provisioned-concurrency-config \
    --function-name ${IMAGE_NAME} \
    --qualifier ${LAMBDA_ALIAS} \
    --query "Status" --output text)

  if [[ ${CONCURRENCY_STATUS} == "READY" ]]; then
    break
  elif [[ ${CONCURRENCY_STATUS} == "FAILED" ]]; then
    echo "Provisioned concurrency addition failed" >&2
    exit 1
  else
    echo "Waiting for provisioned concurrency to be configured..."
    sleep 10
  fi
done

echo "Provisioned concurrency added to the Lambda function."

Lambdaを継続的に更新するためのシェルスクリプトupdate-lambda.shを作成します。update-lambda.shでは以下の内容が実行されます。

  1. 指定したECRイメージからLambdaを更新
  2. Lambdaのエイリアスを更新(エイリアスを更新するとProvisioned Concurrencyも更新される)
deploy/update-lambda.sh
# !/bin/bash

# Update lambda
aws lambda update-function-code --function-name ${IMAGE_NAME} --image ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMAGE_NAME}:latest

while true; do
  LAST_UPDATE_STATUS=$(aws lambda get-function --function-name ${IMAGE_NAME} --region ${AWS_REGION} --query 'Configuration.LastUpdateStatus' --output text)

  if [ ${LAST_UPDATE_STATUS} != "InProgress" ]; then
    break
  fi

  echo "Waiting for Lambda function update to complete..."
  sleep 10
done


NEW_LAMBDA_VERSION=$(aws lambda publish-version --function-name $IMAGE_NAME --region $AWS_REGION --query 'Version' --output text)

# Update lambda alias
aws lambda update-alias \
  --function-name $IMAGE_NAME \
  --name $LAMBDA_ALIAS \
  --function-version $NEW_LAMBDA_VERSION \
  --region $AWS_REGION

# Wait for provisioned concurrency to be configured
while true; do
  CONCURRENCY_STATUS=$(aws lambda get-provisioned-concurrency-config \
    --function-name ${IMAGE_NAME} \
    --qualifier ${LAMBDA_ALIAS} \
    --query "Status" --output text)

  if [[ ${CONCURRENCY_STATUS} == "READY" ]]; then
    break
  elif [[ ${CONCURRENCY_STATUS} == "FAILED" ]]; then
    echo "Provisioned concurrency addition failed" >&2
    exit 1
  else
    echo "Waiting for provisioned concurrency to be configured..."
    sleep 10
  fi
done

echo "Provisioned concurrency added to the Lambda function."

API Gateway関連コード

API Gatewayをセットアップするシェルスクリプトprovision-api-gateway.shを作成します。provision-api-gateway.shは以下を実行します。

  1. REST APIを作成
  2. POSTメソッドを作成
  3. Lambdaと統合
  4. Lambdaのパーミッションを設定
  5. API Gatewayをデプロイ
deploy/provision-api-gateway.sh
#!/bin/bash

LAMBDA_FUNCTION_ARN=arn:aws:lambda:${AWS_REGION}:${AWS_ACCOUNT_ID}:function:${IMAGE_NAME}

# Create REST API
REST_API=$(aws apigateway create-rest-api --name $IMAGE_NAME)
REST_API_ID=$(echo $REST_API | python -c "import sys, json; print(json.load(sys.stdin)['id'])")

# Get root resource ID
RESOURCE=$(aws apigateway get-resources --rest-api-id $REST_API_ID)
RESOURCE_ID=$(echo $RESOURCE | python -c "import sys, json; print(json.load(sys.stdin)['items'][0]['id'])")

# Create POST method for the root resource
aws apigateway put-method \
    --rest-api-id $REST_API_ID \
    --resource-id $RESOURCE_ID \
    --http-method POST \
    --no-api-key-required \
    --authorization-type NONE

# Create integration for Lambda function
aws apigateway put-integration \
    --rest-api-id $REST_API_ID \
    --resource-id $RESOURCE_ID \
    --http-method POST \
    --type AWS \
    --integration-http-method POST \
    --uri "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${LAMBDA_FUNCTION_ARN}:${LAMBDA_ALIAS}/invocations"

aws apigateway put-method-response \
    --rest-api-id $REST_API_ID \
    --resource-id $RESOURCE_ID \
    --http-method POST \
    --status-code 200 \
    --response-models '{"application/json": "Empty"}'

aws apigateway put-integration-response \
    --rest-api-id $REST_API_ID \
    --resource-id $RESOURCE_ID \
    --http-method POST \
    --status-code 200 \
    --response-templates '{"application/json": ""}'

# Add Lambda permission
aws lambda add-permission \
    --function-name $IMAGE_NAME:${LAMBDA_ALIAS} \
    --statement-id apigateway-access \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn "arn:aws:execute-api:${AWS_REGION}:${AWS_ACCOUNT_ID}:${REST_API_ID}/*/POST/"

# Deploy API Gateway
aws apigateway create-deployment \
    --rest-api-id $REST_API_ID \
    --stage-name prod

# Output API Gateway endpoint
echo "https://$REST_API_ID.execute-api.$AWS_REGION.amazonaws.com/prod"

Makefile

以下のようなMakefileを作成します。

Makefile
.PHONY: deploy-init deploy

deploy-init:
	sh deploy/provision-ecr.sh
	sh deploy/push-ecr.sh
	sh deploy/provision-lambda.sh
	sh deploy/provision-api-gateway.sh

deploy:
	sh deploy/push-ecr.sh
	sh deploy/update-lambda.sh

デプロイ

以下のコマンドを実行すると、Lambda、API Gatewayが初回デプロイされます。

$ make deploy-init

make deploy-initでは以下の処理が行われています。

  1. ECRレポジトリを作成
  2. Dockerをビルドし、ECRにプッシュ
  3. Lambdaを作成
  4. API Gatewayを作成

Dockerイメージの更新のタイミングで以下のコマンドを実行するとLambdaが更新されます。

$ make deploy

make deployでは以下の処理が行われています。

  1. Dockerをビルドし、ECRのプッシュ
  2. Lambdaを更新

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!