フリーランチ食べたい

No Free Lunch in ML and Life. Pythonや機械学習のことを書きます。

Aurora Serverless Data APIとLambdaでAPIをServerless Framework(CloudFormation)で作る

昨年末のAurora Serverless Data APIの登場で、Lambdaからの利用が簡単になりました。この記事ではServerless Framework、CloudFormationを用いて、Aurora Serverless Data APIとLambdaを使ったAPIを構築する方法を紹介します。

f:id:ikedaosushi:20190525233641p:plain

使ったコードはすべてGitHubに上げています。記事内ではわかりやすさのためコードを抜粋して記載しますのでコード全体を確認したい場合はそちらを参照してください。GitHubのリンクは記事の最後に貼っています。

環境

  • python: 3.7
  • boto3: 1.9.156
  • serverless framework: 1.43.0
  • serverless-python-requirements: 4.3.0

アーキテクチャ構成

全体のアーキテクチャ構成は次のようになります。API Gateway, Lambda, Aurora Serverless, Secret ManagerをすべてServerless Frameworkで構築します。 Secret ManagerはAurora Serverless Data APIの利用に必ず必要になります。

f:id:ikedaosushi:20190525170411p:plain

Aurora Serverless Data APIとは

Aurora Serverless Data APIとは、一言で言うと、ネットワークに関する設定や制約なしでAurora Serverlessへアクセスができる機能です。詳細はAWSの公式ドキュメントやクラスメソッドさんのブログを読んでいただくのが良いかと思います。

クラスメソッドさんのブログにかかれている通り、今までLambdaからRDSを使うのは一苦労だったのですが、Aurora Serverless Data APIによって敷居が大幅に下がりました。

docs.aws.amazon.com

dev.classmethod.jp

この記事ではLambdaからAurora Serverless Data APIを使うアプリケーションとその環境を、実際にアプリケーションを作りながら、説明します。

Serverlessプロジェクト作成&CloudFormationで環境構築

それでは、アプリケーションを構築していきましょう。 まずServerlessプロジェクトを作成します。

sls create -t aws-python3 -n serverless-aurora-serverless-example

作成したら下記のようにCloudFormationを記載してデプロイします。 これでAurora ServerlessとSecret Managerの環境構築をすることができます。

serverless.yml

custom:
  default_stage: dev
  region: us-east-1
  stage: ${opt:stage, self:custom.default_stage}
  db_id: ${self:service}-db-${self:custom.stage}

provider:
  name: aws
  runtime: python3.7
  stage: ${self:custom.stage}
  region: ${self:custom.region}

resources:
  Resources:
    SecretAurora:
      Type: AWS::SecretsManager::Secret
      Properties:
        GenerateSecretString:
          SecretStringTemplate: '{"username": "admin"}'
          GenerateStringKey: 'password'
          PasswordLength: 16
          ExcludeCharacters: '"@/\'
    Aurora:
      Type: AWS::RDS::DBCluster
      Properties:
        DBClusterParameterGroupName: !Ref DBClusterParameterGroup
        DBClusterIdentifier: ${self:custom.db_id}
        MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref SecretAurora, ':SecretString:username}}' ]]
        MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref SecretAurora, ':SecretString:password}}' ]]
        Engine: aurora
        EngineMode: serverless
        ScalingConfiguration:
          AutoPause: true
          MaxCapacity: 8
          MinCapacity: 1
          SecondsUntilAutoPause: 3600
    DBClusterParameterGroup:
      Type: AWS::RDS::DBClusterParameterGroup
      Properties:
        Description: A parameter group for ${self:service}-db-${self:custom.stage}
        Family: aurora5.6
        Parameters:
          character_set_client: utf8mb4
          character_set_connection: utf8mb4
          character_set_database: utf8mb4
          character_set_results: utf8mb4
          character_set_server: utf8mb4
          time_zone: Asia/Tokyo

ここでポイントが2つあります。

1つ目は、Data APIは現時点(2019/05時点)でus-east-1しか対応していないのでリージョンは必ずus-east-1にしてください。

2つ目は、 AWS::RDS::DBClusterParameterGroup です。ここで文字コードなどの設定を行うことができるので必要であれば設定しましょう。今回は文字コードをutf8にしています。

後はデプロイするだけです。

sls deploy

デプロイが完了したらコンソールからAurora ServerlessとSecret Managerが作成されていることを確認できます。

f:id:ikedaosushi:20190525173056p:plain

f:id:ikedaosushi:20190525173142p:plain

Data APIを有効にする

環境構築で1つだけ手作業が必要な箇所があります。デフォルトで無効になっているData APIを有効にするところです。Data APIはまだBeta版なので、CloudFormationで有効にすることができません。

下記の手順でコンソールからData APIを有効にしてください。

データベースを選択して「変更」をクリック。

f:id:ikedaosushi:20190522010213p:plain

「ネットワーク&セキュリティ」でData APIを有効にする。

f:id:ikedaosushi:20190525173941p:plain

設定を保存して変更が反映されるとData APIが使えるようになります。

Lambda実装

それではアプリケーション側(Lambda)の実装に移ります。

最新版のboto3を使う

