Closed37

Serverless Framework勉強する

とりあえずhello-worldテンプレートからプロジェクトを作ってみる

❯ sls create -t hello-world -p sls-hello-world 
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/kawarimidoll/ghq/github.com/kawarimidoll/sls-hello-world"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v2.46.0
 -------'

Serverless: Successfully generated boilerplate for template: "hello-world"

❯ cd sls-hello-world

❯ ls -A1
.npmignore
handler.js
serverless.yml

生成されたファイルは以下のようになっている(コメントは除く)

handler.js
'use strict';

module.exports.helloWorld = (event, context, callback) => {
  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*', // Required for CORS support to work
    },
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);
};
serverless.yml
service: sls-hello-world

frameworkVersion: '2'

provider:
  name: aws
  runtime: nodejs12.x

functions:
  helloWorld:
    handler: handler.helloWorld
    events:
      - http:
          path: hello-world
          method: get
          cors: true

sls invokeで実行できるので、--function helloWorldを指定してみる

❯ sls invoke local --function helloWorld
Serverless: Deprecation warning: Resolution of lambda version hashes was improved with better algorithm, which will be used in next major release.
            Switch to it now by setting "provider.lambdaHashingVersion" to "20201221"
            More Info: https://www.serverless.com/framework/docs/deprecations/#LAMBDA_HASHING_VERSION_V2
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":\"\"}"
}

handler.helloWorldの内容が実行された

warningメッセージが出ているので、設定を追加する

serverless.yml
provider:
  name: aws
  runtime: nodejs12.x
+  lambdaHashingVersion: 20201221

handler.helloWorldの実行結果にはinputの部分があるので、引数を--dataオプション経由で追加して実行してみる

❯ sls invoke local --function helloWorld --data 'this is the input!'
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":\"this is the input!\"}"
}

入力値が反映された

awsの情報を設定

❯ aws configure
AWS Access Key ID [None]: (入力)
AWS Secret Access Key [None]: (入力)
Default region name [None]: us-west-2
Default output format [None]: 

デプロイ実行、暫し待つ

❯ sls deploy --verbose --region us-west-2
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
(略)
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service sls-hello-world.zip file to S3 (578 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
(略)
Serverless: Stack update finished...
Service Information
service: sls-hello-world
stage: dev
region: us-west-2
stack: sls-hello-world-dev
resources: 12
api keys:
  None
endpoints:
  GET - (略)
functions:
  helloWorld: sls-hello-world-dev-helloWorld
layers:
  None

Stack Outputs
(略)

1分50秒かかった

ローカルディレクトリを確認すると.serverlessディレクトリが作成されている
この.serverless/sls-hello-world.zipがlambdaへアップロードされているもよう

❯ tree
.
├── .npmignore
├── .serverless
│  ├── cloudformation-template-create-stack.json
│  ├── cloudformation-template-update-stack.json
│  ├── serverless-state.json
│  └── sls-hello-world.zip
├── handler.js
└── serverless.yml

実際lambdaを見るとsls deployの結果のfunctionsに出ていたsls-hello-world-dev-helloWorldが設定されているのがわかる

なおregionをいちいち指定するのが面倒なのでserverless.ymlに追加しておく

serverless.yml
provider:
  name: aws
  runtime: nodejs12.x
  lambdaHashingVersion: 20201221
+  region: us-west-2

実行してみる

❯ sls invoke --function helloWorld --data 'deploy ok!'
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":\"deploy ok!\"}"
}

実行できた

あと、lambdaのアプリケーションの方にもちゃんと上がっていた

CloudWatchのログも確認しておく

CloudWatch > Log groupsから今回のlambda functionに対応するグループを選択

ログストリームができていることが確認できる

ログの出力を確認するため、handler.jsをちょっと修正してみる

handler.js
'use strict';

module.exports.helloWorld = (event, context, callback) => {
  const response = {
    // (略)
  };

+  console.log("Hello Serverless World!")

  callback(null, response);
};

