AWS Amplify + Vue.js + AppSync + Cognito +その他諸々でリアルタイム更新なサーバレスWebアプリをデプロイ

# 昨年までのAWS Amplify

昨年のServerless Advent Calendarにて[AWS AmplifyでサーバレスWebアプリの構築](https://qiita.com/nakayama_cw/items/c4aae9ddb19c087e18cb)という投稿を書きました。
この投稿では、AWS AmplifyというJavaScriptライブラリを使用して、Cognitoで認証を行い、API Gatewayに対してリクエストを行うというものでした。

従来のREST APIを使用したWebアプリであれば、この仕組みをベースとして構築することで、大抵のWebアプリは構築可能かと思います。

# AppSync登場！

昨年のre:Invent 2017にて発表され、今年の4月にGAとなったGraphQLを使用したフルマネージドサービスであるAWS AppSyncが登場しました。
GraphQLとはや、AppSyncとはは、既に多くの記事で説明があるので、ここでは触れません。

このAppSyncが登場したことにより、サーバレスWebアプリの可能性が更に広がりました。
それは、リアルタイム更新が可能になったことです。
例えば、チャットを作る場合、従来のREST APIでは、GET /messages のようなAPIをポーリングし続け、表示を更新していく必要がありました。
しかし、AppSyncを使うことで、MQTT over Websocketsによりpub / sub構成が容易に構築でき、バックエンドのデータが更新されたと同時に、クライアントに対し更新情報がパブリッシュされるため、リアルタイムなデータ更新が実現できます。

# 更に進化するAWS Amplify

そして更に今年のre:Invent 2018にて発表されたAWS Amplify Consoleを使用することにより、サーバーレスWebアプリケーションのCI/CDパイプラインの構築やデプロイを容易に行うことができるようになりました。

# 今回のテーマ

今回は[こちら](https://qiita.com/ktsukago/items/1fd66a95420b631595a3)で紹介されているAWS Amplify + AppSync + Reactによる認証付きリアルタイムデータ更新Webアプリの構築を、Reactの代わりにVue.jsで行い、更にAWS Amplify Consoleによりデプロイまでを行いたいと思います。
また、コーディングはAWS Cloud9を使用して行います。

※今回Vue.jsを使用していますが、私自身Vue.jsをあまり使っておらず、直近の業務で使おうとしていたため、
　ついでがてら選択した次第です。下記の内容にVue.jsの使い方として至らない点がありましたら、ご指摘ください。

# やってみた

## 準備

※今回は原則としてオレゴンリージョン (us-west-2)を使用します

- CodeCommit に適当なリポジトリを作成
- Cloud9 に m4.largeのインスタンスで環境を作成
- 起動したCloud9用EC2インスタンスに、Administrator権限のRoleを割り当てる
- 下記のコマンドでCodeCommitをIAM認証にて取得できるようにする

```console
$ git config --global credential.helper '!aws codecommit credential-helper $@'
$ git config --global credential.UseHttpPath true
```

- リポジトリのclone

```console
$ git clone https://git-codecommit.us-west-2.amazonaws.com/v1/repos/amplify-webapp
```

- nodeバージョンをアップデート

```console
$ nvm install v8.14.0
$ nvm alias default v8.14.0
```

- vue-cli のインストール

```console
$ npm i -g @vue/cli
$ npm install -g @vue/cli-init
```

## Vue.js プロジェクトの作成

- 初期化

```console
$ cd amplify-webapp  # CodeCommitのリポジトリ名
$ vue init webpack .
? Generate project in current directory? Yes
? Project name amplify-webapp
? Project description A Vue.js project
? Author 
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? 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
```
今回はお試しのためユニットテスト等は入れませんでした。

- Cloud9でVue.jsを起動できるように修正

```build/webpack.dev.conf.js
- host: HOST || config.dev.host,
- port: PORT || config.dev.port,
+ host: '0.0.0.0',
+ port: '8080',
+ disableHostCheck: true,
```
※ https://qiita.com/ultrasevenstar/items/2874ba37d72db605b487 こちらの記事のおかげで解決しました

- ひとまずVue.jsの初期状態で起動できるか確認

```console
$ npm run dev
```

- Preview -> Preview Running Application で起動したページをプレビューできます

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/e7d44e78-a8f5-98d8-6474-e6ba25bb60aa.png)

ひとまずVue.jsは起動しました

## Amplify CLIを使用したセットアップ

※2019/2/1追記
Cloud9の立ち上げたばかりの環境のままだと、~/.aws/configが存在せず、下記の流れにならないと思います。
そこで下記の手順を実行後amplifyの実行を行ってください。

```console
$ vi ~/.aws/config

[default]
region=us-west-2

```
なお、Cloud9のないリージョンにデプロイしたい場合は、
AWS Managed Temporary Credentialsの設定をOFFにした上で、
EC2インスタンスに必要な権限のあるRoleを割り当て、
上記のregionをap-northest-1などのデプロイ対象リージョンコードに書き換え実行してください

```console
$ npm install -g @aws-amplify/cli
$ amplify init

? Choose your default editor: None
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script serve
Using default provider awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default

$ amplify push
```
上記により、/amplify-webapp/src/aws-exports.js が作成されます。

## まずはAppSyncを使った簡易的な表示を構築

```console
$ amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: amplifywebapp
? Choose the default authorization type for the API API key
? Enter a description for the API key: for Amplify Web App
? After how many days from now the API key should expire (1-365): 365
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? Yes
Please edit the file in your editor: /home/ec2-user/environment/amplify-webapp/amplify/backend/api/amplifywebapp/schema.graphql
? Press enter to continue
```

上記の流れで、schema.graphql を下記のように編集します

```schema.graphql
type City @model {
  id: ID!
  name: String!
  description: String
  location: String
}
```

上記の設定を反映させます

```console
$ amplify push

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Api      | amplifywebapp | Create    | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully. Edit your schema at /home/ec2-user/environment/amplify-webapp/amplify/backend/api/amplifywebapp/schema.graphql
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
```

## データの追加

上記までの作業だけで、なんと自動的にAppSyncへのスキーマ定義等が全て出来上がっています！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/d8a90b83-0c13-a0d0-ac4d-4505e96dd829.png)

