1
1

More than 1 year has passed since last update.

AWS+ServerlessFrameworkを使ってPythonバックエンドを構築する

Last updated at Posted at 2021-11-28

ここしばらく個人でスマホアプリを作ってみたいと思い色々とバックエンド側の構築方法を探っていました。Amplifyを使おうかと思っていたのですが、Flutterではdatastore周りが少し安定していない感がありまして、噂に聞いていたServerlessFrameworkを使ってみることにしました

Flutter的にはバックエンドはFirebaseがメジャーだと思いますが、私のスキルセット的にAWS、LambdaもPythonで構築することにしました

この記事でできること

  • ServerlessFramework、AWSを使ったバックエンドの構築
  • APIGateway + Lambda + S3 + DynamoDBの構成
  • APIKeyを使ったREST APIのアクセス管理
  • S3やDynamoDBに対する、アクセスロールの設定
  • ルートディレクトリに集まりがちなfunctionを関数ごとにディレクトリ分け

と、バックエンドを構築するときの一通りのことができるのではないかと思います。また、ServerlessFrameworkそのものの導入方法は割愛します。すでに詳しく解説されている記事がありますので

環境

Framework Core: 2.57.0
Plugin: 5.4.4
SDK: 4.3.0
Components: 3.16.0

初期化

% serverless create --template aws-python3 --name hello-world <<< Serverlessサービスを作る

ServerlessFrameworkはサービスという単位で環境を作るそうです。上記のコマンドひとつで、Lambda1つを含む基本的なテンプレートが生成されます

serverless.yml
service: hello-world
frameworkVersion: '2'
provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
functions: << これ以下を消すと、作ったLambdaを削除することもできる
  hello:
    handler: handler.hello <<< hander.pyとの紐付け
handler.py
import json

def hello(event, context):
    body = {
        "message": "Go Serverless v1.0! Your function executed successfully!",
        "input": event
    }

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

    return response

このテンプレートをベースにガリガリ触っていきます。ちなみに

  • デフォルトだとバージニア北部リージョンにできる
  • Lambdaは[サービス名]-[環境変数]-[serverless.ymlで指定した名前]でできる
  • 実行ログのCloudWatch吐き出しもデフォルトで設定できるようである

Amplifyと比べて、余計なファイルもできないし、デプロイもほぼ待ち時間はありません。全体的にシンプルで良いですね

APIGateway > Lambda > DynamoDBの流れを作る

serverless.yml
service: hello-world
frameworkVersion: '2'
provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  # ///////// ↓DynamoDBへのアクセス権限を追加↓ ///////////#
  iam:
    role:
      statements:
        - Effect: 'Allow'
          Action:
            - 'dynamodb:*'
          Resource:
            - "arn:aws:dynamodb:us-east-1:xxxxxxxxxx:table/*"
            # xxxxxxxxにはAWSのアカウント番号が入る
  # ///////// ↑DynamoDBへのアクセス権限を追加↑ ///////////#
functions:
  hello:
    handler: handler.hello
# ////////// ↓ここから下でAPIGatewayとDynamoDBを追加↓ ///////// #
    events:
      - httpApi:
          path: /users/create
          method: get
resources:
  Resources:
    DynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          - AttributeName: name
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
          - AttributeName: name
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: hello-dynamodb

この時、Lambdaからはboto3を使ってDynamoDBにアクセスします

handler.py
def hello(event, context):
    dynamodb = boto3.resource('dynamodb')
    result = dynamodb.Table('hello-dynamodb').put_item(
        Item = {
            'id': "hoge_id",
            'name': 'hoge_name'
        }
    )

    response = {
        "statusCode": 200,
        "body": json.dumps('success!')
    }

    return response

スクリーンショット 2021-11-09 8.24.57.png

デプロイをして再度実行すると、DynamoDBにも無事レコードが追加されました。Amplifyでは自動でcreated_at, updated_at, versionなどが付加されていましたが、今回は定義したもの以外は全くなく、非常にシンプルでした。

生成されたURLをブラウザで開いてみると、「success!」とだけ表示されました。成功ですね

APIKeyを使ったREST APIのアクセス管理

このままだとAPIは全世界に公開されていて、URLを知ってさえいれば誰でもアクセスできてしまいます。これはこれで良いのですが、コール数を管理したり制限するときにはAPI Key(AWS的には使用量プラン)をつけます