これでデプロイし、再度sls invokeでfunctionを実行する

再びCloudWatchへ戻りログを見てみる(デプロイし直したのでログストリームが変わる)

console.log()した内容がCloudWatchへ出力されていることが確認できた

serverless.ymlhelloWorld.events.httpが登録されているので公開APIが作られている
deployの実行結果で表示されたentrypointへアクセスしてみる

❯ curl https://xxx.execute-api.us-west-2.amazonaws.com/dev/hello-world
{"message":"Go Serverless v1.0! Your function executed successfully!","input":{"resource":"/hello-world","path":"/hello-world","httpMethod":"GET","headers":(略)

DynamoDBを扱うため、serverless.ymlに設定を追加する

serverless.yml
service: sls-hello-world
frameworkVersion: '2'

provider:
  name: aws
  runtime: nodejs12.x
  region: us-west-2
  lambdaHashingVersion: 20201221
+   environment:
+     DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
+   iamRoleStatements:
+    - Effect: Allow
+      Action:
+        - dynamodb:Query
+        - dynamodb:Scan
+        - dynamodb:GetItem
+        - dynamodb:PutItem
+        - dynamodb:UpdateItem
+        - dynamodb:DeleteItem
+      Resource: arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}

funcsions:
  (略)

+ resources:
+   Resources:
+     helloTable:
+       Type: AWS::DynamoDB::Table
+       Properties:
+         AttributeDefinitions:
+           - AttributeName: id
+             AttributeType: S
+         KeySchema:
+           - AttributeName: id
+             KeyType: HASH
+         ProvisionedThroughput:
+           ReadCapacityUnits: 1
+           WriteCapacityUnits: 1
+         TableName: ${self:provider.environment.DYNAMODB_TABLE}

追加した点の解説

provider

serverless.yml
service: sls-hello-world
provider:
  name: aws
  region: us-west-2
+   environment:
+     DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
+   iamRoleStatements:
+    - Effect: Allow
+       Action:
+         - dynamodb:Query
+         - dynamodb:Scan
+         - dynamodb:GetItem
+         - dynamodb:PutItem
+         - dynamodb:UpdateItem
+         - dynamodb:DeleteItem
+       Resource: arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}
  • environmentでテーブル名を環境変数に設定している
    • ${self:xxx}serverless.ymlの内容を、${opt:xxx}はコマンドライン引数にアクセスできる
      • たとえばsls deploy --stage prodが実行されたら${opt:stage}prodになる
    • ${opt:xxx, self:xxx}の場合、前者が存在すればその値を、なければ後者の値を使用する
    • ${self:provider.stage}のデフォルト値は'dev'
    • ということで今回は引数がなければDYNAMODB_TABLE=sls-hello-world-devが環境変数に追加される
  • iamRoleStatementsでlambdaから他のAWSリソースを触る際に必要なIAMロールを設定する
    • ActionでDynamoDBへの各種アクションを許可
    • Resourceで対象となるリソースを設定
      • ${opt:region, self:provider.region}は前述の通りregionが引数として指定されていればその値を、なければproviderの値を、それもなければデフォルト値(us-east-1)が使用される
      • 今回はregion:us-west-2を指定しているのでリソース名はarn:aws:dynamodb:us-west-2:*:table/sls-hello-world-devとなる

resources

serverless.yml
+ resources:
+   Resources:
+     helloTable:
+       Type: AWS::DynamoDB::Table
+       Properties:
+         AttributeDefinitions:
+           - AttributeName: id
+             AttributeType: S
+         KeySchema:
+           - AttributeName: id
+             KeyType: HASH
+         ProvisionedThroughput:
+           ReadCapacityUnits: 1
+           WriteCapacityUnits: 1
+         TableName: ${self:provider.environment.DYNAMODB_TABLE}
  • Resources配下のキーがリソースの論理IDとなる、今回はhelloTable(一意な値なら何でもOK)
  • AttributeDefinitionsKeySchemaでテーブルのパーティションキーを設定
  • コスト削減のためCapacityUnitsは読み書きともに1に設定
  • スタックを削除した際にテーブルを削除したくない場合は(DeletionPolicy 属性)を設定する

