API GatewayとLambdaのデプロイ中にAPIアクセスしてみた

API GatewayとLambdaで作成したWebAPIがあります。 デプロイ作業中にこのWebAPIへアクセスし続けた場合、どのような動作になるか試してみました。
2020.08.28

ふと気になりました。「Lambdaのデプロイ中にAPI経由でアクセスすると、どういう動きになるんだろう?」と。

  • ある時刻を境にバッチリ変わる?
  • デプロイ付近で新しいLambdaと古いLambdaがランダムに実行される?

試してみました。

おすすめの方

  • AWS SAMを使ってみたい方
  • API GatewayとLambdaでAPIを作ってみたい方
  • LambdaとDynamoDBを使ってみたい方
  • API GatewayとLambdaのデプロイ中にアクセスした場合の挙動に興味がある方

ざっくり概要

APIを1つ作成し、同時アクセスします。APIの裏にいるLambdaにて受け取ったデータをDynamoDBに書き込みます。 APIアクセスを続けている状態でLambdaをデプロイ(コード変更)したとき、どのような動きになるのか?を試してみました。

概要図

まずはWebAPIを作成する

SAM Init

sam init \
    --runtime python3.7 \
    --name api-lambda-deploy-sample \
    --app-template hello-world

SAMテンプレートファイル

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: api-lambda-deploy-sample-stack

Parameters:
  Number:
    Type: String

Resources:
  TodoFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 10
      AutoPublishAlias: Sample
      Environment:
        Variables:
          TABLE_NAME: !Ref TodoTable
      Policies:
        - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /todo
            Method: post

  TodoFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${TodoFunction}

  TodoTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub todo-sample-${Number}-table
      AttributeDefinitions:
        - AttributeName: todoId
          AttributeType: S
      KeySchema:
        - AttributeName: todoId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

Outputs:
  ApiEndpoint:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/todo"

Lambdaコード

DynamoDBに書き込むデータにlambda:aaaを含めています。新しいLambdaではこの内容を変更するため、デプロイ前後どちらのLambdaが書き込んだのかを区別できるようにしています。

app.py

import os
import json
import boto3

from datetime import datetime, timezone

dynamodb = boto3.resource('dynamodb')
table_name = os.environ['TABLE_NAME']

def lambda_handler(event, context):
    payload = json.loads(event['body'])
    table = dynamodb.Table(table_name)

    try:
        table.put_item(Item={
            'todoId': payload['todoId'],
            'title': payload['title'],
            'createdAt': int(datetime.now(timezone.utc).timestamp() * 1000),
            'lambda': 'aaa'
        })
    except Exception as e:
        print(e)
        return {
            "statusCode": 500,
            "body": json.dumps({
                "message": "ng",
            })
        }
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "ok",
        })
    }

AWS SAM デプロイ

下記でデプロイします。1としている部分の数字を変更することで、スタック名を変えて同じ環境を構築できるようにしています。

sam build

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-sam-test-bucket

sam deploy \
    --template-file packaged.yaml \
    --stack-name api-lambda-deploy-sample-1-stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset \
    --parameter-overrides Number=1

APIエンドポイントを取得

下記コマンドでAPIエンドポイントを取得し、メモしておきます。

aws cloudformation describe-stacks \
    --stack-name api-lambda-deploy-sample-1-stack \
    --query 'Stacks[].Outputs'

APIアクセスし続けるスクリプトを作成

下記のPythonスクリプトを作成します。同ディレクトリにfinish.txtが置かれるまで、APIアクセスを続けます。 API_ENDPOINTにはAPIエンドポイントを記載します。

poster.py

import json
import os
import time
import sys
import requests

# programme finish trigger file
FINISH_FILE = 'finish.txt'

API_ENDPOINT = 'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/todo'

def post(index):
    count = 1
    result_ok = 0
    result_ng = 0
    while True:
        payload = get_payload(index, count)
        resp = requests.post(API_ENDPOINT, data=json.dumps(payload))
        if resp.status_code == 200:
            result_ok += 1
        else:
            result_ng += 1
        if is_finish():
            break
        time.sleep(0.3)
        count += 1

    print(f'ok: {result_ok}')
    print(f'ng: {result_ng}')

