プロジェクト

全般

プロフィール

3回目 アジェンダ

2023/11/10(金) 16:10~17:40

サーバレスアーキテクチャ関連の主要なサービスであるAWS Lambda(ラムダ)とApiGateway、DynamoDBを触ってみます。
前回はVPCとVPC内のサービス(EC2、RDS)について扱いましたが、今回使ってみるサービスはVPCの外にあるサービスになります(ただしAWS LambdaはVPC内に配置することも設定により可能)。

今までの復習(さらっと)

1回目
2回目

1. AWS Lambdaについて

AWS Lambda

シンプルに各種ランタイム(java, Node.js, Python, .Netなど)での「関数の実行」を提供するサーバーレスなコンピューティングサービス。
EC2などでサーバーを立てる必要なくWebAPIのバックエンド処理やバッチ処理などを実行でき、負荷状態に応じて自動でスケールする。
他サービスとの組み合わせで使い道は色々で、非常に汎用性が高い。

メモリサイズの選択(現時点で128MB~10GBで選択可)でCPUとネットワークの性能も変わる。

前段にAPIGatewayを配置してWebAPIを構築することが可能。
最大実行時間が15分(現時点)の制限があるが、小さなバッチ処理ならLambdaで十分。
StepFunctionsというサービスと複数のLambdaを使ってワークフローを作ることも可能。

実行時間(性能に応じて)と実行回数が課金対象で、共にけっこうな永続無料枠あり。
無料枠

参考: 私のQiita記事

2. API Gatewayについて

Amazon API Gateway

バックエンドアプリケーションへWebインターフェースとして入り口を提供するサービス。
先述したようにLambda関数をバックエンドとしてサーバーレスなWebAPI構成を構築したり、VPC内のEC2やECS上で稼働するアプリケーションに繋げて認証やセキュリティを追加したりできる。

Route53ACMと連携して独自ドメインでの無料SSL証明書を使用したAPI公開も可能。

主に呼び出し回数が課金対象で、けっこうな永続無料枠あり。
料金

3. DynamoDBについて

Amazon DynamoDB

Key-Value型のNoSQLデータベースサービス。
「パーティションキー」(必須)と「ソートキー」(オプション)の組み合わせでプライマリキー(一意なキー)が決まる。
プライマリキーの他に2種類のIndexを追加可能。

  • グローバルセカンダリインデックス:ベースのものとは別の「パーティションキー」と「ソートキー」のセットを追加できる
  • ローカルセカンダリインデックス:ベースの「パーティションキー」を使って、追加の「ソートキー」を追加できる

RDBMSと異なりSQLによるクエリはできないが、無限のスケーラビリティを持ち巨大な規模のデータにも性能劣化なく安定したデータアクセスを提供可能。
「キャパシティユニット」という単位で処理能力が決まる。

  • RCU(ReadCapacityUnit): 4KBまでのデータを1秒間に1回読み取り可能(強整合性の場合は2倍必要)
  • WCU(WriteCapacityUnit): 1KBまでのデータを1秒間に1回書き込み可能

↑を超えてもバーストキャパシティとして利用されなかった過去300秒の分は利用可能

利用するキャパシティユニットとストレージのサイズが主な課金対象。
キャパシティユニットはアクセス量が予想できる場合は事前にプロビジョニングする方が低コスト(最小限必要分をプロビジョニングした上でオートスケーリングも可能)。
オンデマンドモードにすることでアクセス量が予想できない場合にも対応可能。
※ 無料枠が用意されているのはプロビジョニングモードのみ


本日のハンズオン

AWS Lambda(ラムダ)とApiGateway、DynamoDBを使って簡単なREST APIをサーバレス構成で構築してみます。
※ 今回のハンズオンで作成するAPIは認証などのアクセス制限が無く、誰でもアクセスできてしまうものなので、終了後には必ずAPI Gatewayのリソース削除を行うか設定変更によりOpenな状態にしないようにして下さい!

DynamoDBのテーブル作成

  • 任意のテーブル名(例として「study-202311-table」とします)
    • パーティションキー: "id" 文字列型
  • テーブル設定はデフォルト設定のまま(RCUとWCUは各25まで無料枠内)

Lambda関数の作成

  • 「一から作成」を選択
    • 任意の関数名(例として「study-202311-Function」とします)
    • ランタイム: Python3.9
    • 実行ロールはデフォルト設定(基本的な Lambda アクセス権限で新しいロールを作成)のまま
  • 初期状態のコードでテスト実行してみる
  • HTTPメソッドとパスに応じてDynamoDBを参照/更新する下記コードに上書きしてデプロイ
import json
import os
import boto3
import uuid