deployしたところDeprecation warningが表示された

❯ sls deploy
Serverless: Deprecation warning: Variables resolver reports following resolution errors:
              - Cannot resolve variable at "provider.environment.DYNAMODB_TABLE": Value not found at "self" source
            From a next major this will be communicated with a thrown error.
            Set "variablesResolutionMode: 20210326" in your service config, to adapt to new behavior now
            More Info: https://www.serverless.com/framework/docs/deprecations/#NEW_VARIABLES_RESOLVER
Serverless: Deprecation warning: Starting with version 3.0.0, following property will be replaced:
              "provider.iamRoleStatements" -> "provider.iam.role.statements"
            More Info: https://www.serverless.com/framework/docs/deprecations/#PROVIDER_IAM_SETTINGS

一件目はprovider.environment.DYNAMODB_TABLEが参照できないとなっているが、sls printするとちゃんと見えているので問題はなさそう
variablesResolutionMode: 20210326を設定するとエラーで止まってしまう
いろいろ試してみたが間違っていることはなさそうだし修正できなかったのでこれに関する変更は無し
バージョンが上がってエラーを起こすようになったら使えなくなるかも…

二件目は記法が変わるようなので修正しておく

serverless.yml
-   iamRoleStatements:
+   iam:
+     role:
+       statements:
      # 以下インデント修正…

ということでiamRoleStatementsの部分のみ修正してデプロイ実行

DynamoDBのコンソールを見るとたしかにsls-hello-world-devテーブルが作成されている

また、IAM > ロール > sls-hello-world-dev-us-west-2-lambdaRole(今回のデプロイに対応するポリシー)を確認すると、DynamoDBの読み書き権限が設定されている

ここからは、公式チュートリアルとサンプルリポジトリを見つつ、TODOリストを実装する

https://www.serverless.com/examples/aws-node-rest-api-with-dynamodb
https://github.com/serverless/examples/tree/master/aws-node-rest-api-with-dynamodb

aws-sdkuuidを導入

❯ yarn init -y && yarn add aws-sdk uuid && echo node_modules >> .gitignore

APIはREST形式に従う

path method DynamoDBのaction 作成するfunction名
/todos GET Scan list
/todos/:id GET GetItem get
/todos POST PutItem create
/todos/:id PUT UpdateItem update
/todos/:id DELETE DeleteItem delete

Scanを行うlist.jsを作成する

lambda/todos/list.js
const { DynamoDB } = require("aws-sdk");

const dynamoDB = new DynamoDB.DocumentClient();
const params = {
  TableName: process.env.DYNAMODB_TABLE,
};

module.exports.list = (event, context, callback) => {
  dynamoDB.scan(params, (error, result) => {
    if (error) {
      console.error(error);

      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { "Content-Type": "text/plain" },
        body: "Couldn't fetch the todos.",
      });
      return;
    }

    callback(null, {
      statusCode: 200,
      body: JSON.stringify(result.Items),
    });
  });
};

解説:

  • aws-sdkからDynamoDBをインポートし、クライアントを生成する
  • クライアントに渡すパラメータにテーブル名を入れるが、serverless.ymlで定義したとおり、DYNAMODB_TABLEという環境変数に保存されている
  • ハンドラのmodule.exports.[handler name] = (event, context, callback) => { callback(null, {}) }はおまじない
  • 実行結果のコールバックをdynamoDB.scan()の第2引数に渡す

追加したlambda/todos/todos.list.jsserverless.ymlに追記する

serverless.yml
functions:
+   list:
+     handler: lambda/todos/list.list
+     events:
+       - http:
+           path: todos
+           method: get
+           cors: true

ローカルでテスト実行

