2020年6月4日 更新

【AzureでJAMstack!!】Static Web Apps × Cosmos DB × Nuxt.js × PAY.JPでECサイト作ってみる

先日開催された「Microsoft Build 2020」でAzureの新サービス「Static Web Apps」が発表されましたね!
このサービスは静的コンテンツの無償ホスティングサービスで、Azure Functions(API)と統合されており、GitHub Actionsでの自動ビルド&デプロイも初期設定済みという、JAMstackのためのサービスといっても過言ではないのではないかと!
さらにCosmos DBの無料枠と組み合わせれば全てAzure内で完結したJAMstackが構築できます!
収筆時点で「Static Web Apps」はまだプレビュー版との事ですが気になったので今回早速試してみました。
今回は実践的に「Static Web Apps」「Cosmos DB」「Nuxt.js」「GitHub Actions」「PAY.JP」を組み合わせてECサイトを作ってみました。

出来たのはこんなサイトです。
https://happy-dune-046172f00.azurestaticapps.net/



まずは使用サービスの説明からいきます。

Azure Static Web Appsについて

https://azure.microsoft.com/ja-jp/services/app-service/static/
(公式サイトより引用)

ざっくり要点を書くと下記の特徴があります。

  • 静的コンテンツのホスティング
  • コンテンツのグローバル配信
  • サーバレスAPIと統合
  • GitHub ActionsとCI/CD連携
  • カスタムドメイン、SSL対応


また、プレビュー版の仕様は下記になります。

  • 含まれる帯域幅:100 GB/月
  • 超過帯域幅:使用不可
  • Azure サブスクリプションあたりのアプリ数:10個
  • アプリのサイズ:100 MB
  • 実稼働前の環境数:1環境
  • カスタムドメイン:1個
  • ロールを割り当て:25人
  • Azure Functions:利用可能
  • SLA:なし


収筆時点で料金設定はまだプレビュー版で未公開ですが無償で始められるとの事です。
今回は、静的サイトジェネレータで生成した静的コンテンツのホスティング、及びサーバレスAPIを利用した決済処理に使用します。

Azure Cosmos DBについて

https://azure.microsoft.com/ja-jp/services/cosmos-db/
スケーラブルで高パフォーマンスなNoSQLデータベースサービスです。
様々なAPIがあり、

  • SQL API
  • Cassandra API
  • MongoDB API
  • Gremlin API
  • Azure Table Storage API

から選択することが出来ます。用途に合わせて適切なDBを選択できるのは良いですね。
400RU/秒のスループット、及び5GBのストレージを無料で提供されています。400RUを超えて処理を要求した際はエラーが返るそうなので、一度に大量のデータを扱う際はよしなに区切ってスリープ入れておくなど対策しておいた方が良さそうです。今回は商品情報のデータ管理として使用します。

PAY.JPについて

https://pay.jp/
ネットショップの開設サービスで有名なBASE株式会社の子会社、PAY株式会社が運営する決済サービスです。前回紹介したStripeと違い、こちらは和製なため日本の開発者に扱いやすく、また、手数料もStripeより同程度か、多少安いことが特徴です。

  • Stripe → 一律3.6%
  • PAY.JP → Visa/MasterCardは3.0%、他は3.6% ※プランアップで更に安くなる

開発者向けにSDKやAPIが用意されており、容易に決済処理を自前のシステムに導入することができます。

以上が各サービスの紹介になります。
それではモジュールのの準備から始めていきます。

モジュールを準備する

要点かいつまんで説明していきます。

Nuxt.jsプロジェクトを作成する

JAMstackプロジェクトなのでuniversalモードで作成します。

決済フォームを作る

PAY.JPで提供されているv2のフォームを使います。

nuxt.config.jsにスクリプトを追加

head: {
    script: [{ src: '//js.pay.jp/v2/pay.js', async: true }]
  },


画面を実装します。
cartPayContainer.vue(抜粋)

<template>
  <div class="pay-container">
    <div id="payjp-form" />
    <button class="pay-button" @click="clickCheckoutButton" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      token: ''
    }
  },
  mounted() {
    const payjp = window.Payjp('pk_test_cdabcdda7155614fe7f68cf6')
    const elements = payjp.elements()
    const cardElement = elements.create('card')
    cardElement.mount('#payjp-form')
    cardElement.on('change', async (event) => {
      if (event.complete) {
        const res = await payjp.createToken(cardElement)
        this.token = res.id
      }
    })
  },
  methods: {
    async clickCheckoutButton() {
      if (!this.token || this.token === '') return

      const axios = require('axios')
      await axios.post(`${process.env.API_URL}/charge`, {
        amount: 9999,
        token: this.token
      })
    }
  }
}
</script>

