Blog 開発ブログ

Blog

Auth0 + AppSync + StepFunctions + Stripe + Vuejs(Nuxt.js)でサーバーレスECサイトを作る

概要

以下の様な構成でGraphQLでサーバーレスなECサイトを作ってみましょう。AppSyncのバックエンドにStepFunctionsに置くことで決済のトランザクションを可能にしています。トランザクションの結果はGraphQLのSubscriptionでフロントエンド側に通知を行う構成になっています。

GraphQLスキーマ

スキーマを以下の様に定義します。createPayment ミューテーションで購入リクエストをブラウザから送ります。そして、StepFunctionsの最後のステップでpublishPaymentResult ミューテーションに購入処理のトランザクション結果を送り、ブラウザでは onGetResult サブスクリプションで結果を受け取る設計になってます。

type Mutation {
	createPayment(input: PaymentInput!): Payment
	publishPaymentResult(result: ResultInput): Result
}

type Payment {
	id: ID!
	price: Int!
	amount: Int!
}

input PaymentInput {
	id: ID!
	price: Int!
	amount: Int!
}

type Query {
	getPayment(id: ID!): Payment
}

type Result {
	id: ID!
	status: ResultStatus!
}

input ResultInput {
	id: ID!
	status: ResultStatus!
}

enum ResultStatus {
	SUCCESS
	FAILURE
}

type Subscription {
	onGetResult(id: ID!): Result
		@aws_subscribe(mutations: ["publishPaymentResult"])
}

schema {
	query: Query
	mutation: Mutation
	subscription: Subscription
}

AppSyncの設定

Auth0による認証

OIDC認証を以下のように設定します。プロバイダードメインには契約したあなたのAuth0テナントのURLを設定しましょう

StepFunctionsとの連携

Classmethodの岩田さんのAppSyncのクエリからStep Functionsのステートマシンを起動して時間のかかる処理を非同期に実行する を参考にさせてもらいました。

記事のとおりにHTTPのデータソースを作成してそこからIAMで認可させてStepFunctionsのエンドポイントにリクエストを投げています。

リクエストのマッピングテンプレートは以下のように定義しています。
$context.identity.claims.substripe_user_id などを渡すことでStepFunctions内のLambdaでAuth0上のユーザデータやStripe上のデータとやり取りできるようにしています。

{
  "version": "2018-05-29",
  "method": "POST",
  "resourcePath": "/",
  "params": {
    "headers": {
      "content-type": "application/x-amz-json-1.0",
      "x-amz-target":"AWSStepFunctions.StartExecution"
    },
    "body": {
      "stateMachineArn": "arn:aws:states:us-east-1:<accountID>:stateMachine:myStateMachine",
      "input": "{ \"id\": \"$context.arguments.input.id\", \"idToken\": \"$context.request.headers.authorization\", \"user_id\":\"$context.identity.claims.sub\", \"stripe_user_id\":\"$context.identity.claims['https://serverless-ec.com/stripe_user_id']\" }"
    }
  }
}

StepFunctionsの設定

以下の様にワークフローを定義します。決済のトランザクション処理を実施できる仕組みになっています。どこかで処理が失敗した場合には処理をロールバックさせて失敗したことを通知出来るような仕組みになっています。

それぞれのステップでは以下の処理を実施しています。

ChargeStripeStripeのChargeAPIを叩いて決済処理を実施します
Order注文処理を実施します
NotificationSuccessPublishPaymentResultミューテーションに成功を送信します
Rollback注文処理が失敗した場合にStripeの決済を取り消して状態を元に戻します
NotificationErrorPublishPaymentResultミューテーションに失敗を送信します

ChargeStripeでは以下のような形で決済処理を実施します。

'use strict';

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

module.exports.handler = async event => {
  const charge = await stripe.charges.create({
    amount: 3000,
    currency: "jpy",
    description: 'テスト決済',
    customer: event.stripe_user_id
  })

  return {
	  id: event.id,
	  idToken: event.idToken,
	  charge: charge.id
  }
}

そして処理がすべて成功したらNotificationSuccessで成功通知用にミューテーションを送ります。以下のコードもClassmethodの岩田さんの記事を参考にさせてもらいました。

'use strict';
const axios = require('axios')

const PublishPaymentResultMutation = `mutation PublishPaymentResult(
    $id: ID!,
    $status: ResultStatus!
  ) {
    publishPaymentResult(result: {id: $id, status: $status}) {
      id
      status
    }
  }`;

module.exports.handler = async event => {
  console.log(event)

  const mutation = {
    query: PublishPaymentResultMutation,
    operationName: 'PublishPaymentResult',
    variables: {
      id: event.id,
      status: 'SUCCESS'
    },
  };

  try {
    const response = await axios({
	method: 'POST',
	url: process.env.APPSYNC_URL,
	data: JSON.stringify(mutation),
	headers: {
	  'Content-Type': 'application/json',
	  'Authorization': `${event.idToken}`,
	}
    });
} catch (error) {
    console.error(`[ERROR] ${error.response.status} - ${JSON.stringify(error.response.data)}`);
    throw error;
  }
};