❯ sls invoke local --function list
{
    "statusCode": 200,
    "body": "[]"
}

DBは現在からっぽ

PutItemを行うcreate.jsを作成する

lambda/todos/create.js
const uuid = require("uuid");
const { DynamoDB } = require("aws-sdk");

const dynamoDB = new DynamoDB.DocumentClient();
const errMsg = "Couldn't create the todo item.";

module.exports.create = (event, context, callback) => {
  console.log(`input: ${event.body}`);

  const data = event.body;

  if (typeof data?.text !== "string") {
    console.error("Validation failed");
    callback(new Error(errMsg));
    return;
  }

  const timestamp = new Date().getTime();

  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: uuid.v4(),
      text: data.text,
      checked: false,
      createdAt: timestamp,
      updatedAt: timestamp,
    },
  };

  dynamoDB.put(params, (error, result) => {
    if (error) {
      console.error(error);
      callback(new Error(errMsg));
      return;
    }

    callback(null, {
      statusCode: 200,
      body: JSON.stringify(params.Item),
    });
  });
};

解説:

  • helloWorldでやったように、eventからデータを受け取る
    • 受け取ったデータが無ければエラーを返す
  • スキーマで定義されているのはidだけだがDynamoDBではスキーマ以外の項目は任意なので追加可能
  • iduuid.v4()で生成している

追加したlambda/todos/create.create.jsserverless.ymlに追記する

serverless.yml
functions:
+   create:
+     handler: lambda/todos/create.create
+     events:
+       - http:
+           path: todos
+           method: post
+           cors: true

ローカルでテスト実行

❯ sls invoke local --function create
undefined
Validation failed
{
    "errorMessage": "Couldn't create the todo item.",
    "errorType": "Error",
    "stackTrace": [(略)]
}

❯ sls invoke local --function create --data '{"body":{"text":"test todo"}}'
{ text: 'test todo' }
{
    "statusCode": 200,
    "body": "{\"id\":\"6234b715-af2e-412f-abd8-0547367dfdc9\",\"text\":\"test todo\",\"checked\":false,\"createdAt\":1623758628964,\"updatedAt\":1623758628964}"
}

❯ sls invoke local --function list
{
    "statusCode": 200,
    "body": "[{\"checked\":false,\"createdAt\":1623758628964,\"text\":\"test todo\",\"id\":\"6234b715-af2e-412f-abd8-0547367dfdc9\",\"updatedAt\":1623758628964}]"
}

DBにデータを追加できた

GetItemを行うget.jsを作成する わりとlist.jsに近い

lambda/todos/get.js
const { DynamoDB } = require("aws-sdk");

const dynamoDB = new DynamoDB.DocumentClient();

module.exports.get = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };

  dynamoDB.get(params, (error, result) => {
    if (error) {
      console.error(error);

      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { "Content-Type": "text/plain" },
        body: "Couldn't fetch the todo item.",
      });
      return;
    }

    if (result?.Item) {
      callback(null, {
        statusCode: 200,
        body: JSON.stringify(result.Item),
      });
      return;
    }

    console.warn(`Item not found: id: ${params.Key.id}`);

    callback(null, {
      statusCode: 404,
      headers: { "Content-Type": "text/plain" },
      body: "Item not found.",
    });
  });
};

解説:

  • ほぼlist.jsに近いが、scanではなくgetを実行している
    • このとき渡すパラメータにKey:{ id: event.pathParameters.id }を入れている

追加したlambda/todos/get.jsserverless.ymlに追記する
パラメータは{id}の形で指定する

serverless.yml
functions:
+   get:
+     handler: lambda/todos/get.get
+     events:
+       - http:
+           path: todos/{id}
+           method: get
+           cors: true

ローカルでテスト実行、createのときにできたidを使う

