【ハンズオン】サーバーレスでボタン実行のAPIを作ろう(Vue.js+Serverless Fremework)

記事タイトルとURLをコピーする

こんにちは!サーバーワークスの松井です!

サーバーレスでAPIを作成し 、WEBページからボタンを押してAPIを実行させてみようという記事になります。

このハンズオンは、VScodeでEC2にRemortSSHをして実施していただくことを想定しております。

AWSでEC2を起動してVScodeでログインしてから試してください。

スクリプトについては、基本的にコピペで作成可能ですが、不要なインデントがないかやコメントを外すなど注意が必要です。

構成図

f:id:swx-matsui:20210629094709p:plain

backendを構築

Serverless Fremeworkを使うためにnodenv環境を構築

$ sudo yum -y  install git-all
$ mkdir handson
$ cd handson
$ git clone https://github.com/riywo/anyenv ~/.anyenv
$ echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(anyenv init -)"' >> ~/.bashrc
$ exec $SHELL -l
$ anyenv install --init
$ anyenv install nodenv
$ exec $SHELL -l
$ nodenv install 15.14.0
$ nodenv local 15.14.0
$ nodenv versions
$ mkdir back-end //Serverless Fremework用のディレクトリを作成
$ cd back-end

Serverless Fremeworkを使ってAWS環境を構築します。

Serverless Fremeworkのプロジェクト作成

$ npm install serverless
$ $(npm bin)/serverless create --template aws-nodejs --name serverless-backend

AWS Lambda、APIGateway、DynamoDB、IAM用にserverless.yamlを編集する

serverless.yaml

# Serverless Frameworkのプロジェクト名
service: backend
# Serverless Frameworkのversion
frameworkVersion: "2"
provider:
  name: aws
  # 言語のversion(今回はnode.js)
  runtime: nodejs14.x
  lambdaHashingVersion: 20201221
  # ここがAPIのパスになる
  stage: items
  region: ap-northeast-1
# Lambda関数を作成
functions:
  # 関数の名前を定義
  backend:
    # lambda_handler名・APIGatewayを定義
    handler: backend.handler
    name: ${self:service}-api
    events:
      - http:
          path: /
          method: post
          integration: lambda-proxy
          cors:
            headers: '*'
      - http:
          path: /{id}
          method: get
          integration: lambda-proxy
          cors:
            headers: '*'
      - http:
          path: /{id}
          method: delete
          integration: lambda-proxy
          cors:
            headers: '*'
    # LambdaにアタッチするIAMロールを選択(resourceでRoleの権限は別途定義して作成する)
    role: !GetAtt backendrole.Arn
# IAMとDynamoDBを作成
# resources以下はCloudFormationと同じ記法が可能
resources:
  Resources:
    # IAMロールの名前を定義
    backendrole:
      Type: AWS::IAM::Role
      # 以下でIAMロールを定義
      Properties:
        # IAMロールの内容を定義
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: "Allow"
              Principal:
                Service:
                  - "lambda.amazonaws.com"
              Action:
                - "sts:AssumeRole"
        Path: /
        Description: backend
        # IAMポリシーを作成
        Policies:
          # IAMポリシーの名前
          - PolicyName: backendPolicy
            # IAMポリシーの内容を定義
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: "Allow"
                  Action:
                    - "sts:AssumeRole"
                    - "logs:CreateLogStream"
                    - "logs:PutLogEvents"
                  Resource: "*"
                - Effect: "Allow"
                  Action: 
                    - "dynamodb:DeleteItem"
                    - "dynamodb:GetItem"
                    - "dynamodb:PutItem"
                    - "dynamodb:Scan"
                    - "dynamodb:UpdateItem"
                  Resource: "arn:aws:dynamodb:*:*:table/*"
    DynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: backend-items
        # キーの型を指定
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5

Serverless Fremeworkを使って、Lambda関数を編集

編集する前にjsファイルの名前を変更してください。

$ mv handler.js backend.js

backend.js

const AWS = require("aws-sdk");

const dynamo = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event, context) => {
  let body;
  let statusCode = 200;
  const headers = {
    "Content-Type": "application/json",
  };
  console.log(JSON.stringify(event));
  try {
    if (
      event.requestContext.httpMethod == "DELETE" &&
      event.requestContext.resourcePath == "/{id}"
    ) {
      await dynamo
        .delete({
          TableName: "backend-items",
          Key: {
            id: event.pathParameters.id,
          },
        })
        .promise();
      body = `Deleted item ${event.pathParameters.id}`;
    } else if (
      event.requestContext.httpMethod == "GET" &&
      event.requestContext.resourcePath == "/{id}"
    ) {
      body = await dynamo
        .get({
          TableName: "backend-items",
          Key: {
            id: event.pathParameters.id,
          },
        })
        .promise();
      console.log(body);
    } else if (
      event.requestContext.httpMethod == "GET" &&
      event.requestContext.path == "/items"
    ) {
      body = await dynamo
        .scan({ TableName: "backend-items" })
        .promise();
    } else if (
      event.requestContext.httpMethod == "POST" &&
      event.requestContext.path == "/items"
    ) {
      let requestJSON = JSON.parse(event.body);
      await dynamo
        .put({
          TableName: "backend-items",
          Item: {
            id: requestJSON.id,
            name: requestJSON.name,
          },
        })
        .promise();
      body = `Put item ${requestJSON.id}`;
    } else {
      throw new Error(`Unsupported route: "${event.routeKey}"`);
    }
  } catch (err) {
    statusCode = 400;
    body = err.message;
  } finally {
    body = JSON.stringify(body);
  }

  return {
    statusCode,
    body,
    headers: {
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
    },
  };
};