Rollbackでは以下の様なコードで決済をRefundする処理を書いています

'use strict';

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

module.exports.handler = async event => {
  const error = JSON.parse(event.Cause)
  const data = JSON.parse(error.errorMessage)
  await stripe.refunds.create({
    charge: data.charge
  })

  return {
	  id: data.id,
	  idToken: data.idToken
  }
}

Vue.js(Nuxt.js)からGraphQLにリクエストを送るためにApolloクライアントを設定する

Auth0 と連携する

フロントエンドのサンプルは Vue.js(Nuxt.js) で作成していきます。先ずはOIDC 認証モードでリクエストを送るためにIDトークンを取得する必要があり、フロントエンド側とAuth0を連携する設定を行います。package.json に @nuxtjs/auth モジュールを追加し、nuxt.config.js に以下の設定を追加します。

const config = {
  modules: [
    // ...
    '@nuxtjs/auth'
  ],
  // ...
  auth: {
    strategies: {
      auth0: {
        domain: 'xxx.auth0.com' // Auth0 Application Domain
        client_id: '...' // Auth0 Application Client ID,
        scope: [ 'openid' ],
        response_type: 'id_token token',
        token_key: 'id_token'
      }
    },
    redirect: {
      login: '/',
      logout: '/',
      callback: '/callback',
      home: '/main'
    }
  }
}

サインイン画面に遷移させたいところで以下のように実装します。IDトークンを取得する方法については後述します。

  methods: {
    login() {
      this.$auth.loginWith('auth0')
    }
  }

Apollo クライアントを設定、利用する

package.json にいくつか必要なモジュールを追加します。

yarn add @nuxtjs/apollo aws-appsync aws-appsync-subscription-link apollo-link

nuxt.config.js には以下のように設定を追加します。

const config = {
  modules: [
    // ...
    '@nuxtjs/apollo'
  ],
  // ...
  apollo: {
    authenticationType: '',
    clientConfigs: {
      default: '~/apollo/config.js'
    }
  },
}

注意点としてはauthenticationType に空文字 '' を設定する必要があります。Bearer のような認証タイプは特に指定しません。

続いて、Apollo クライアントの設定ファイル ~/apollo/config.js を書いていきます。AppSyncのSubscriptionを利用するためには、AppSyncのリアルタイムエンドポイントとのWebSocket接続を確立してくれるモジュールを利用する必要があります。


import { Context } from '@nuxt/types/app'
import { ApolloLink } from 'apollo-link'
import { AUTH_TYPE } from 'aws-appsync'
import { createSubscriptionHandshakeLink } from 'aws-appsync-subscription-link'

export default ctx => {

  const { APPSYNC_GRAPHQL_ENDPOINT, APPSYNC_REGION } = ctx.env
  const link = ApolloLink.from([

    createSubscriptionHandshakeLink({
      url: APPSYNC_GRAPHQL_ENDPOINT,
      region: APPSYNC_REGION,
      auth: {
        type: AUTH_TYPE.OPENID_CONNECT,
        // Context, Vuex, localStorage 等からセット可能
        jwtToken: () => 'idToken'
      }
    })

  ])
  return { link }
}

以上、AppSync とつなぐための準備ができました。Vue コンポーネントの中で apollo プロパティを利用するか、this.$apollo.getClient()でクライアントインスタンスを取得するなどして、query/mutation/subscriptionを実装できます。

以下のようにAuth0連携で取得した ID Token を指定することもできます。

// ID Token を取得
const bearerIdToken = this.$auth.getToken('auth0')

// 先頭 'Bearer' 文字列を外す
const idToken = bearerToken.substring(constants.BEARER_PREFIX.length)

// クライアントに ID Token をセット
this.$apolloHelpers.onLogin(idToken)

// GraphQLリクエストを送る
this.$apollo.getClient().query({ /* ... */ })
this.$apollo.getClient().mutation({ /* ... */ })
this.$apollo.getClient().subscribe({ /* ... */ })

動作確認

ローカルでサイトにアクセスして、まずはAuth0からログインを行います。

すると購入ページに遷移するので購入してみましょう。

以下の通りちゃんと結果をサブスクリプションで受け取ることが出来ました。

今度はOrder処理をわざと落としてちゃんとロールバックして失敗の結果が通知くるか検証してみましょう。

以下の通り失敗のステータスで結果が受け取れました。

StepFunctionsは以下の通りOrderが失敗してRollbackが発生していることがわかります。

Stripeでもちゃんと返金処理が実施されていました。

Our Partners

サーバーレスで
クラウドの価値を最大限にMaximize the cloud value with serverless

Serverless Operationsはこれまでグローバルの第一線で培ってきたクラウド技術(AWS − アマゾンウェブサービス)の豊富な実績と知見を活かし、お客さまのサーバーレスに関するさまざまな課題を解決します。