❯  sls invoke local --function get --data '{"pathParameters":{"id":"6234b715-af2e-412f-abd8-0547367dfdc9"}}'
{
    "statusCode": 200,
    "body": "{\"checked\":false,\"createdAt\":1623758628964,\"text\":\"test todo\",\"id\":\"6234b715-af2e-412f-abd8-0547367dfdc9\",\"updatedAt\":1623758628964}"
}

❯  sls invoke local --function get --data '{"pathParameters":{"id":"hogehoge"}}' 
Item not found: id: hogehoge
{
    "statusCode": 404,
    "headers": {
        "Content-Type": "text/plain"
    },
    "body": "Item not found."
}

データを取得できた

UpdateItemを行うupdate.jsを作成する

lambda/todos/update.js
const { DynamoDB } = require("aws-sdk");

const dynamoDB = new DynamoDB.DocumentClient();

module.exports.update = (event, context, callback) => {
  const data = event.body;
  console.log(`input: ${data?.text} ${data?.checked}`);

  // validation
  // data is not exist || data.text is not string || data.checked is not boolean
  if (typeof data?.text !== "string" && typeof data.checked !== "boolean") {
    console.error("Validation Failed");
    callback(null, {
      statusCode: 400,
      headers: { "Content-Type": "text/plain" },
      body: "Couldn't update the todo item.",
    });
    return;
  }

  const timestamp = new Date().getTime();

  const expressionAttributeNames = {};
  const expressionAttributeValues = {
    ":updatedAt": timestamp,
  };

  const updateExpressionArray = ["updatedAt = :updatedAt"];
  if (data.text) {
    expressionAttributeNames["#todo_text"] = "text";
    expressionAttributeValues[":text"] = data.text;
    updateExpressionArray.push("#todo_text = :text");
  }
  if (data.checked != null) {
    expressionAttributeValues[":checked"] = data.checked;
    updateExpressionArray.push("checked = :checked");
  }

  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
    ExpressionAttributeValues: expressionAttributeValues,
    UpdateExpression: `SET ${updateExpressionArray.join(",")}`,
    ReturnValues: "ALL_NEW",
  };

  // add ExpressionAttributeNames when it is not empty
  if (Object.keys(expressionAttributeNames).length > 0) {
    params.ExpressionAttributeNames = expressionAttributeNames;
  }

  dynamoDB.update(params, (error, result) => {
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { "Content-Type": "text/plain" },
        body: "Couldn't fetch the todo item.",
      });
      return;
    }

    callback(null, {
      statusCode: 200,
      body: JSON.stringify(result.Attributes),
    });
  });
};

解説:

  • updateにわたすパラメータが重要
    • ExpressionAttributeValuesに更新する具体的な値を入れる
    • UpdateExpressionに更新する式を入れる
      • 例えばcheckedを更新する場合は 'SET checked = :checked, updatedAt = :updatedAt'となる
    • 更新するカラムを別名で指定する場合はExpressionAttributeNamesに指定する
      • 今回はtextカラムをそのまま更新しようとするとAttribute name is a reserved keyword; reserved keyword: textが出るので名前を変更している
      • この項目が空だったり使わないのに指定した場合はエラーが出るのでtextを更新する場合のみ追加する
    • ReturnValuesALL_NEWを指定することで更新後のレコード全体を返してくれる

追加したlambda/todos/update.jsserverless.ymlに追記する

serverless.yml
functions:
+   update:
+     handler: lambda/todos/update.update
+     events:
+       - http:
+           path: todos/{id}
+           method: put
+           cors: true

ローカルでテスト実行

❯ sls invoke local --function update --data '{"pathParameters":{"id":"6234b715-af2e-412f-abd8-0547367dfdc9"},"body":{"checked":true}}'
input: undefined true
{
    "statusCode": 200,
    "body": "{\"checked\":true,\"createdAt\":1623758628964,\"text\":\"test todo\",\"id\":\"6234b715-af2e-412f-abd8-0547367dfdc9\",\"updatedAt\":1623803748901}"
}

