むだいありー

AWS SAMを使ってAPIを作るまで 前編

概要

AWSのSAM(Serverless Application Model)を使ってAPIを作成するまでの手順をまとめる。

  • API GatewayでRest Apiを立ててCongnito認証を設定する
  • API GatewayでHttp Apiを立てるLambda Authorizerを設定する
  • RDS に接続する
  • どちらのAPIもCORS対応する

基本的な設定はSAMのDeveloper Guideを見ればなんとなくわかる感じだと思うが、初心者に易しくはないかなと思う。 高度な設定をしようと思ったら、SAMの元になっているAWS CloudFormationのUser Guildeも読み込まなければならない。

使う機会があったので、忘れないうちに書き残すことにする。 正確ではないかもしれないけど、こんな事できるんか〜ってのと、調べる取っ掛かりになれば。

AWSコンソールの使い方などは省略する。

前編はHTTPApiの設定、後半はRESTApiの設定を行う。

環境

  • macOS 11.0.1
  • SAM CLI 1.15.0
  • AWS Toolkit 1.17.0 (vscode拡張)
  • python 3.8.6

ディレクトリ構成

root/
┣ layers/
┃  ┗ common/
┃  ┃ ┗ common_layer.py
┃  ┗ ┗ requirements.txt
┣ src/
┃  ┗ get_function/
┃  ┃ ┗ __init.py__
┃  ┃ ┗ app.py
┃  ┗ ┗ requirements.txt
┗ template.yaml

HTTPApiを作る

template.yamlにCloudFormationを作る定義を書いていく。 TransformとResourcesは必須セクション。

Transform: AWS::Serverless-2016-10-31  # 現状唯一有効な値

# 各リソースへのグローバル設定
Globals:
  Function:
    Environment:
      Variables:
        RDS_ENDPOINT: "https://xxxx.xxx.xxx"
        RDS_DB_NAME: "xxxxxxx"
        RDS_USER: "xxxxxx"
        RDS_PASSWORD: "xxxxx"

# CloudFormationに登録されたりするリソース定義
Resources:
 # Lambda関数に付与されるRole。VPCとログ書き込み権限
  MyFunctionRoll:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: lambda.amazonaws.com
      ManagedPolicyArns:
        - !Sub "arn:aws:iam::aws:policy/AmazonVPCFullAccess"
        - !Sub "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"

  # Lambda Authorizerを使うための許可
  MyApiAuthorizerPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref MyApiAuthorizer
      Principal: apigateway.amazonaws.com

  # Lambda Authorizerで呼び出される関数の定義
  MyApiAuthorizer:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/api_authorizer/
      Handler: app.lambda_handler
      Runtime: python3.8
      Role: !GetAtt MyFunctionRoll.Arn

  # ApiGateway(HTTPApi)自体の定義
  GetFunctionApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      CorsConfiguration:
        AllowOrigins:
          - "*"
        AllowMethods:
          - GET
        AllowHeaders:
          - Authorization
          - Accept    # IEだとこれも定義しないとCORSで弾かれる
      Auth:
        Authorizers:
          FunctionApiAuthorizer:
            AuthorizerPayloadFormatVersion: 2.0
            EnableSimpleResponses: true
            FunctionArn: !GetAtt MyApiAuthorizer.Arn
            Identity:
              Headers:
                - Authorization

  # Lambda Authorizerを使うための許可
  GetFunctionPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref GetFunction
      Principal: apigateway.amazonaws.com

  # Lambda関数で使用するLayer(共通処理)定義
  MyApiLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      Description: API用共通処理
      ContentUri: "layers/common/"
      CompatibleRuntimes:
        - python3.8
    Metadata:
      BuildMethod: python3.8

  # Lambda関数の定義
  GetMarkerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/get_function/
      Handler: app.lambda_handler
      Runtime: python3.8
      Layers:
        - !Ref MyApiLayer
      # RDS Proxyに接続するためのVPC設定
      Role: !GetAtt MyFunctionRoll.Arn
      VpcConfig:
        SecurityGroupIds:
          - sg-xxxxxxx
        SubnetIds:
          - subnet-xxxxxxxx
          - subnet-xxxxxxxx
          - subnet-xxxxxxxx
      # Lambdaのトリガー設定(ApiGatewayのHttpApi)
      Events:
        GetEvent:  #一意なら好きな名前でOK
          Type: HttpApi
          Properties:
            Path: /get-api-path
            Method: get
            ApiId: !Ref GetFunctionApi  # 上で定義したApiGateway
            Auth:
              Authorizer: MyApiAuthorizer

Lambda Authorizer関数について

def lambda_handler(event, context):
    result = False
    value = event['headers']['authorization']

    # 何らかのチェック処理

    return {
        'isAuthorized': result
    }

Lambda Authorizerのレスポンス形態は2種類あって、今回はシンプルレスポンスなので認証が成功したかのbool値をisAuthorizedで返す。

Layer

import os
import json
import sqlalchemy
from sqlalchemy.orm import sessionmaker
from http import HTTPStatus

def db_connection_init():
    """RDSインスタンスへ接続します。"""
    try:
        # 環境変数から接続情報読み込み
        DATABASE = 'mysql+pymysql://%s:%s@%s/%s?charset=utf8mb4' % (
            os.environ.get('RDS_USER'),
            os.environ.get('RDS_PASSWORD'),
            os.environ.get('RDS_ENDPOINT'),
            os.environ.get('RDS_DB_NAME'),
        )

        ENGINE = sqlalchemy.create_engine(
            DATABASE,
            encoding="utf-8",
            echo=False  # Trueだと実行のたびにSQLが出力される
        )

        Session = sessionmaker(autocommit=False, autoflush=False, bind=ENGINE)
        session = Session()
        return session
    except Exception as e:
        print(e)
        raise e

def create_success_response(respose_param={}):
    """通常レスポンスを返します。"""
    return {
        'statusCode': HTTPStatus.OK,
        'headers': {
            'Content-type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        'body': json.dumps(respose_param, ensure_ascii=False)
    }

def create_error_response(message='', status=HTTPStatus.BAD_REQUEST):
    """エラーレスポンスを返します"""
    return {
        'statusCode': status,
        'headers': {
            'Content-type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        'body': json.dumps({
            'message': message
        }, ensure_ascii=False)
    }

layers/common/requirements.txtに必要なパッケージを記述しておく。

PyMySQL
sqlalchemy

GetApi

import datetime
from http import HTTPStatus
import common_layer

# コネクションが再利用されるように(ウォームスタート)
session = common_layer.db_connection_init()

def lambda_handler(event, context):
    try:
        results = {}

        # DBから取得等

        return common_layer.create_success_response(results)
    except Exception as e:
        print(e)
        return common_layer.create_error_response({
            'message': 'Error'
        }, status=HTTPStatus.INTERNAL_SERVER_ERROR)
    finally:
        session.close()

Lambda関数のレスポンスについて

headerでAccess-Control-Allow-Originを返す必要がある。

{
  'headers': {
    'Content-type': 'application/json',
    'Access-Control-Allow-Origin': '*'
  },
}

ローカル起動

Dockerがインストールされていれば、ローカル環境で動作を確認できる。 ただ、認証機能を含めて動作させることは出来ない。 Lambda AuhthorizerもLambda関数なので、個別に動作確認できる。

AWS上ではHeaderのキーは小文字で統一されるが、Docker上では変換されないので注意が必要。

ビルド

template.yamlの定義からリソースをビルドする

sam build

Api起動

sam local api-start

参考