データの取得やフィルターなどの操作については、[こちら](https://qiita.com/ktsukago/items/1fd66a95420b631595a3#%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%96%E5%BE%97)を参考にしていただくとして、同様にデータの追加だけ行なってみましょう

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/50b113fc-39e3-2442-2fac-62aeb198b81c.png)

左ペインからQueriesを選択し、再生マーク（実行ボタン）のボタンの下の欄に下記のように入力し、実行ボタンを押しましょう

```
mutation create {
    createCity(input:{
    name:"fukuoka"
    description:"nice food"
  }){
    id name description
  }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/64bcdcff-c21a-7ab6-28a2-b1639cc765a6.png)

追加されたようです。

DynamoDBのCity****なテーブルを確認してみると、しっかりデータが入っています

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/22f5f0e8-4711-6abf-99a8-c6f1d61a36ab.png)

## クライアント側からデータを取得する

まずはamplifyライブラリのインストール

```console
$ npm install aws-amplify aws-amplify-vue --save
```

※執筆時点（2018/12/7のaws-amplify-vue 0.1.10には [#2254](https://github.com/aws-amplify/amplify-js/issues/2254)の問題が存在しております。修正がされるまでは、 aws-amplify-vue@unstable をインストールすることで回避できます


ライブラリの読み込み
下記のように書き換えます

```src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import Amplify from 'aws-amplify'
import aws_exports from './aws-exports'
import { components } from 'aws-amplify-vue' 

Vue.config.productionTip = false