Serverless Fremeworkを実行

$ $(npm bin)/serverless deploy

CloudFormationがAWSリソースを作成する

作成が終われば、バックエンドの準備はOK AWSマネージメントコンソールから作成したサービスを確認する。

API Gatewayの画面を開いてURLをコピーして控えておきます。

ステージ>items>URL の呼び出し:https://{hogehoge}.execute-api.ap-northeast-1.amazonaws.com/items

ターミナルでAPIを叩いて確認してみましょう

$ curl  -v \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"id": "1","name": "hoge"}'  \
  https://{hogehoge}.execute-api.ap-northeast-1.amazonaws.com/items

DynamoDBのテーブルにデータが登録されているはずです。

frontendを構築

vue.jsの実行環境を作成します。

$ cd handson
$ mkdir front-end //vue.js環境用のディレクトリを作成
$ cd front-end
$ npm install vue
$ npm install -g @vue/cli
$ echo export PATH=$PATH:`npm bin -g` >> ~/.bash_profile
$ source ~/.bash_profile
$ vue --version
$ vue init webpack front-end

? Project name front-end
? Project description A Vue.js project
? Author 
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after
 the project has been created? (recommended
) npm


$ npm install bootstrap bootstrap-vue
$ npm install axios --save

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'

import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' //追加
import 'bootstrap/dist/css/bootstrap.css' //追加
import 'bootstrap-vue/dist/bootstrap-vue.css' //追加
 
Vue.use(BootstrapVue) //追加
Vue.use(IconsPlugin) //追加

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

HelloWorld.vue

<template>
    <div class='home'>
        <b-container>
            <b-row>
                <b-col sm='12' class='mb-3'>
                    <hr>
                </b-col>
                <b-col sm='3' class='mx-auto mb-3'>
                    <b-form-input v-model='id' placeholder='id'></b-form-input>
                    <b-form-input v-model='name' placeholder='name'></b-form-input>
                </b-col>
                <b-col sm='12' class='mb-5'>
                    <b-button variant='primary' v-on:click='postData'>登録</b-button>
                </b-col>
                <b-col sm='3' class='mx-auto mb-3'>
                    <b-table striped hover :items='items'></b-table>
                    <b-form-input v-model='text' placeholder='id'></b-form-input>
                </b-col>
                <b-col sm='12' class='mb-5'>
                    <b-button variant='success' v-on:click='getData'>表示</b-button>
                </b-col>
                <b-col sm='12' class='mb-5'>
                    <b-button variant='danger' v-on:click='deleteData'>削除</b-button>
                    <hr>
                </b-col>
            </b-row>
        </b-container>
    </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'home',
  components: {
  },
  data() {
      return {
          // API Gatewayの設定パス
          path: 'https://{hogehoge}.execute-api.ap-northeast-1.amazonaws.com/items',
          id: '',
          name: '',
          text: '',
          items: []
      }
  },
  methods: {
      getData: function () {
          // 検索ID指定
          const path = this.path + '/' + this.text;
          const myInit = {
              headers: {},
              response: true,
          };
          // データ取得
          axios.get(path, myInit).then(response => {
              // テーブル表示
              console.log(response)
              this.items = [
                  {
                      id: response.data.Item.id,
                      name: response.data.Item.name
                  }
              ];
          }).catch(error => {
              // テーブルリセット
              this.items = [];
              console.log(error)
          });
      },
      postData: function () {
          const myInit = {
              headers: {},
              response: true,
                  id: String(this.id),
                  name: String(this.name)
          };
          // データ登録
          axios.post(this.path, myInit).then(response => {
              console.log(response);
          }).catch(error => {
              console.log(error)
          });
      },
      deleteData: function () {
          const path = this.path + '/' + this.text;
          const myInit = {
              headers: {},
              response: true,
          };
          // データ削除
          axios.delete(path, myInit).then(response => {
              console.log(response);
              this.items = [];
          });
      }
  }
}
</script>

※以下App.vueに関しては、編集しなくてもWEBページは表示させられます。

App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">従業員登録サイト</router-link> //変更
    </div>
    <router-view/>
  </div>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

vue.jsを実行

$ npm run dev

Open browserとポップが出てくるのでOpenをクリックすると以下の画面が表示されます。

※真っ白な画面が出てしまった場合は、どこか誤りがあります

f:id:swx-matsui:20210627234015p:plain

API実行手順

1. id・nameを入力して登録ボタンを押す

DynamoDBのテーブルに入力したid・nameのデータが表示されます。

f:id:swx-matsui:20210627235136p:plain

2. 先程作成したid・nameを入力して、表示ボタンを押す

DynamoDBのデータがWEBページに表示されます。

3. 削除ボタンを押す

DynamoDBからデータが削除されます。

お疲れさまでした!

これでWEBページからAPIを実行している挙動を確かめることができました。

確認ができたらAWSのリソースは以下で削除してしまいましょう。

serverless remove -v

以上、サーバーレスハンズオンでした。