Help us understand the problem. What is going on with this article?

Serverless Framework で API Gateway & Lambda を構築する

この記事は CodeChrysalis Advent Calendar 2019 の記事です。

Serverless FrameworkでDynamoDBを構築することについての記事と[Serverless FrameworkでCognitoを構築する]ことについての記事もぜひ御覧ください。

はじめに

Serverless Framework はYAMLでサーバサイドの環境構築ができる、Infrastructure As Code の真骨頂です。確かにAWS等のコンソールでもポチポチクリックしながら環境構築はできますが、

  1. 変更の管理ができない
  2. 画面でポチポチクリックするのが本当に面倒(Lazy...)

なのでServerless Framework の CLI でコマンド一発で全てが構築されるのはとても気持ちが良い。今回はその中でもAWSのCognitoをどのように構築するかを紹介します。

この説明の前提は、データベースはDynamoDBをすでにServerless Framework(CloudFormation)で構築しているものとし、また認証はCognitoを使うこととします。

まずはYAML

早速YAMLを紹介します。

a

service: cc-hello-api

plugins:
  - serverless-webpack
  - serverless-domain-manager

custom:
  stage: ${opt:stage, self:provider.stage}

  webpack:
    webpackConfig: ./webpack.config.js
    includeModules:
      packagePath: ./package.json
      forceExclude:
        - aws-sdk

  domains:
    dev: dev-api.cc.io
    prod: api.cc.io
  customDomain:
    domainName: ${self:custom.domains.${self:custom.stage}}
    basePath: "v1"
    stage: ${self:custom.stage}
    certificateName: "*.cc.io"
    createRoute53Record: false

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  profile: cc-admin
  region: ap-northeast-1

functions:
  hello:
    handler: hello.main
    events:
      - http:
          path: hello
          method: get
          cors: true
          authorizer: aws_iam

resources:
  Outputs:
    ApiGatewayRestApiId:
      Value:
        Ref: ApiGatewayRestApi
      Export:
        Name: ${self:custom.stage}-ApiGatewayRestApiId

    ApiGatewayRestApiRootResourceId:
      Value:
        Fn::GetAtt:
          - ApiGatewayRestApi
          - RootResourceId
      Export:
        Name: ${self:custom.stage}-ApiGatewayRestApiRootResourceId

b

service: cc-users-api

plugins:
  - serverless-webpack
  - serverless-dotenv-plugin

custom:
  stage: ${opt:stage, self:provider.stage}
  dotenv:
    path: ./.env
  environment: ${file(./env.yml):${self:custom.stage}, file(./env.yml):default}

  webpack:
    webpackConfig: ./webpack.config.js
    includeModules:
      packagePath: ./package.json
      forceExclude:
        - aws-sdk

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  profile: cc-admin
  region: ap-northeast-1

  apiGateway:
    restApiId:
      "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
    restApiRootResourceId:
      "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiRootResourceId

  environment:
    USERS_DYNAMODB_TABLE: ${file(../../database/serverless.yml):custom.usersTableName}

  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:DescribeTable
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - "Fn::ImportValue": ${self:custom.stage}-UsersDynamoDbTableArn

functions:
  createUser:
    handler: createUser.main
    events:
      - http:
          path: users
          method: post
          cors: true
          authorizer: aws_iam

  getUser:
    handler: getUser.main
    events:
      - http:
          path: users/{userId}
          method: get
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              paths:
                userId: true

  listUsers:
    handler: listUsers.main
    events:
      - http:
          path: users
          method: get
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              querystrings:
                userEmail: false
                paginationToken: false
                limit: false

  updateUser:
    handler: updateUser.main
    events:
      - http:
          path: users/{userId}
          method: put
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              paths:
                userId: true

  deleteUser:
    handler: deleteUser.main
    events:
      - http:
          path: users/{userId}
          method: delete
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              paths:
                userId: true

デプロイの順番