Amplify.configure(aws_exports)

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { 
    App,
    ...components
  },
  template: '<App/>'
})
```

App.vueからデフォルトのVue.jsロゴを削除します

```src/App.vue
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

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

Cityコンポーネントを作成

```src/components/City.vue
<template>
  <div class="cities">
    <template v-for="(city,index) in cities">
      <div key={{index}}>
        <h3>{{city.name}}</h3>
        <p>{{city.description}}</p>
      </div>
    </template>
  </div>
</template>

<script>
import Vue from 'vue'
import { API, graphqlOperation} from "aws-amplify"
import { listCitys } from "../graphql/queries"

export default {
  name: 'City',
  data () {
    return {
      cities: []
    }
  },
  mounted: async function () {
    let cities = await API.graphql(graphqlOperation(listCitys));
    console.log(cities);
    this.cities = cities.data.listCitys.items;
  }

}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
```

Routerの修正

```src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import City from '@/components/City'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'City',
      component: City
    }
  ]
})
```

保存し実行結果を確認します

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/5eaef6a0-1f1a-ca97-ec1c-fb1e0bd89be7.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/1f1f294e-6a6d-9801-373e-143497f544c9.png)

先程登録したデータがしっかり取得できていることが分かります。

## データの更新

続いては、サイト上からデータを追加をしましょう

```City.vue
<template>
  <div class="cities">
    <div class="createCity">
      <input v-model="name" name="name" />
      <input v-model="description" name="description" /> 
      <button v-on:click="createCity()">Create City</button>
    </div>
    <template v-for="city in cities">
      <div>
        <h3>{{city.name}}</h3>
        <p>{{city.description}}</p>
      </div>
    </template>
  </div>
</template>

<script>
import Vue from 'vue'
import { API, graphqlOperation} from "aws-amplify"
import { listCitys } from "../graphql/queries"
import { createCity } from "../graphql/mutations"

export default {
  name: 'City',
  data () {
    return {
      cities: [],
      name: "",
      description: ""
    }
  },
  mounted: async function () {
    let cities = await API.graphql(graphqlOperation(listCitys))
    console.log(cities)
    this.cities = cities.data.listCitys.items
  },
  methods: {
    createCity: async function () {
      if ((this.name === "") || (this.description === "")) return 
      const city = {name: this.name, description: this.description}
      try {
        const cities = [...this.cities, city]
        this.cities = cities
        this.name = "";
        this.description = "";
        await API.graphql(graphqlOperation(createCity, {input: city}))
        console.log('success')
      } catch (error) {
        console.log('error: ', error)
      }
    }
  },

}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
```

上記のようにCity.vueを書き換えます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/d31ce21f-55f1-be25-a71d-ed84fb9367bb.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/859fa828-c893-7bb4-afd0-ad235c0a77a2.png)

データが追加できました。

## データの購読

続いてはデータの変更がされたら、その情報をキャッチして表示を更新する処理です

```City.vue
<template>
  <div class="cities">
    <div class="createCity">
      <input v-model="name" name="name" />
      <input v-model="description" name="description" /> 
      <button v-on:click="createCity()">Create City</button>
    </div>
    <template v-for="city in cities">
      <div>
        <h3>{{city.name}}</h3>
        <p>{{city.description}}</p>
      </div>
    </template>
  </div>
</template>

<script>
import Vue from 'vue'
import { API, graphqlOperation} from "aws-amplify"
import { listCitys } from "../graphql/queries"
import { createCity } from "../graphql/mutations"
import { onCreateCity } from "../graphql/subscriptions"

export default {
  name: 'City',
  data () {
    return {
      cities: [],
      name: "",
      description: ""
    }
  },
  mounted: async function () {
    let cities = await API.graphql(graphqlOperation(listCitys))
    console.log(cities)
    this.cities = cities.data.listCitys.items
    
    API.graphql(
      graphqlOperation(onCreateCity)
    ).subscribe({
      next: (eventData) => {
        console.log('eventData: ', eventData);
        const city = eventData.value.data.onCreateCity;
        const cities = [...this.cities.filter(content => {
          return ((content.name !== city.name) && (content.description !== city.description));
        }), city];
        this.cities = cities;
      }
    });
  },
  methods: {
    createCity: async function () {
      if ((this.name === "") || (this.description === "")) return 
      const city = {name: this.name, description: this.description}
      try {
        const cities = [...this.cities, city]
        this.cities = cities
        this.name = "";
        this.description = "";
        await API.graphql(graphqlOperation(createCity, {input: city}))
        console.log('success')
      } catch (error) {
        console.log('error: ', error)
      }
    }
  },

}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
```