serverless.yml
# 関連しないところは省略しています
service: hello-world
frameworkVersion: '2'
provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  stage: dev # api-keyがstageできりかわるようにするために追加

  # ////////// ↓全function共通で使用するAPIKeyを追加↓ ////////// #
  apiGateway:
    apiKeys: # API Keyの設定
      - mobileApp: 
        - name: ${self:provider.stage}-app-key
          value: # お好みのAPI Key
    usagePlan: # 使用量プラン
      - mobileApp:
          quota:
            limit: 1000
            offset: 0
            period: DAY
          throttle:
            rateLimit: 100
            burstLimit: 100
# ////////// ↑全function共通で使用するAPIKeyを追加↑ ////////// #
functions:
  hello:
    handler: handler.hello
    events:
      - http: # httpApi -> http
          path: /users/create
          method: get
          private: true # ApiKeyをつけたいものにprivateをつける

スクリーンショット 2021-11-28 8.40.15.png
これで画像のようにAPI KeyをHeaderにつけてAPIコールしないと動かなくなります。(API Key無しだとForbidden)

S3の作成と、functionごとへにアクセスロールの設定

serverless.yml
custom:
  backetName: hello-serverless-bucket-${self:provider.stage}

resources:
    Bucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.backetName}
    S3AccessRole:
      Type: AWS::IAM::Role
      DependsOn: Bucket
      Properties:
        Path: /
        RoleName: hello-serverless-lambda-role-with-s3
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        Policies:
          - PolicyName: hello-serverless-lambda-policy-with-s3
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - logs:CreateLogGroup
                    - logs:CreateLogStream
                    - logs:PutLogEvents
                  Resource:
                    - 'Fn::Join':
                        - ':'
                        -
                          - 'arn:aws:logs'
                          - Ref: 'AWS::Region'
                          - Ref: 'AWS::AccountId'
                          - 'log-group:/aws/lambda/*:*:*'
                - Effect: "Allow"
                  Action:
                    - "s3:PutObject"
                    - "s3:GetObject"
                    - "s3:ListBucket"
                  Resource:
                    - 'Fn::Join':
                        - ''
                        -
                          - "arn:aws:s3:::"
                          - ${self:custom.backetName}
                    - 'Fn::Join':
                        - ''
                        -
                          - "arn:aws:s3:::"
                          - ${self:custom.backetName}
                          - "/*"
  • LambdaごとにAWSの各リソースに対するアクセス権限を付与しています
  • Bucket名は定数化して環境ごとに切り替わるようにしています
hendler.py
def hello(event, context):

    filepath = '/tmp/data.csv'
    with open(filepath, 'w') as f:
        writer = csv.writer(f)
        writer.writerow(["a", "b", "c"])

    s3 = boto3.resource('s3')
    s3.meta.client.upload_file(filepath, 'hello-serverless-bucket-dev', 'hgoe.csv')


    response = {
        "statusCode": 200,
        "body": json.dumps('success!')
    }

    return response

Lambdaのコードは上記のようになります

functionのディレクトリ分けと、外部ライブラリのアップロード

functionは数十個になりうるのですが、このままだとServerlessのルートディレクトリに全て並べないといけなくなります。また、Lambdaがデフォルトでサポートしていない外部ライブラリを使う場合にアップロードする方法に迷いました

これらを解決してくれるのが、ServerlessFrameworkのライブラリserverless-python-requirementsです。導入方法は公式を参照していただくのが良いかと思います。ちなみにDockerが必要です

ディレクトリ構成

/
├── serverless.yml
├── functions/
│   ├── hello/
│   │   ├── handler.py
│   │   └── requirements.txt

こんな感じでfunctionごとにディレクトリを切ることができます。ちなみに、各ディレクトリ中のrequirements.txtには利用したい外部ライブラリを列挙するのですが、外部ライブラリを使わない場合でも必要です

Serverless定義

serverless.yml
service: hello-world
frameworkVersion: '2'
provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  stage: dev

plugins: # 追加
  - serverless-python-requirements # 追加

custom:
  backetName: hello-serverless-bucket-${self:provider.stage}
  pythonRequirements:  # 追加
    dockerizePip: true # 追加

package: # 追加
  individually: true # 追加

functions:
  hello:
    role: S3AccessRole
    module: functions/hello # 追加
    handler: handler.hello
    events:
      - http:
          path: /users/create
          method: get
          private: true

まとめ

ServerlessFrameworkはやや情報が少ないのか、一度ハマると解決するのが結構大変でした(requirement.txtが必須とか)。今回の記事で、ServerlessFrameworkで、AWSを使って、Pythonを使う場合の基本的な内容はカバーできた気がします

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1