見出し画像

今から始めるServerless Frameworkでサーバーレスデビュー【バックエンド編#1】

この記事では、前編と後編に分けて「ServerlessFrameworkを使ったサーバーレス環境の構築」について紹介していきます。

前編では、ServerlessFrameworkを利用してAWS上でサーバーレス環境を構築します。
後編では、フロントエンドにReactを使用し、Amplifyを介してバックエンドと接続し、簡単な認証やCRUD操作ができるようにします。

1.SeverlessFrameworkとは?

Serverless Frameworkは、サーバーレスなアーキテクチャを簡単に構築するためのオープンソースのフレームワークです。
AWS Lambda、Azure Functions、Google Cloud Functionsなど、さまざまなプラットフォームに対応しています。

2.Serverless Frameworkのインストール

Serverless Frameworkは、npmを介して提供されており、-gオプションを使用してグローバル環境にインストールします。

$ npm install serverless -g

インストールが完了したら、次はServiceを作成します。

$ serverless create --template aws-nodejs --path my-serverless-backend
Serverless: Generating boilerplate...
_______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v2.0.0
-------'
Serverless: Successfully generated boilerplate for template: "aws-nodejs"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name

templateオプションに使用するサービス名を指定し、pathオプションを指定することでディレクトリを作成します。

● 作成されるファイル
handler.js: Lambda関数を実装するJSファイル
serverless.yml: フレームワークの設定ファイル

作成されたymlファイルに設定を書いていきます。ただし、インデントのミスに注意しましょう。

# ------------------------------------------
# service
# サービス名を設定
# ------------------------------------------
service: my-serverless-backend


# ------------------------------------------
# plugins
# サービスに必要なプラグインを設定
# ------------------------------------------
plugins:
- aws-amplify-serverless-plugin
- serverless-appsync-plugin
- serverless-plugin-typescript


# ------------------------------------------
# package
# サービスから除外するディレクトリを設定
# ------------------------------------------
package:
exclude:
  - ./node_modules/**
  - node_modules/**


# ------------------------------------------
# frameworkVersion
# フレームワークのバージョンを設定
# ------------------------------------------
frameworkVersion: '2.0.0'


# ------------------------------------------
# provider
# サービスの基本情報を設定
# ------------------------------------------
provider:
name: aws
runtime: nodejs12.x
stage: ${opt:stage, "dev"}
region: ap-northeast-1
apiGateway:
  minimumCompressionSize: 1024
environment:
  TABLE_POST: ${self:service}-${opt:stage, self:provider.stage}-Post
iamRoleStatements:
  - Effect: Allow
    Action:
      - "dynamodb:*"
    Resource:
      - 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/*'
      - 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/*/*'

  - Effect: Allow
    Action:
      - lambda:*
    Resource:
      - 'arn:aws:lambda:${opt:region, self:provider.region}:*:function:*'

  - Effect: 'Allow'
    Action:
      - 'cognito-idp:*'
    Resource: 'arn:aws:cognito-idp:*'


# ------------------------------------------
# functions
# ラムダで使用する関数を設定
# ------------------------------------------
functions:

# 初期ファンクション
hello:
  handler: functions/hello.handler
  memorySize: 128
  timeout: 10
  events:
    - http:
        path: /hello
        method: get
        integration: lambda
        authorizer:
          name: ApiGatewayAuthorizer
          type: COGNITO_USER_POOLS
          authorizerId: !Ref ApiGatewayAuthorizer

# ユーザー作成認証する処理
customMessages:
  handler: functions/cognito.handler
  memorySize: 128
  timeout: 10
  events:
    - cognitoUserPool:
        pool: ${self:service}-user-pool-${opt:stage, self:provider.stage}
        trigger: CustomMessage
        existing: true
        
# ユーザー作成時にDBに情報を格納する処理
postConfirmation:
 handler: functions/user.handler
 memorySize: 128
 timeout: 10
 events:
   - cognitoUserPool:
       pool: ${self:service}-user-pool-${opt:stage, self:provider.stage}
       trigger: PostConfirmation
       existing: true