def get_payload(index, count):
    return {
        'todoId': f't{index:02}-{count:04}',
        'title': 'アレを買う'
    }

def is_finish():
    if os.path.isfile(FINISH_FILE):
        return True
    return False

if __name__ == "__main__":
    args = sys.argv
    if len(args) == 2:
        post(int(args[1]))

やってみる

APIアクセスを開始する

APIアクセスするスクリプトを並列実行します。

python poster.py 1 &
python poster.py 2 &
python poster.py 3 &

Lambdaコードを修正する

下記に変更します。DynamoDBに書き込むデータについて、一部をaaaからxxxに変更しています。 これによって、デプロイ前後のどちらのLambdaから書き込んだのか区別できます。

app.py

import os
import json
import boto3

from datetime import datetime, timezone

dynamodb = boto3.resource('dynamodb')
table_name = os.environ['TABLE_NAME']

def lambda_handler(event, context):
    payload = json.loads(event['body'])
    table = dynamodb.Table(table_name)

    try:
        table.put_item(Item={
            'todoId': payload['todoId'],
            'title': payload['title'],
            'createdAt': int(datetime.now(timezone.utc).timestamp() * 1000),
            'lambda': 'xxx'
        })
    except Exception as e:
        print(e)
        return {
            "statusCode": 500,
            "body": json.dumps({
                "message": "ng",
            })
        }
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "ok",
        })
    }

デプロイする

sam build

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-sam-test-bucket

sam deploy \
    --template-file packaged.yaml \
    --stack-name api-lambda-deploy-sample-1-stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset \
    --parameter-overrides Number=1

デプロイ完了後、APIアクセスを停止する

touch finish.txt

結果

  • デプロイ付近で新しいLambdaと古いLambdaがランダムに実行される

スクリプト(APIアクセス)の結果

デプロイ途中でも、APIアクセス自体は成功しました。

回数 ok ng
1回目 252 0
2回目 280 0
3回目 274 0

CloudWatch Logsの様子

複数のLambdaが同時実行されました。

1回目

CloudWatch Logsの様子(1回目)

2回目

CloudWatch Logsの様子(2回目)

3回目

CloudWatch Logsの様子(3回目)

DynamoDBテーブルの様子

DynamoDBテーブルから全データを取得し、時刻順にソートしました。デプロイ時刻付近で新しいLambdaと古いLambdaが混在実行されていることが分かります。