上記のように購読処理を追加しました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/78c425dd-2db6-26a9-6d56-2c3317df5b0d.png)

先ほどと同じように、新しい情報をAppSyncのコンソール上から追加してみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/128eb392-2775-d126-6108-462125d93ba1.png)

その瞬間、追加した情報が更新されました！
同様の処理はウィンドウを2つ開いて情報を追加した場合でも確認できます。

## 認証機能の追加

サイトの機能としては最後にCognitoを使用したログイン認証を付けてみたいと思います

```console
$ amplify add auth
Using service: Cognito, provided by: awscloudformation
 The current configured provider is Amazon Cognito. 
 Do you want to use the default authentication and security configuration? Yes, use the default configuration.
$ amplify push
```

ユーザー認証を必須としてCityコンポーネントにアクセス出来るように修正します

```router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import City from '@/components/City'
import { components, AmplifyEventBus } from 'aws-amplify-vue';
import Amplify, * as AmplifyModules from 'aws-amplify';
import { AmplifyPlugin } from 'aws-amplify-vue';

Vue.use(Router)
Vue.use(AmplifyPlugin, AmplifyModules);

let user;

getUser().then((user, error) => {
  if (user) {
    router.push({path: '/'})
  }
})

AmplifyEventBus.$on('authState', async (state) => {
  if (state === 'signedOut'){
    user = null;
    router.push({path: '/auth'})
  } else if (state === 'signedIn') {
    user = await getUser();
    router.push({path: '/'})
  }
});

function getUser() {
  return Vue.prototype.$Amplify.Auth.currentAuthenticatedUser().then((data) => {
    if (data && data.signInUserSession) {
      return data;
    } 
  }).catch((e) => {
    return null
  });
}

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'City',
      component: City,
      meta: { requiresAuth: true}
    },
    {
      path: '/auth',
      name: 'Authenticator',
      component: components.Authenticator
    }
  ]
});

router.beforeResolve(async (to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    user = await getUser();
    if (!user) {
      return next({
        path: '/auth',
        query: {
          redirect: to.fullPath,
        }
      });
    }
    return next()
  }
  return next()
})

export default router
```

保存して読み込み直すとあっという間にAmplifyのUI Componentによりログイン画面が表示されました
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/3b527254-ade0-b292-360d-7425590e32f9.png)

Create accountからサインアップを行い、ログインをします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/d8fccb5b-b108-4b3b-cf7e-80e737c4d6eb.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/24cecf0f-dcd2-9524-abd3-8a6424ac459b.png)
ログインが成功すると今までの画面が表示されました。

このままではログアウトができないので、ログアウト機能を実装します。

```City.vue
~~~省略~~~~
  <div class="cities">
    <amplify-sign-out></amplify-sign-out>
    <div class="createCity">
~~~省略~~~~
```
なんと1行追加するだけ！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/6edb9c85-24e1-9a3e-f7e7-dd0d2544159b.png)

無事サインアウトボタンが表示されました

## デプロイ

それでは最後に作ったサイトをデプロイして公開したいと思います。

デプロイの方法としては、今までだとCode Buildなどを使ってS3にビルドしたファイル群を上げて、Cloud Frontで公開というのが王道だったのかなと思いますが、今回はre:Invent 2018で新たに発表されたAWS Amplify Consoleを使ってみたいと思います。