❯ sls invoke local --function update --data '{"pathParameters":{"id":"6234b715-af2e-412f-abd8-0547367dfdc9"},"body":{"text":"updated!"}}'
input: updated! undefined
{
    "statusCode": 200,
    "body": "{\"checked\":true,\"createdAt\":1623758628964,\"text\":\"updated!\",\"id\":\"6234b715-af2e-412f-abd8-0547367dfdc9\",\"updatedAt\":1623803797589}"
}

データを更新できた

DeleteItemを行うdelete.jsを作成する

lambda/todos/delete.js
const { DynamoDB } = require("aws-sdk");

const dynamoDB = new DynamoDB.DocumentClient();

module.exports.delete = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
    ReturnValues: "ALL_OLD",
  };

  dynamoDB.delete(params, (error, result) => {
    if (error) {
      console.error(error);

      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { "Content-Type": "text/plain" },
        body: "Couldn't fetch the todo item.",
      });
      return;
    }

    console.log(JSON.stringify(result));

    if (result?.Attributes) {
      callback(null, {
        statusCode: 200,
        body: JSON.stringify(result.Attributes),
      });
      return;
    }

    console.warn(`Item not found: id: ${params.Key.id}`);

    callback(null, {
      statusCode: 404,
      headers: { "Content-Type": "text/plain" },
      body: "Item not found.",
    });
  });
};

解説:
-ほぼget.jsgetdeleteに変えただけ

  • 削除対象が存在したかどうかわからないのでパラメータにReturnValues: "ALL_OLD"を設定する
  • これにより削除結果がAttributesというキーで帰ってくるのでそれを返す
    • 帰ってこなければ対象のデータがないということなので404を返却

追加したlambda/todos/delete.jsserverless.ymlに追記する

serverless.yml
functions:
+   delete:
+     handler: lambda/todos/delete.delete
+     events:
+       - http:
+           path: todos/{id}
+           method: delete
+           cors: true

ローカルでテスト実行
(ミスってデータを消してしまったので再度createから)

❯ sls invoke local --function create --data '{"body":{"text":"new one"}}'
input: [object Object]
{
    "statusCode": 200,
    "body": "{\"id\":\"a0145b57-e7ce-448b-8e3c-5827a3a149f2\",\"text\":\"new one\",\"checked\":false,\"createdAt\":1623805583487,\"updatedAt\":1623805583487}"
}

❯ sls invoke local --function list                                                                            
{
    "statusCode": 200,
    "body": "[{\"checked\":false,\"createdAt\":1623805583487,\"text\":\"new one\",\"id\":\"a0145b57-e7ce-448b-8e3c-5827a3a149f2\",\"updatedAt\":1623805583487}]"
}

❯ sls invoke local --function delete --data '{"pathParameters":{"id":"a0145b57-e7ce-448b-8e3c-5827a3a149f2"}}'
{
    "statusCode": 200,
    "body": "{\"checked\":false,\"createdAt\":1623805583487,\"text\":\"new one\",\"id\":\"a0145b57-e7ce-448b-8e3c-5827a3a149f2\",\"updatedAt\":1623805583487}"
}

❯ sls invoke local --function list                                                                            
{
    "statusCode": 200,
    "body": "[]"
}

データを消去できた

serverless.ymlがでかくなってきたので分割する
functionsresources以下を別ファイルに出して${file()}構文で読み込む

serverless.yml
service: sls-hello-world
frameworkVersion: "2"

provider:
  name: aws
  runtime: nodejs12.x
  region: us-west-2
  lambdaHashingVersion: 20201221
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource: arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}

functions: ${file(./resources/functions.yml)}

resources:
  - ${file(./resources/dynamodb.yml)}
ersources/functions.yml
helloWorld:
  handler: lambda/handler.helloWorld
  events:
    - http:
        path: hello-world
        method: get
        cors: true

list:
  handler: lambda/todos/list.list
  events:
    - http:
        path: todos
        method: get
        cors: true

create:
  handler: lambda/todos/create.create
  events:
    - http:
        path: todos
        method: post
        cors: true