# ------------------------------------------
# custom
# カスタムするサービス郡を設定
# ------------------------------------------
custom:
amplify:
  - filename: ./src/aws-exports.js
    type: javascript
    appClient: UserPoolClient

appSync:
  name: ${self:service}-${self:provider.stage}
  authenticationType: AMAZON_COGNITO_USER_POOLS
  userPoolConfig:
    awsRegion: ap-northeast-1
    defaultAction: ALLOW
    region: ap-northeast-1
    userPoolId:
      Ref: UserPool
  mappingTemplatesLocation: mapping-templates
  mappingTemplates:

    # ------------------------------------------
    # Queries
    # ------------------------------------------
    -
      type: Query
      field: getPost
      dataSource: Post
    -
      type: Query
      field: listPost
      dataSource: Post
    -
      type: Query
      field: getUser
      dataSource: User

    # ------------------------------------------
    # Mutations
    # ------------------------------------------
    -
      type: Mutation
      field: createPost
      dataSource: Post
    -
      type: Mutation
      field: updatePost
      dataSource: Post
    -
      type: Mutation
      field: deletePost
      dataSource: Post
  schema: schema.graphql
 
  dataSources:
     - type: AMAZON_DYNAMODB
      name: User
      config:
        tableName: ${self:provider.environment.TABLE_USER}
        serviceRoleArn: { Fn::GetAtt: [AppSyncDynamoDBServiceRole, Arn] }
        region: ap-northeast-1
        
     - type: AMAZON_DYNAMODB
      name: Post
      config:
        tableName: ${self:provider.environment.TABLE_POST}
        serviceRoleArn: { Fn::GetAtt: [AppSyncDynamoDBServiceRole, Arn] }
        region: ap-northeast-1

# ------------------------------------------
# resources
# サービス郡の詳細リソースを設定
# ------------------------------------------
resources:
- ${file(resources/cognito-user-pool.yml)}
- ${file(resources/dynamodb-table.yml)}
- ${file(resources/i-am-role.yml)}
- ${file(resources/authorizer.yml)}

セクションごとにポイントを説明します。

● service
サービスを名を任意で指定します。
後述のCognitoなどでサービス名を変数として使用することがありますのでわかりやすい名前をつけましょう。

● plugins
ServerlessFrameworkにはPluginsをinstallすることによって便利な機能を追加することができます。
Pluginsはnpm pakcageにて提供されているので使用したい機能をnpm installしていきましょう。

- aws-amplify-serverless-plugin
フロント側で必要なGraphQLなどのエンドポイントやCognitoのプールIDなどが記載された設定ファイルを書き出すためのプラグインです。

- serverless-appsync-plugin
その名の通りAppSyncを使用するためのプラグインです。

- serverless-plugin-typescript
LambdaのFunctionsをTypeScriptで記述できるように導入するプラグインです。

※ 初回のcreate時にserverless create --template aws-nodejs-typescriptとすることでTypeScriptを導入できますが、設定ファイルはymlで記述したいのでこの方法を取っています。

- serverless-offline
ローカルでLambdaのFunctionsを実行するためのプラグインです。

● package
packageのincludeやexcludeを行います。
excludeするディレクトリを指定しています。

● frameworkVersion
使用しているバージョンを指定しましょう。
使用していないバージョンを指定すると怒られます。

● provider
環境変数やロールやリージョンなどの基本的な情報を主に記述していきます。
注意として、iamRoleStatementsではLambdaからLambdaをさわるInvokeFunctionを特定のroleに対して許可をする設定や、LambdaからDynamoDBへのroleの設定をここでします。
パッと見のymlの階層でいくとLambdaのiamRoleStatementsを設定しているように見えないので注意が必要です。

● functions
AWS Lambdaで使用するFunctionsを記載していきます。
CognitoUserPoolのtriggerと合わせてeventsを設定することができたり、API Gatewayとして設定することが可能です。

