第二回で、Amplify CLIを使ってバックエンドリソースを作成し、CICDを回してVueアプリを外部公開するところまで辿り着いた。
今回は、さらにAPIとLambda関数を用意し、Vue.jsからaxiosで呼び出すところまでをトライ。これができれば、「Vue.jsでフロントエンドを作ってAWSサービスを叩いてみる」という当初の目的を達成できたことになる。
前回の内容はこちら。
Vue.js、AWS Amplifyおよびboto3でサンプルアプリを作ってみる(第二回:Amplifyとバックエンドリソース編)
(今回)やりたいこと
- axiosをセットアップする
- APIとLambda関数を作る
- Vue.jsからaxios経由でAPIを呼び出す
あえてやらないこと
AmplifyはライブラリやUIコンポーネントを備えていて、その気になればVue.js内から直接AWSサービスを操作できる様子(例:学習/デプロイ済みの機械学習モデルエンドポイントに推論リクエストを投げる、S3にオブジェクトをアップロードするなど)。
ただ、それをやるとなるとVue.jsのディレクティブやJavascript SDKにもう少し深入りする必要がありそう。あっちもこっちも手を出したくないので、今回は初志貫徹でコードはPython、SDKはboto3に留めておく。
ということで、上図の通りフロントエンドからはAPIを叩くだけにして、ロジックやSDKの使用はバックエンドで行う役割分担にする。
axiosについて
簡単に言うとHTTPクライアントで、Vue.jsのコード内からPostman的にREST APIを呼び出す方法がないかと探すうちに発見。
これなら簡単にAPIの呼び出しと結果の受領ができそうだ。
以下を参考にさせていただきました。感謝。
axios を利用した API の使用
axiosを乗りこなす機能についての知見集
Vue.js+axiosでDynamo DBにAjax通信する
[axios] axios の導入と簡単な使い方
Step by Step
1. Amplifyライブラリのインストール
やらないとは言いつつも、後学のために、セットアップして使えるようにするところまでは試しておく。
% npm install aws-amplify
% npm install aws-amplify-vue
2. src/main.jsへの取り込み
src/main.jsを編集して、Amplifyライブラリの取り込みを指定する。
// Amplify
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)
Vue.use(AmplifyPlugin, AmplifyModules)
aws-exportsが見つからない、というエラーが出る場合は、.gitignore
でGitのトラッキング対象外になってしまっていないかを確認する。
自分の環境では、これをコメントアウトすると動いた。
.gitignore
への追加はAmplify自身が行っているようなので若干謎だが、とりあえず動いたので、気にせず先に進む。
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#amplify
amplify/\#current-cloud-backend
amplify/.config/local-*
amplify/mock-data
amplify/backend/amplify-meta.json
amplify/backend/awscloudformation
build/
dist/
node_modules/
# aws-exports.js <-- ここ
awsconfiguration.json
amplifyconfiguration.json
amplify-build-config.json
amplify-gradle-config.json
amplifyxc.config
3. axiosのインストール
Amplifyライブラリと同じ手順。
% npm install axios
4. src/main.jsへの取り込み
ここもAmplifyライブラリと同様だが、axoisの仕様に若干のクセがありハマった。
axiosは厳密にはプラグインでないので、main.jsでVue.use()
に書いてあっても、this.axios
がUndefined
となってしまう。
代わりに以下のようにprototype.$axios
として定義し、読み込み元では$axios.get()
として呼ぶ必要がある。
// ダメな例
// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(AmplifyPlugin, AmplifyModules, VueAxios, axios)
// 動く例
// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(AmplifyPlugin, AmplifyModules, VueAxios)
Vue.prototype.$axios = axios
main.js
は最終的にこのようになる。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// Element UI
import './plugins/element.js'
// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'
// Amplify
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)
Vue.config.productionTip = false
Vue.use(AmplifyPlugin, AmplifyModules, VueAxios)
Vue.prototype.$axios = axios
new Vue({
router,
render: h => h(App)
}).$mount('#app')
これでようやくaxiosの事前準備が完了。
5. Lambda関数の準備
バックエンド側を作る。
そろそろAmplifyに全部やらせるのも飽きてきたので、練習も兼ねて、amplify add function
ではなくスクラッチで用意する。
こんな感じのLambda関数を書く。
import boto3
import json
import os
import datetime
print('Loading function')
glue = boto3.client('glue')
gluedb = os.environ['GLUEDBNAME']
# dict内のdatetime型データをJSON対応のISOフォーマット(文字列)に変換する
def convert_datetime2iso(object):
if isinstance(object, type(datetime)):
return object.isoformat()
# 本体
def lambda_handler(event, context):
operation = 'GET'
if operation == 'GET':
tables = glue.get_tables(
DatabaseName=gluedb
)["TableList"]
response = json.dumps(tables, default=convert_datetime2iso)
return {
'isBase64Encoded': False,
'statusCode': 200,
'headers': {
# CORSの許可
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': response
}
else:
response = ('Unsupported method:' + format(operation))
return response
ファイル名がlambda_function.py
、関数名がlambda_handler()
なので、Lambdaからはlambda_function.lambda_handler
として呼び出すことになる。
AWS Glueを呼び出し、DB名を渡してテーブル一覧を取得する簡単な関数で、GLUEDBNAME
をLambdaの環境変数として渡す形を取っている。今回はsh10_external
をDB名とした。よーく見る(見なくても)とメソッドがGETしか定義されてないが、お試しなのでご容赦。。
実際にGlueに渡す命令はglue.get_tables()のみで、特に複雑な処理はない。
6. APIの準備
次に、axiosから呼び出すREST APIを作成する。このAPIのバックエンドとして、先程のLambda関数を実行する形。
これもamplify add api
ではなくスクラッチでAPIを作成してみる。
マネジメントコンソールでAPI Gatewayの画面に移動し、以下の仕様でAPIを作成。
- Lambdaプロキシー統合
- /tablesリソース
- ANYメソッド
API作成まではすんなりいったものの、Lambda関数と統合して動かすまでに色々ハマった。
まず、not JSON seriarizable
エラー(クライアントから見ると500エラー)が出まくる。Lambda単体では動くようになっても、APIから呼ぶとまた出る。ここで大分時間を使った。
詳細はまた項を改めるが、結論としてはGlueからのdict型のレスポンスをjson.dump()
した上で、API GatewayがLambdaプロキシ統合で要求する形式に成形して返すことで、無事API様に受け取って貰えた。
ようやく関門突破かと思いきや、今度はCORS header 'Access-Control-Allow-Origin' missing'といったエラーが出る。何か見覚えある単語が。 これは、axiosというか今回のSPAが当該APIを別のオリジンから呼ぶ形になるので、CORSを許可する必要があるということだ。 CORSは一般にサーバー側で設定し、API Gatewayも
OPTIONS`メソッドでこれを設定するメニューがあったので設定してみたが、どうやら効いてなさそうだ。
色々調べたところ、今回使用したLambdaプロキシー統合の場合はどうもバックエンドのLambdaの方で明示的にCORSを許可するヘッダを記述してやる必要がある模様。
上記の返値の中ほど、'headers':{}
の中にCORS設定を書いてやると、ようやく動いた。やれやれ。
return {
'isBase64Encoded': False,
'statusCode': 200,
'headers': {
# CORSの許可
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': response
}
7. API呼び出しとテーブルへの取り込み設定
最後は再びフロントエンド側に戻り、axiosでAPIを呼び出してデータを取得する箇所と、取得したデータをElement UIのtablesの中に成形して取り込む受け皿部分を作る。
今回のSPAではsrc/components/
配下に全てのコンポーネントを配置してVue Routerからルーティングしているが、この一つとしてCatalogueTable.vue
というページを用意する。
ここでのポイントは以下の三つ。
- getDummyData()内の
this.$axios.get
でaxiosを呼び出し、REST APIを叩く -
tables
という配列でデータを受け取る - Element UIの
el-table
コンポーネントでtablesのデータ(:data="tables"
)を成形する
axiosの引数となるREST APIのURIには、先程API Gatewayで作成したAPIのエンドポイントを指定する。
成形は<el-table></el-table>
や<el-table-column></el-table-column>
の中で、Element UIの書式を使ってわりと自由に行うことができる。
ここでは最低限の属性だけを設定してみた。
- テーブル
属性 | 内容 |
---|---|
:data | データソース |
stripe | ストライプ表示にする |
style="width: 100%" | 横幅の長さ |
align="center" | 中央揃え |
- 列
属性 | 内容 |
---|---|
prop | 列名 |
label | 列の表示名 |
width | 列の長さ |
sortable | ソート可能な列として指定 |
<template>
<div class="cataloguetable">
<p></p>
<h2>テーブル一覧</h2>
<el-button type="primary" @click="getDummyData" :loading="false">実行</el-button>
<el-table
:data="tables"
stripe
style="width: 100%"
align="center">
<el-table-column
prop="Name"
label="Table"
width="250"
sortable>
</el-table-column>
<el-table-column
prop="DatabaseName"
label="Database"
width="200"
sortable>
</el-table-column>
<el-table-column
prop= "TableType"
label="Type"
width="200"
sortable>
</el-table-column>
<el-table-column
prop="PartitionKeys[0][Name]"
label="Partition Key 1"
width="150"
sortable>
</el-table-column>
<el-table-column
prop="PartitionKeys[1][Name]"
label="Partition Key 2"
width="150"
sortable>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
input: '',
tables: []
}
},
methods: {
getDummyData() {
let uri = 'https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/tables';
this.$axios.get(uri)
.then((response) => {
this.tables = response.data
})
.catch((e) => {
alert(e);
});
}
}
}
</script>
これを呼び出す側のApp.vue
はこんな感じで記述する。
(最終的には色々やりたいので、Element UIを使ったコンポーネントのガラだけはたくさん作ってあるが、CatalogueTable.vue
以外の中身はまだ空に近い)。
<template>
<div id="app">
<div>
<img alt="Vue Logo" src="@/assets/gluelogo.png/" width="60" height="60">
<h1>Data Catalogue Explorer</h1>
<el-menu :default-active="activeIndex" mode="horizontal" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" router>
<el-menu-item index="home" :route="{ name:'Home' }">ホーム</el-menu-item>
<el-menu-item index="dataset" :route="{ name:'Dataset' }">データセットの一覧を見る</el-menu-item>
<el-menu-item index="finder" :route="{ name:'Finder' }">データセットを探す</el-menu-item>
<el-menu-item index="adhoc" :route="{ name:'Adhoc' }">アドホック検索</el-menu-item>
<el-submenu index="catalogue">
<template slot="title">システムカタログ</template>
<el-menu-item index="catalogue-database" :route="{ name:'CatalogueDatabase' }">データベース</el-menu-item>
<el-menu-item index="catalogue-table" :route="{ name:'CatalogueTable' }">テーブル</el-menu-item>
<el-menu-item index="catalogue-crawler">クローラー</el-menu-item>
<el-menu-item index="catalogue-job">ジョブ</el-menu-item>
<el-menu-item index="catalogue-jdbc">JDBC接続</el-menu-item>
</el-submenu>
<el-menu-item index="api" :route="{ name:'API' }">API実行</el-menu-item>
<el-menu-item index="element" :route="{ name:'Element' }">Element UI</el-menu-item>
<el-menu-item index="about" :route="{ name:'About' }">Vue.js</el-menu-item>
<el-menu-item index="resources" :route="{ name:'Resources' }">リソース</el-menu-item>
</el-menu>
<router-view />
</div>
</div>
</template>
<script>
export default {
name: 'app',
data () {
return {
activeIndex: this.$route.name
}
}
}
</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>
Vue Routerにもこのコンポーネントへのルートを忘れずに追加してやる必要がある。
import Vue from 'vue'
import VueRouter from 'vue-router'
...(略)...
# ここと
import CatalogueTable from '@/components/CatalogueTable.vue'
...(略)...
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
...(略)...
# ここ
{
path: '/cataloguetable',
name: 'CatalogueTable',
component: CatalogueTable
},
...(略)...
最後にここまでの内容をコミットし、レポジトリにプッシュ(このままAmplifyの方で自動ビルドが回り、ホスティングされている内容が更新される。詳しくは前回の記事を参照)。
% git add .
% git commit -m "axios defined"
% git push -u origin master
9. 動作確認
Amplifyがデプロイを回してサイトの更新を完了するまで、待つこと数分。
ブラウザ、スマホでアクセスしてみると、どうやら無事動作している模様。
10. 落ち穂拾い
- Amplifyマネージドのビルドで、なぜか
console.log
が失敗する。- 構文チェックツールの
ESlint
設定が何かおかしいのかも、と疑って、このあたりを参考にあれこれ検証してみるも解決に至らず。
console.log
を出さなければいいだけの話なので、ここではいったん忘れることにした。いずれ解決したい。
- 構文チェックツールの
ポスト三個分の長丁場になってしまったが、ようやくタイトル通りの自習が完了。
追々、APIを追加したりUIをいじってみたりと、色々遊んでみたい。