get:
  handler: lambda/todos/get.get
  events:
    - http:
        path: todos/{id}
        method: get
        cors: true

update:
  handler: lambda/todos/update.update
  events:
    - http:
        path: todos/{id}
        method: put
        cors: true

delete:
  handler: lambda/todos/delete.delete
  events:
    - http:
        path: todos/{id}
        method: delete
        cors: true
resources/dynamodb.yml
Resources:
  helloTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      TableName: ${self:provider.environment.DYNAMODB_TABLE}

デプロイ実行

❯ sls deploy -v
(略)
Serverless: Stack update finished...
Service Information
service: sls-hello-world
stage: dev
region: us-west-2
stack: sls-hello-world-dev
resources: 42
api keys:
  None
endpoints:
  GET - https://xxx.execute-api.us-west-2.amazonaws.com/dev/hello-world
  GET - https://xxx.execute-api.us-west-2.amazonaws.com/dev/todos
  POST - https://xxx.execute-api.us-west-2.amazonaws.com/dev/todos
  GET - https://xxx.execute-api.us-west-2.amazonaws.com/dev/todos/{id}
  PUT - https://xxx.execute-api.us-west-2.amazonaws.com/dev/todos/{id}
  DELETE - https://xxx.execute-api.us-west-2.amazonaws.com/dev/todos/{id}
functions:
  helloWorld: sls-hello-world-dev-helloWorld
  list: sls-hello-world-dev-list
  create: sls-hello-world-dev-create
  get: sls-hello-world-dev-get
  update: sls-hello-world-dev-update
  delete: sls-hello-world-dev-delete
layers:
  None
(略)

sls invokeでデプロイしたlambdaにアクセスしてみる

❯ sls invoke --function list
{
    "statusCode": 200,
    "body": "[]"
}

❯ sls invoke --function create --data '{"body":{"text":"this is a task"}}'
{
    "errorType": "Runtime.UserCodeSyntaxError",
    "errorMessage": "SyntaxError: Unexpected token '.'",
    "trace": [
        "Runtime.UserCodeSyntaxError: SyntaxError: Unexpected token '.'",
        "    at _loadUserApp (/var/runtime/UserFunction.js:98:13)",
        "    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)",
        "    at Object.<anonymous> (/var/runtime/index.js:43:30)",
        "    at Module._compile (internal/modules/cjs/loader.js:999:30)",
        "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)",
        "    at Module.load (internal/modules/cjs/loader.js:863:32)",
        "    at Function.Module._load (internal/modules/cjs/loader.js:708:14)",
        "    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)",
        "    at internal/main/run_main_module.js:17:47"
    ]
}
 (略)

createでエラーが出てしまった

ランタイムがNode.js 12.xだったため、?.構文を使えないことが原因だった

https://stackoverflow.com/questions/63263172/runtime-usercodesyntaxerror-while-trying-to-declare-an-async-aws-lambda-functi

lambdaのページからランタイムを修正することで解消した

❯ sls invoke --function create --data '{"body":{"text":"this is a task"}}'
{
    "statusCode": 200,
    "body": "{\"id\":\"b5250f4e-b22c-4704-a959-711066873188\",\"text\":\"this is a task\",\"checked\":false,\"createdAt\":1623842464484,\"updatedAt\":1623842464484}"
}

ということでこれをserverless.ymlに反映し、再度デプロイ

serverless.yml
provider:
  name: aws
- runtime: nodejs12.x
+ runtime: nodejs14.x
  region: us-west-2
(略)

updateなど他のハンドラもNode.js 14.xになっていることを確認した

sls removeして削除しておく

❯ sls remove
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack delete progress...
...........................................................................
Serverless: Stack delete finished...

Lambdaからアプリケーションが削除され、DynamoDBからテーブルが削除された

現状Postmanからリクエストすると403になってしまうが、証明書を設定すると解決しそうなのでこのまま進む

プラグインを導入