● custom
ServerlessFrameworkというインフラをベースにどのようなサービスを組合せて使用していくのかの詳細を記述していきます。
このセクションがServerlessFrameworkでは中心となるのではないかと思います。

- amplify
フロント側でバックエンドとの接続をAmplifyを使用して接続します。
大事なサービスのid情報などを書き出すために使用します。

- appSync
GraphQLベースでDynamoDBと繋げてデータをCRUDするために使用します。また、AppSync経由でDBへ書き込みを行う部分にログイン認証を入れたいのでauthenticationTypeにAWSのCognitoを使用します。
AppSyncにはDataSourceを指定することでLambdaやDynamoDBと連携することが可能になりますので今回はDynamoDBをDataSourceとして使用します。

● resources
AWS Providerで使用するサービスのAWSインフラストラクチャリソースを記述します。
CognitoのポリシーやDynamoDBのキーなど詳細設計を記述していきます。
各resoucesファイルが長くなるので別ファイルに切り出してファイルをimportして読み込んで使用します。

// resources/cognito-user-pool.yml

Resources:
UserPool:
  Type: 'AWS::Cognito::UserPool'
  Properties:
    UserPoolName: ${self:service}-user-pool-${opt:stage, self:provider.stage}
    UsernameAttributes:
      - email
    AccountRecoverySetting:
      RecoveryMechanisms:
        - Name: verified_email
          Priority: 1
    AdminCreateUserConfig:
      AllowAdminCreateUserOnly: false
    AutoVerifiedAttributes:
      - email
    MfaConfiguration: 'OFF'
    Policies:
      PasswordPolicy:
        MinimumLength: 8
        RequireLowercase: false
        RequireNumbers: false
        RequireSymbols: false
        RequireUppercase: false
        TemporaryPasswordValidityDays: 7
    Schema:
      - AttributeDataType: String
        DeveloperOnlyAttribute: false
        Mutable: true
        Name: email
        Required: true
    VerificationMessageTemplate:
      DefaultEmailOption: CONFIRM_WITH_CODE
UserPoolClient:
  Type: AWS::Cognito::UserPoolClient
  Properties:
    ClientName: ${self:service}-user-pool-client-${opt:stage, self:provider.stage}
    UserPoolId:
      Ref: UserPool
    GenerateSecret: false
          
# Cognito - Lambda

CognitoTriggerLambdaPermission:
  Type: AWS::Lambda::Permission
  Properties:
    Action: lambda:InvokeFunction
    FunctionName:
      Fn::GetAtt:
        - CustomMessagesLambdaFunction
        - Arn
    Principal: cognito-idp.amazonaws.com
// resources/i-am-role.yml

Resources:
# AppSync - DynamoDB
AppSyncDynamoDBServiceRole:
  Type: "AWS::IAM::Role"
  Properties:
    RoleName: ${opt:stage, self:provider.stage}-appsync-dynamodb-role
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: "Allow"
          Principal:
            Service:
              - "appsync.amazonaws.com"
          Action:
            - "sts:AssumeRole"
    Policies:
      - PolicyName: "dynamo-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: "Allow"
              Action:
                - "dynamodb:Query"
                - "dynamodb:BatchWriteItem"
                - "dynamodb:GetItem"
                - "dynamodb:DeleteItem"
                - "dynamodb:PutItem"
                - "dynamodb:Scan"
                - "dynamodb:UpdateItem"
              Resource:
                - 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/*'
                - 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/*/*'
// resources/authorizer.yml

ApiGatewayAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
  Name: ApiGatewayAuthorizer
  Type: COGNITO_USER_POOLS
  IdentitySource: method.request.header.Authorization
  RestApiId:
    Ref: ApiGatewayRestApi
  ProviderARNs:
    - { Fn::GetAtt: [UserPool, Arn] }

GatewayResponse401:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
  ResponseParameters:
    gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
    gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
  ResponseType: UNAUTHORIZED
  RestApiId:
    Ref: 'ApiGatewayRestApi'
  StatusCode: '401'