マウントされたら決済フォームを描画、入力完了後にトークン発行、決済ボタンクリックでAzure Functionsに決済処理をリクエストします。
マウント時のキーは、PAY.JPログイン後のAPIメニューから、テスト公開鍵を設定します。

決済APIを作る

最初にAzure Functions Core Toolsをインストールします。
参考→https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local?tabs=macos%2Ccsharp%2Cbash

次にapiプロジェクトを作成します。

$ func init api

ランタイム:node
言語:JavaScriptかTypeScriptかお好みで選択(今回はJavaScriptで実装します。)

下記のようなファイル群が自動生成されます。

api
┗.vscode
 ┗extensions.json
┗.gitignore
┗host.json
┗local.settings.json
┗package.json


環境の設定していきます。

PAY.JP決済処理用のライブラリの追加

$ yarn add payjp


local.settings.json
CORSの設定、PAY.JP環境変数の追加

{
  "Values": {
    "PAYJP_SK": "sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  },
  "Host": {
    "CORS": "http://localhost:3000"
  }
}


APIの新規追加

$ func new
Select a number for template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Durable Functions activity
4. Durable Functions HTTP starter
5. Durable Functions orchestrator
6. Azure Event Grid trigger
7. Azure Event Hub trigger
8. HTTP trigger
9. IoT Hub (Event Hub)
10. Azure Queue Storage trigger
11. SendGrid
12. Azure Service Bus Queue trigger
13. Azure Service Bus Topic trigger
14. SignalR negotiate HTTP trigger
15. Timer trigger

テンプレートは今回は8(HTTP trigger)を選択
関数名は「charge」(※お好みで)

すると、下記ファイルが自動生成されます。

api
┗charge
 ┗function.json
 ┗index.js


function.json
今回使用するのはPOSTだけなのでGETは削除します。

index.js

module.exports = async function(context, req) {
  try {
    const payjp = require('payjp')(process.env.PAYJP_SK)
    const result = await payjp.charges.create({
      amount: req.body.amount,
      currency: 'jpy',
      card: req.body.token
    })
    if (result && result.paid) {
      context.res = {
        body: 'NORMAL'
      }
    } else {
      throw new Error(result.error.message)
    }
  } catch (error) {
    context.res = {
      status: 500,
      body: error.message
    }
  }

  context.done()
}

PAY.JPの支払いAPIをCallします。
以上でAPIの作成は完了です。

Cosmos DBと連携する

プロジェクトルートに戻り、Cosmos DB操作用のライブラリを追加します。

$ yarn add @azure/cosmos


そしたら実際にデータ連携箇所に実装していきます。
ローカル開発及びジェネレート時で共通で使用するので今回は別モジュールに切り出しました。
modules/getCosmosData.js

module.exports.getCosmosData = async (
  COSMOS_EP,
  COSMOS_KEY,
  COSMOS_DB,
  COSMOS_CONTAINER
) => {
  const CosmosClient = require('@azure/cosmos').CosmosClient
  const client = new CosmosClient({
    endpoint: COSMOS_EP,
    key: COSMOS_KEY
  })
  const database = client.database(COSMOS_DB)
  const container = database.container(COSMOS_CONTAINER)
  const productQuerySpec = {
    query:
      'SELECT c.id, c.name, c.description, c.type, c.amount, c.image, c.hit from c where c.category = @category',
    parameters: [{ name: '@category', value: 'products' }]
  }
  const { resources: products } = await container.items
    .query(productQuerySpec)
    .fetchAll()

  const reviewQuerySpec = {
    query: 'SELECT c.name, c.comment from c where c.category = @category',
    parameters: [{ name: '@category', value: 'reviews' }]
  }
  const { resources: reviews } = await container.items
    .query(reviewQuerySpec)
    .fetchAll()
  return { products, reviews }
}

エンドポイント、キー、DB名、コンテナ名を環境変数から設定し、
商品(products)及びレビュー(reviews)データを取得しています。
各種環境変数の設定値は後の手順で説明してます。

ジェネレート時の設定を追加
nuxt.config.js

  generate: {
    async routes() {
      const module = require('./modules/getCosmosData')
      return [
        {
          route: '/',
          payload: await module.getCosmosData(
            COSMOS_EP,
            COSMOS_KEY,
            COSMOS_DB,
            COSMOS_CONTAINER
          )
        }
      ]
    }
  }


画面表示処理にも追加
pages/index.vue

async asyncData({ payload, env }) {
    const getData = async () => {
      if (payload) return payload
      const module = require('../modules/getCosmosData')
      return await module.getCosmosData(
        env.COSMOS_EP,
        env.COSMOS_KEY,
        env.COSMOS_DB,
        env.COSMOS_CONTAINER
      )
    }
    const data = await getData()
    return {
      products: [
        {
          title: 'Popular products',
          items: data.products.filter((item) => item.hit)
        },
        {
          title: 'Big Product',
          items: data.products.filter((item) => item.type === 'Big Product')
        },
        {
          title: 'Small Product',
          items: data.products.filter((item) => item.type === 'Small Product')
        }
      ],
      reviews: data.reviews
    }
  },


モジュール準備はこれでほぼ完了です。
次にAzureの設定をしていきます。

Static Web Appsの設定をする

Azureポータルを開きます。


「リソースの作成」から「Static Web Apps」を検索します。


サービスが表示されたら「作成」をクリックします。


下記画像のように各種設定をしていきます。




設定の入力が完了したら、「確認および作成」のタブから「作成」ボタンをクリックします。
そしたら自動的にGitHub Actionsの設定がリポジトリに追加され、初回ビルドが行われます。
「リソースに移動」します。


「構成」メニューから環境変数を設定します。(PAY.JPのテスト秘密鍵を設定)


これにてStatic Web Appsの設定は完了です!

Cosmos DBの設定をする

Azureポータルを開きます。


「リソースの作成」から「Azure Cosmos DB」を検索します。


サービスが表示されたら「作成」をクリックします。


下記画像のように各種設定をしていきます。※画像上に無いですが「Apply Free Tier Discount」は無料枠で使用する場合、「Apply」を選択してください。





設定の入力が完了したら、「確認および作成」のタブから「作成」ボタンをクリックします。
リソースの作成が完了したら実際にデータを作成していきます。
Containers > 設定 から作成できます。 ※無料枠内で構築する場合はスループットを400RU/sで設定してください。


最後に、設定 > キー から必要なキーをメモしておきます。

ローカルで動かしてみる

最初にAzureのCosmos DBなどの環境変数を設定します。
.env

BASE_URL=http://localhost:3000
API_URL=http://localhost:7071/api
COSMOS_EP=https://azure-payjp.documents.azure.com:443/
COSMOS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXX
COSMOS_DB=AzurePayjp
COSMOS_CONTAINER=AzurePayjpContainer


次に、実行スクリプトを修正します。
package.json

"scripts": {
    "dev": "func start --script-root api & nuxt",
    "build": "nuxt generate",
},

ローカル環境用、及びジェネレート用の設定を変更する。
ローカル環境用は、Nuxt.jsアプリケーションとAzure Functionsを同時に動かすために設定。
本番ジェネレート用は、CI/CDのデフォルトコマンドがyarn build(npm run build)なので合わせておくために設定。

設定できたら動かしてみます。

$ yarn dev


正常に動作しましたでしょうか。

最後にCi/CD設定します。(追加分のみ記載)
.github/workflows/azure-static-web-apps-XXXXXXXXXXXX.yml

jobs:
  build_and_deploy_job:
    steps:
    - uses: actions/checkout@v2
    - name: Build And Deploy
      with:
        api_build_command: 'yarn install'
      env:
          API_URL: ${{ secrets.API_URL }}
          BASE_URL: ${{ secrets.BASE_URL }}
          COSMOS_EP: ${{ secrets.COSMOS_EP }}
          COSMOS_KEY: ${{ secrets.COSMOS_KEY }}
          COSMOS_DB: ${{ secrets.COSMOS_DB }}
          COSMOS_CONTAINER: ${{ secrets.COSMOS_CONTAINER }}

APIの関連モジュールのインストールコマンド、及び環境変数の設定を追加します。

GitHub Actionsの環境変数も設定しておきます。


あとはGitHubにもろもろpushして自動ビルド&デプロイが正常に完了すれば完成です!

以上で説明は終了となります。
興味がある方は是非試してみてください!