まず、残念なことにLambdaからデフォルトで使えるboto3のVersionではData APIを使うことができません。 serverless-python-requirementsを使って最新版のboto3をデプロイしましょう。

github.com

sls plugin install -n serverless-python-requirements

serverless.yml

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    usePipenv: true
    noDeploy: [pytest, jmespath, docutils, pip, python-dateutil, setuptools, s3transfer, six]

Pipfile

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
"boto3" = "*"
sls deploy

これで準備完了です。

Data APIを使ったAurora Serverlessへのアクセス

Aurora ServerlessへのアクセスはData APIを使って次のように行うことができます。

hander.py

def execute_sql(sql, db_exists=True):
    client = boto3.client('rds-data', region_name='us-east-1')
    args = {
        "awsSecretStoreArn": SECRET_ARN,
        "dbClusterOrInstanceArn": DB_CLST_ARN,
        "sqlStatements": sql
    }
    if db_exists:
        args['database'] = DB_NAME

    response = client.execute_sql(**args)
    return response

ここで、 SECRET_ARN , DB_CLST_ARN は環境変数でServerlessを使って下記のように渡しています。このようにCloudFormationで作成した環境のプロパティをアプリケーション側に簡単に渡せるのがServerlessの非常に便利なところです。

serverless.yml

custom:
  region: us-east-1
  db_cluster_arn: !Join
    - ""
    - - "arn:aws:rds:${self:custom.region}:"
      - !Ref AWS::AccountId
      - ":cluster:"
      - !Ref Aurora

provider:
  environment:
    DB_CLST_ARN: ${self:custom.db_cluster_arn}
    SECRET_ARN: !Ref SecretAurora
    TEST: !Ref AWS::AccountId

データベース準備

APIで使うデータベースを準備していきましょう。 今回はデータベース・テーブルの作成・ダミーデータの作成をするSQLとそれを呼び出す関数を書きました。

handler.py

DB_NAME = "test_db"
TABLE_NAME = "test_table"

create_db_sql = f"""
CREATE DATABASE IF NOT EXISTS `{DB_NAME}`;
"""

create_table_sql = f"""
CREATE TABLE IF NOT EXISTS `{TABLE_NAME}` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `content` TEXT DEFAULT NULL,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` TIMESTAMP NOT null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
"""

insert_sql = """
INSERT INTO `{table_name}` (`content`)
VALUES ( "{content}" );
"""

def setup(event, content):
    execute_sql(create_db_sql, db_exists=False)
    execute_sql(create_table_sql)
    for i in range(20):
        sql = insert_sql.format(table_name=TABLE_NAME, content=i)
        execute_sql(sql)

    return {
        "statusCode": 200,
        "body": "success"
    }

serverless.yml

functions:
  setup:
    handler: handler.setup

あとは実行するだけです。エラーにならなければデータベース・テーブルが作成されダミーデータが入っていると思います。

sls deploy
sls invoke -f setup

APIの作成

それでは今入れたダミーデータを返すAPIを作成します。ここまで来たら、あとはかなり簡単にできてしまいます。

handler.py

select_sql = f"""
SELECT
    *
FROM
    `{TABLE_NAME}`
;
"""

def index(event, context):
    response = execute_sql(select_sql)

    return {
        "statusCode": 200,
        "body": json.dumps(parse_aurora(response))
    }

serverless.yml

functions:
  index:
    handler: handler.index
    events:
      - http:
          path: index
          method: get
          cors: true
sls deploy
# (略)
# endpoints:
#   GET - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/index
# (略)

あとは今表示されたURLにアクセスするとAPIがデータが返ってくるのを確認できます。

curl https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/index
# [{"id": 1, "content": "0", "created_at": "2019-05-25 09:44:42.0", "updated_at": "2019-05-25 09:44:42.0"}, ... (略)

無事APIを実装することができました。

現時点での問題点

Aurora Serverless Data APIとLambdaの組み合わせを実装してみて感じた問題点を書いておきます。

まず、セキュリティの問題が一番大きいと思います。現状では、SQLのprepared statementが未サポートでプレースホルダを使うことができません。また、複数クエリも同時実行できてしまいます。 これは、アプリケーション側でインジェクション対策を適切に行う必要があることを意味していて、とてもリスキーです。

もう一点はData APIのResponseのデータ構造が複雑で直感的でないことです。 今回、 parse_aurora(response) というData APIのResponseをパースする関数を書いたのですが、正直構造がわかりやすいとは言い難く、苦労しました。

また、上記の問題点に加えてパフォーマンスの問題点などもあり、下記のエントリに詳しく書かれています。興味がある方は読んでみてください。

www.jeremydaly.com

これらの課題はGAになるときには解決されていることを願っています。というか解決されないと本番運用は難しいケースが多いかもしれません😅

さいごに

Serverless Frameworkを活用して、Aurora Serverless Data APIとLambdaの組み合わせでAPIを作る方法を紹介しました。最後に書いたようにまだ問題点もありますが、Aurora Serverless Data APIはサーバーレスなアプリケーションを作る上で非常な便利な存在になると思います。今後のアップデートに心から期待しています。アップデートがあればまた記事を書きたいと思います。

今回使ったコードは下のリポジトリにすべてあげてあります。

github.com