dynamo_table_name = os.getenv('dynamo_table_name', 'study-202311-table')
dynamo = boto3.resource('dynamodb')
dynamo_table = dynamo.Table(dynamo_table_name)

def lambda_handler(event, context):
    rc = event['requestContext']
    method = rc['http']['method']
    path = rc['http']['path']
    print(f'method:{method} path:{path}')

    if method == 'GET':
        if path.endswith('/'):
            res = execIdListOperation()
        else:
            id = path.split('/')[-1]
            res = execGetOperation(id)
    elif method == 'PUT':
        body = event['body']
        res = execInsertOperation(body)        
    elif method == 'POST':
        id = path.split('/')[-1]
        body = event['body']
        res = execUpdateOperation(id, body)
    elif method == 'DELETE':
        id = path.split('/')[-1]
        res = execDeleteOperation(id)
    else:
        res = {
            'statusCode': 400,
            'body': json.dumps({
                'message': 'method not supported'
            })
        }

    return res

def execIdListOperation():
    print('GET ID List operation.')
    scan_res = dynamo_table.scan(
        ProjectionExpression = 'id'
    )
    return {
        'statusCode': 200,
        'body': json.dumps(scan_res['Items'])
    }

def execGetOperation(id):
    print(f'GET operation. id: {id}')
    get_res = dynamo_table.get_item(
        TableName = dynamo_table_name,
        Key = { 'id': id }
    )
    if not 'Item' in get_res:
        return {
            'statusCode': 404,
            'body': json.dumps({
                'message': f'data not found. id: {id}'
            })
        }
    return {
        'statusCode': 200,
        'body': json.dumps({
            'data': get_res['Item']['data']
        })
    }

def execInsertOperation(body):
    print(f'Insert operation. body: {body}')
    id = str(uuid.uuid4())
    bodyJson = json.loads(body)
    dynamo_table.put_item(
        Item = {
            'id': id,
            'data': bodyJson['data']
        }
    )
    return {
        'statusCode': 200,
        'body': json.dumps({
            'operation': 'create',
            'id': id,
            'message': 'success'
        })
    }

def execUpdateOperation(id, body):
    print(f'Update operation. id:{id} body: {body}')

    get_res = execGetOperation(id)
    if get_res['statusCode'] != 200:
        return get_res

    bodyJson = json.loads(body)
    dynamo_table.put_item(
        Item = {
            'id': id,
            'data': bodyJson['data']
        }
    )
    return {
        'statusCode': 200,
        'body': json.dumps({
            'operation': 'update',
            'id': id,
            'message': 'success'
        })
    }


def execDeleteOperation(id):
    print(f'Delete operation. id: {id}')
    get_res = dynamo_table.delete_item(
        TableName = dynamo_table_name,
        Key = { 'id': id }
    )
    return {
        'statusCode': 200,
        'body': json.dumps({
            'operation': 'deleted',
            'id': id,
            'message': 'success'
        })
    }

API Gatewayの作成

  • APIタイプに「HTTP API」を選択 > 構築
    • 任意のAPI名(例として「study-202311-api」とします)
    • ルートを設定: そのまま次へ
    • ステージを定義: そのまま次へ
    • 作成
  • 作成後、ルートの追加
    • "$default"のパスを設定 > 作成
  • ルートへの統合をアタッチ
    • "$default"のルートを選択 >「統合をアタッチする」
    • 統合タイプに「Lambda 関数」を選択
    • 前の手順で作成した関数を選択
    • 作成

Lambda関数への権限追加

  • IAMロールのコンソール画面を開く
    • LambdaにアタッチされているIAMロールにDynamoDBテーブルにアクセスする権限追加
        {
            "Effect": "Allow",
            "Action": "dynamodb:*",
            "Resource": "arn:aws:dynamodb:リージョン:AWSアカウントID:table/study-202311-table"
        }

curlコマンドでの動作確認

# 変数設定
API_ENDPOINT=APIのエンドポイント

# ID一覧取得
curl -i -X GET $API_ENDPOINT

# データ登録
curl -i -X PUT $API_ENDPOINT -H "Content-Type: application/json" -d '{"data":"test1"}'

# データ取得(ID指定)
curl -i -X GET $API_ENDPOINT/(IDの値)

# データ更新
curl -i -X POST $API_ENDPOINT/(IDの値) -H "Content-Type: application/json" -d '{"data":"test1"}'

# データ削除
curl -i -X DELETE $API_ENDPOINT/(IDの値)

ハンズオンで作成したリソースの削除

  • APIGatewayのHTTP API
  • Lambda関数
  • Lambda関数用のIAMロール
  • Lambda関数のログ(CloudWatch Logsのロググループ)
  • DynamoDBのテーブル