GatewayResponse500:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
  ResponseParameters:
    gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
    gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
  ResponseType: AUTHORIZER_FAILURE
  RestApiId:
    Ref: 'ApiGatewayRestApi'
  StatusCode: '500'
// resources/dynamodb-table.yml

Resources:
DynamoDBUser:
  Type: 'AWS::DynamoDB::Table'
  Properties:
    TableName: ${self:provider.environment.TABLE_USER}
    AttributeDefinitions:
      - AttributeName: userId
        AttributeType: S
    KeySchema:
      - AttributeName: userId
        KeyType: HASH
    ProvisionedThroughput:
      ReadCapacityUnits: 1
      WriteCapacityUnits: 1
   
DynamoDBPost:
 Type: 'AWS::DynamoDB::Table'
 Properties:
   TableName: ${self:provider.environment.TABLE_POST}
   AttributeDefinitions:
     - AttributeName: postId
       AttributeType: S
   KeySchema:
     - AttributeName: postId
       KeyType: HASH
   ProvisionedThroughput:
     ReadCapacityUnits: 1
     WriteCapacityUnits: 1

これにより、各設定が完了しました。

$ serverless deploy -v // aliasを活用してsls deploy -vとすることも可能

コマンドを入力すると、CloudFormation上にスタックが作成され、環境が構築されます。
これだけで、AWS上にコードベース以下のリソースが簡単に作成されました。

● AppSync
● Cognito
● DynamoDB
● Lambda

3.今回のディレクトリ構造

├── .severless // deploy後に生成されるディレクトリ
├── functions // LambdaのFunctionsディレクトリ
│   ├── cognito.ts
│   └── hello.ts
│   └── user.ts
├── mapping-templates // appSyncのCRUDで必要なリゾルバーディレクトリ
│   ├── Mutation.createPost.request.vtl
│   ├── Mutation.createPost.response.vtl
│   ├── Mutation.deletePost.request.vtl
│   ├── Mutation.deletePost.response.vtl
│   ├── Mutation.updatePost.request.vtl
│   ├── Mutation.updatePost.response.vtl
│   ├── Query.getUser.request.vtl
│   ├── Query.getUser.response.vtl
│   ├── Query.getPost.request.vtl
│   ├── Query.getPost.response.vtl
│   ├── Query.listPost.request.vtl
│   └── Query.listPost.response.vtl
├── package.json
├── resources // 各サービスの詳細インフラファイルがあるディレクトリ
│   ├── cognito-user-pool.yml
│   ├── dynamodb-table.yml
│   └── i-am-role.yml
│   └── authorizer.yml
├── schema.graphql // appSyncのGraphQLで使用するスキーマファイル
├── serverless.yml // serverlessframeworkの設定ファイル
├── src
│   └── aws-exports.js // amplifyに繋ぐための設定ファイル
├── tsconfig.json
└── yarn.lock

Serverless Frameworkを活用することで、ステージングやローカル環境など、異なる環境での構築が簡単に行えることがわかりました。
また、コードベースでのインフラアーキテクチャを構築できるなど、多くのメリットがあります。
これらの理由から、サーバーレスを本格的に活用する際には、Serverless Frameworkは有力な選択肢となるでしょう。
本記事と合わせて、Serverless Frameworkを活用するポイントを押さえてお読みいただければ幸いです。

● 今から始めるServerless Frameworkでサーバーレスデビュー【バックエンド編#1】
https://note.com/ryoppei/n/n9163712b68ad

今から始めるServerless Frameworkでサーバーレスデビュー【フロントエンド編#2】
https://note.com/ryoppei/n/n0858cfca7784

今から始めるServerless Frameworkでサーバーレスデビュー【プラグイン番外編#3】https://note.com/ryoppei/n/ne110ac6a440f

今から始めるServerless Frameworkでサーバーレスデビュー【ファンクション番外編#4】https://note.com/ryoppei/n/n71809e2520e0

今から始めるServerless Frameworkでサーバーレスデビュー【マッピングテンプレート番外編#5】
https://note.com/ryoppei/n/n20b4afd705e5

この記事が気に入ったらサポートをしてみませんか?