まずは作ったアプリケーションをリポジトリにコミット

※ aws-exports.jsが.gitignoreで除外されている場合デプロイ後にエラーとなります。
　除外されている場合は.gitignoreから消して下記コマンドを叩いてください。

```console
$ git add .
$ git commit -am "create app"
$ git push
```

続いて、AWSコンソールから、[Amplify](https://us-west-2.console.aws.amazon.com/amplify/home?region=us-west-2#/)を選択しましょう


DeployのGET STARTEDを選択
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/5328392a-eb32-0ac2-4757-e8aab134625d.png)

AWS Code Commitを選択して次へ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/db4dce71-7ac2-f372-ff44-3d8678acb35a.png)

リポジトリとブランチを選択します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/9252d461-9217-122e-c514-51440ec81c0f.png)

ビルド設定を行います。
自動的にフレームワークなどを読み取ってくれていて、基本的に変更する必要なさそうな感じです
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/76111b65-c9ca-c8db-2592-ed9d8db4c0a1.png)

確認して、デプロイを実行しちゃいましょう
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/14080e2c-7af4-e02e-67cb-5c403d761ce1.png)

デプロイパイプラインが作られました
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/96e1909f-29f9-f850-4465-6dc599a8b770.png)

数分待っていると下記のようになります
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/13ee8b3e-e667-7974-26f5-9847e8f657cc.png)
なんと、スクリーンショットまで表示されているじゃないですか！

そのスクリーンショットをクリックすると実際にデプロイされた画面を表示します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/fb823cfb-73bd-2e1a-092b-6bab0e456e51.png)

ちゃんとドメインが割り振られどこからでもアクセス可能なURLが発行されていることがわかります。
先程登録したユーザーでログインを行なってみると、しっかり認証され、Cityコンポーネントが表示されました

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/8cb88c61-66ba-3fff-c688-afba8770a5a9.png)

楽しくなってきたので調子に乗って、BASIC認証をかけてみたいと思います。

左ペインのアクセスコントロールを選択します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/bcce0bb9-a43b-ff49-7ce3-7aae8e890f0c.png)

右上の「アクセスの管理」ボタンを選択すると下記の画面になります。
Access settingの欄を「制限 - パスワードが必須です」を選択し、ユーザー名とパスワードを入れましょう
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/977ee7b0-272d-bfaf-3198-286284f95612.png)

再度アクセスし直してみましょう

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/4fe08e17-aa80-836c-6564-e5d4100d9347.png)

ちゃんとBASIC認証が掛かってます！

おまけ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/d79b3162-a7d0-3334-2a25-b5d7f283864b.png)

こちらの画面の、ブランチ名（master)を選択してみましょう
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/d50ef940-c84b-534f-cf96-50a1d6141369.png)

検証の欄には、なんといくつかの端末向けにレンダリングされた結果が表示されているではないですか！

# まとめ

いかがだったでしょうか？
AmplifyとAppSyncを使用すると、こんなにも簡単にリアルタイム更新が出来るサーバーレスWebアプリをつくることが出来ます
そして、新たに発表されたAmplify Consoleを使うとデプロイまでもあっという間に行うことができます。

そして気づいたかもしれませんが、サーバーレスの代名詞的な存在であるLambdaを今回は全く使うことなく、
それどころかバックエンドのスクリプトを1文字も書くことなくリアルタイム更新なDynamoDBをデータストアとした
アプリケーションを作ってしまうことが出来ました。

***これは控えめに言って、革命です***

これからの時代を担うだろうGraphQLのフルマネージドサービス「AWS AppSync」と、
それらの設定や構築などのサーバーレスであろうとやらなければいけなかったはずのオペレーションまでも
ほぼ無意識にやってくれる、まさにサーバーレスなツール「AWS Amplify」を是非積極的に使っていきたいと思いました。