Serverless Framework で Cognito を構築することについての記事でも触れたのですが、Serverless Frameworkでマイクロサービスをデプロイするときはデプロイする順番を気にする必要があります。

前述の記事ApiGatewayRestApiIdという値を使っているのですが、API Gatewayをデプロイした結果を参照させる必要があるので、まずはAPI Gatewayをデプロイします。そしてそれが終わった後にCognitoと各エンドポイントをデプロイします。

それぞれの要素を解説

serverless.yml - 1

1つ目のLambdaのデプロイです。これを通してAPI Gatewayもデプロイされます。

plugins:
  - serverless-webpack
  - serverless-domain-manager

プラグインを2つ使っています。この説明はNode.jsを使う前提としていて、一つはWebpackを使うためのプラグインと、カスタムドメインを使うためのプラグインです。後者については使わない場合はAWSのデフォルトのドメインが提供されますが、ご自身で取得したドメインを使いたい場合はこのプラグインが使えます。

  webpack:
    webpackConfig: ./webpack.config.js
    includeModules:
      packagePath: ./package.json
      forceExclude:
        - aws-sdk

WebpackのConfigがどこにあるのか、Package.jsonはどこにあるのかを設定しています。

forceExcludeaws-sdkを指定して点について説明します。aws-sdkpackage.jsonの中では通常devDependenciesに置かれていると思います。なぜならLambdaにすでにインクルードされているからです。しかしそれはAWS特有の話で、Serverless Frameworkは他のクラウドプラットフォームにも対応している性質からそれは気づけないので、この設定が必要になります。原文はこちら

  domains:
    dev: dev-api.cc.io
    prod: api.cc.io
  customDomain:
    domainName: ${self:custom.domains.${self:custom.stage}}
    basePath: "v1"
    stage: ${self:custom.stage}
    certificateName: "*.cc.io"
    createRoute53Record: false

ここはドメインの設定です。開発環境と本番環境のそれぞれを設定しています。basePathとはdomainsで設定したURIの続きを何にするかです。certificateNameはAWSのACMで取得した証明書を指しています。SSLで接続させるので電子証明書が必要になるからです。DNSについてはRoute53を使っていないのなら、createRoute53Recordfalseとします。

  runtime: nodejs12.x

つい最近(2019/12/24時点)ですが、Node.jsのVer.12世代がLambdaでサポートされました。

functions:
  hello:
    handler: hello.main
    events:
      - http:
          path: hello
          method: get
          cors: true
          authorizer: aws_iam

ここでLambdaです。helloはLambda関数の名前、handlerはどこに処理が書いてあるかのファイル名と関数名を書いています。hello.mainhelloがファイル名、mainが関数名です。

eventsに関しては詳しくはServerless Frameworkのドキュメントに詳細がありますが、この例だと、pathはURI、methodはHTTPメソッド、corscorsを使うかどうかです。

authorizerはデフォルトで良ければaws_iamです。なにかカスタマイズした場合は、そのリソースを指定します。

resources:
  Outputs:
    ApiGatewayRestApiId:
      Value:
        Ref: ApiGatewayRestApi
      Export:
        Name: ${self:custom.stage}-ApiGatewayRestApiId

    ApiGatewayRestApiRootResourceId:
      Value:
        Fn::GetAtt:
          - ApiGatewayRestApi
          - RootResourceId
      Export:
        Name: ${self:custom.stage}-ApiGatewayRestApiRootResourceId

そして忘れずにOutputsを設定することによって、他のデプロイでもAPI Gatewayを参照できるようにします。

serverless.yml - 2

service: cc-users-api

plugins:
  - serverless-webpack
  - serverless-dotenv-plugin