1回目抜粋
↑aaaのみ
1598499496969: t03-0056, aaa
1598499497006: t02-0055, aaa
1598499497117: t01-0056, aaa
1598499497417: t02-0056, aaa
1598499497427: t03-0057, aaa
1598499498051: t01-0057, xxx
1598499498363: t03-0058, aaa
1598499498399: t02-0057, xxx
1598499498694: t01-0058, xxx
1598499499022: t03-0059, aaa
1598499499047: t02-0058, xxx
1598499499119: t01-0059, aaa
1598499499471: t03-0060, xxx
1598499499482: t02-0059, aaa
1598499499579: t01-0060, xxx
1598499499882: t02-0060, xxx
1598499499902: t03-0061, xxx
1598499500008: t01-0061, xxx
1598499500319: t02-0061, xxx
1598499500321: t03-0062, xxx
↓xxxのみ
2回目抜粋
↑aaaのみ
1598519657408: t01-0062, aaa
1598519657743: t02-0063, aaa
1598519657809: t03-0063, aaa
1598519658397: t01-0063, yyy
1598519658701: t02-0064, yyy
1598519658727: t03-0064, yyy
1598519659065: t01-0064, yyy
1598519659375: t02-0065, yyy
1598519659389: t03-0065, yyy
1598519659497: t01-0065, yyy
1598519659811: t02-0066, yyy
1598519659825: t03-0066, yyy
1598519659934: t01-0066, yyy
1598519660249: t02-0067, yyy
1598519660262: t03-0067, yyy
1598519660367: t01-0067, yyy
1598519660666: t02-0068, yyy
1598519660825: t01-0068, yyy
1598519661101: t02-0069, yyy
1598519661217: t03-0068, aaa
1598519661262: t01-0069, yyy
1598519661549: t02-0070, yyy
1598519661715: t01-0070, yyy
1598519661860: t03-0069, yyy
1598519661998: t02-0071, aaa
1598519662138: t01-0071, yyy
1598519662287: t03-0070, yyy
1598519662463: t02-0072, yyy
1598519662566: t01-0072, yyy
1598519662746: t03-0071, yyy
1598519662920: t02-0073, yyy
1598519663000: t01-0073, yyy
1598519663201: t03-0072, yyy
1598519663321: t02-0074, yyy
1598519663427: t01-0074, yyy
1598519663642: t03-0073, yyy
1598519663766: t02-0075, aaa
1598519663866: t01-0075, yyy
1598519664101: t03-0074, yyy
1598519664221: t02-0076, yyy
1598519664323: t01-0076, yyy
1598519664540: t03-0075, yyy
1598519664637: t02-0077, yyy
1598519664759: t01-0077, yyy
1598519664957: t03-0076, aaa
1598519665054: t02-0078, yyy
1598519665210: t01-0078, yyy
1598519665403: t03-0077, yyy
1598519665482: t02-0079, yyy
1598519665643: t01-0079, yyy
1598519665846: t03-0078, yyy
1598519665910: t02-0080, aaa
1598519666080: t01-0080, yyy
1598519666294: t03-0079, yyy
1598519666366: t02-0081, yyy
↓yyyのみ
3回目抜粋
↑aaaのみ
1598519930276: t03-0062, aaa
1598519930390: t02-0063, aaa
1598519931209: t03-0063, zzzzz
1598519931261: t01-0064, zzzzz
1598519931317: t02-0064, aaa
1598519931858: t03-0064, zzzzz
1598519931928: t01-0065, zzzzz
1598519931995: t02-0065, aaa
1598519932322: t03-0065, zzzzz
1598519932358: t01-0066, zzzzz
1598519932425: t02-0066, zzzzz
1598519932764: t03-0066, zzzzz
1598519932815: t01-0067, zzzzz
1598519932873: t02-0067, zzzzz
1598519933195: t03-0067, zzzzz
1598519933280: t01-0068, zzzzz
1598519933411: t02-0068, zzzzz
1598519933651: t03-0068, aaa
1598519933734: t01-0069, zzzzz
1598519933854: t02-0069, zzzzz
1598519934090: t03-0069, zzzzz
1598519934177: t01-0070, zzzzz
1598519934332: t02-0070, zzzzz
1598519934541: t03-0070, zzzzz
1598519934629: t01-0071, zzzzz
1598519934771: t02-0071, aaa
1598519934994: t03-0071, zzzzz
1598519935081: t01-0072, zzzzz
1598519935200: t02-0072, zzzzz
1598519935435: t03-0072, zzzzz
1598519935537: t01-0073, zzzzz
1598519935667: t02-0073, zzzzz
1598519935859: t03-0073, zzzzz
1598519935980: t01-0074, zzzzz
1598519936091: t02-0074, zzzzz
1598519936311: t03-0074, zzzzz
1598519936407: t01-0075, zzzzz
1598519936540: t02-0075, zzzzz
1598519936739: t03-0075, zzzzz
1598519936897: t01-0076, zzzzz
1598519936967: t02-0076, zzzzz
1598519937177: t03-0076, zzzzz
1598519937306: t01-0077, zzzzz
1598519937431: t02-0077, zzzzz
1598519937608: t03-0077, zzzzz
1598519937775: t01-0078, zzzzz
1598519937884: t02-0078, zzzzz
1598519938056: t03-0078, aaa
1598519938194: t01-0079, zzzzz
1598519938354: t02-0079, zzzzz
1598519938511: t03-0079, zzzzz
↓zzzzzのみ

(3回目にして、文字数を変えると分かりやすいことに気づきました……)

さいごに

「デプロイ付近で新しいLambdaと古いLambdaがランダムに実行される」という結果になりました。 だからどうだというわけではないですが、なにかの際に役立つかもしれません。