❯ yarn add -D serverless-domain-manager
serverless.yml
plugins:
  - serverless-domain-manager

証明書の設定が必要っぽいのでACMから作業する

こちらの記事を参考にする

https://t-dilemma.info/static-website-hosting-by-aws-2020

ACMから「証明書のプロビジョニング」の「今すぐ始める」を選択

パブリック証明書をリクエスト

ドメイン名を追加して「次へ」

検証方法の選択はそのまま(DNSの検証)、タグ設定も特に追加せず次へ進み、確認画面で「確定とリクエスト」を押す
検証状態が「検証保留中」の画面が出てきたらひとまずOK、続行

Route53のHostedZoneの設定が出来ていなかったので設定する

Route53のダッシュボードから「ホストゾーンの作成」を選択

ドメインを入力して「ホストゾーンの作成」

これを行うとACSの独自ドメインの部分に「Route53でのレコードの作成」が出現する

ホストゾーンに設定して暫く待つ

CloudFrontのCreate Distribution→Get StartedとクリックしてCreate Distribution画面へ進む

Origin SettingsではOrigin Domain Nameにserverlessで作成されたバケット名を選択、Origin IDも自動で入力される
Enable Origin ShieldをYesにし、Origin Shield Regionは設定したRegion(今回はus-west-2)に設定
Restrict Backet AccessをYesにし、Origin Access IdentityはCreate a New Identityを選択、Commentに適当なメッセージを入力、Grant Read Permissions on BucketもYesにしておく

Default Cache Behavior SettingsではViewer Protocol PolicyをRedirect HTTP to HTTPSに、Allowed HTTP MEthodsを全部入りに設定

Route53の設定が完了すると証明書の状況が「発行済み」に、ドメインの検証状態が「成功」になる
90分近くかかった…

一旦消して再度実行したら今度は17秒で完了した、よくわからん

これでCloudFrontのDistribution Settingsから独自ドメインを選択できるようになったので設定する

AWSの設定ができたということでserverless.ymlに設定を追加

serverless.yml
+ custom:
+   customDomain:
+     domainName: api.kawarimidoll.com
+     basePath: lambda
+     certificateName: '*.kawarimidoll.com'
+     createRoute53Record: true
+     endpointType: 'regional'
+     securityPolicy: tls_1_2

ドメイン追加を実行 regionを修正していなかった場合は失敗したのでus-east-1にしておく

❯ sls create_domain
Serverless Domain Manager: Info: Custom domain api.kawarimidoll.com was created.
                        New domains may take up to 40 minutes to be initialized.

「設定に40分くらいかかるよ」といのことなので暫し待つ

serverless.ymlで設定したdomainNameに対応したAレコードとAAAAレコードがRoute53に追加される

sls deployすることでAPI Gatewayにカスタムドメインが追加される

ここまでは出来たがapi.kawarimidoll.comにアクセスしてもVercel管理下だったのでLambda実行はできなかった、残念…
今回はある程度確認できたので良しとする

なおこのデプロイ内容を取り下げる際はLambdaを消してからカスタムドメインを消す
順番を間違えると削除に失敗する可能性があるとのこと

❯ sls remove
Serverless Domain Manager: Info: Found apiId: xxxx for api.kawarimidoll.com
Serverless Domain Manager: Info: Removed basepath mapping.
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack delete progress...
..............................................................................
Serverless: Stack delete finished...

❯ sls delete_domain
Serverless Domain Manager: Info: Custom domain api.kawarimidoll.com was deleted.

いずれfreenomとか使ってリベンジするかも

https://qiita.com/sugurutakahashi12345/items/c4e0cf5708a792703b9d

ホストゾーンの維持で料金が発生していたので削除や

先にホストゾーンの詳細にアクセスしてCNAMEレコードを削除(NSとかSOAは削除できない)

SAYONARA!

ふう きれいになりました

このスクラップは2021/06/17にクローズされました
ログインするとコメントできます