custom:
  stage: ${opt:stage, self:provider.stage}
  dotenv:
    path: ./.env

  webpack:
    webpackConfig: ./webpack.config.js
    includeModules:
      packagePath: ./package.json
      forceExclude:
        - aws-sdk

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  profile: cc-admin
  region: ap-northeast-1

  apiGateway:
    restApiId:
      "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
    restApiRootResourceId:
      "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiRootResourceId

  environment:
    CC_DYNAMODB_TABLE: ${file(../../database/serverless.yml):custom.ccTableName}

  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:DescribeTable
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - "Fn::ImportValue": ${self:custom.stage}-CcDynamoDbTableArn

functions:
  createUser:
    handler: createUser.main
    events:
      - http:
          path: users
          method: post
          cors: true
          authorizer: aws_iam

  getUser:
    handler: getUser.main
    events:
      - http:
          path: users/{userId}
          method: get
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              paths:
                userId: true

  listUsers:
    handler: listUsers.main
    events:
      - http:
          path: users
          method: get
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              querystrings:
                userEmail: false
                paginationToken: false
                limit: false

  updateUser:
    handler: updateUser.main
    events:
      - http:
          path: users/{userId}
          method: put
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              paths:
                userId: true

  deleteUser:
    handler: deleteUser.main
    events:
      - http:
          path: users/{userId}
          method: delete
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              paths:
                userId: true

さて実際のエンドポイントのデプロイです。

plugins:
  - serverless-webpack
  - serverless-dotenv-plugin

今回はもちろんWebpackとserverless-dotenv-pluginを入れています。環境変数を使うためにdotenvを使います。

  dotenv:
    path: ./.env

これによって環境変数ファイルを読み込んでいます。

  apiGateway:
    restApiId:
      "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
    restApiRootResourceId:
      "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiRootResourceId

ここでAPI Gatewayを参照することによって、API Gatewayと関連付けます。関連付けないとまた他のAPI Gatewayが立ち上がることになり、それをIAMが参照しないことになるので、このエンドポイントは誰もコールできなくなりますのでご注意。

  environment:
    CC_DYNAMODB_TABLE: ${file(./database/serverless.yml):custom.ccTableName}

これはServerless Framework で DynamoDB を構築することについての記事でデプロイしているDynamoDBのテーブル名を参照しています。プログラムの中で使いたい意図です。

このようにしてすべてのファイルでテーブル名をハードコーディングしなくとも良いようにできます。${file(./database/serverless.yml)これがファイル自体を参照するための文法です。

  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:DescribeTable
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - "Fn::ImportValue": ${self:custom.stage}-CcDynamoDbTableArn

IAMです。DynamoDBに接続させるためにはIAMで許可する必要があります。これもServerless Framework で DynamoDB を構築することについての記事で説明したとおり、デプロイしたテーブルをOutputsに設定しているので、"Fn::ImportValue"でArnを取得できるようにしています。

  listUsers:
    handler: listUsers.main
    events:
      - http:
          path: users
          method: get
          cors: true
          authorizer: aws_iam
          request:
            parameters:
              querystrings:
                userEmail: false
                paginationToken: false
                limit: false

クエリパラメータを使う場合は

            parameters:
              querystrings:
                userEmail: false
                paginationToken: false
                limit: false

を設定します。falseになっているのは必須ではないという意味です。

またパス(/users/{userId})を使う場合は

            parameters:
              paths:
                userId: true

を使います。trueになっているのは必須という意味です。

さいごに

どのようにAPI Gateway、Lambdaをデプロイするのか、またserverless-domain-managerserverless-dotenv-pluginに触れてきました。次はCodeBuildのを説明したいと思います。

maaaashin324
大手SIerでミッションクリティカルなシステムのシステムエンジニア、プロジェクトマネージャー、ITコンサルタントとカスタマーサポートに従事。 Code Chrysalis (https://www.codechrysalis.io/) のImmersiveコースを卒業してフルスタックソフトウェアエンジニアとなり、スタートアップのアプリケーション開発に携わったのち、株式会社yui 取締役CTO。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした