- 追加された行はこの色です。
- 削除された行はこの色です。
#author("2019-10-17T01:18:27+00:00","","")
#author("2019-10-22T05:38:17+00:00","","")
#mynavi(AWSメモ)
#setlinebreak(on);
* 目次 [#z4fb8e28]
#contents
- 関連
-- [[AWSメモ]]
-- [[1つのAWS Lambda関数でSPAフレームワーク作成]]
-- [[1つのAWS LambdaでSPA(CloudFront編)]]
-- [[API Gateway&Lambda で画像データを返却する]]
-- [[Vue.js]]
* 概要 [#y70c87af]
#html(<div style="padding-left: 10px;">)
[[1つのAWS Lambda関数でSPAフレームワーク作成]] の別バージョン。
違いは、Lambda の言語が Python、クライアント側が Vue.js である事と、Cloudformationテンプレート化した事ぐらい。
小さなアプリケーションであれば、恐らくこれで十分だと思う。
[補足]
- css や js をLambdaに内包しておいて、リクエストURLに応じてファイルを返却する。
- /api/xxx へのリクエストは対象の api の処理を読み込んで実行する。
- 画像も返却できるように API Gateway の BinaryMediaTypes を指定。
- サブディレクトリ配下で公開する場合、Vue.js のルート設定は相対PATHで指定しておいた方が良い。
(API Gateway をベースパスマッピングなしで使用する場合、ステージ名がURLに入る為、この考慮は必須)
(Vue Router のヒストリモードはOFFにしておく。)
#html(</div>)
* フォルダ構成 [#ce245159]
#html(<div style="margin-left: 10px; padding: 5px; border: 1px solid #333;">)
#html(<div style="display: inline-block">)
/aws_build.sh
/client-project
/.env.development
/.env.production
/dist
/node_modules
/package-lock.json
/package.json
/public
/src
/App.vue
/EventHub.js
/assets
/components
/Footer.vue
/Header.vue
/PageSample1.vue
/PageSample2.vue
/PageTop.vue
/main.js
/src
/api
sample1.py
sample2.py
/index.py
/libs
/myenv.py
/client
/css
/xxxxx.css
/js
/app.xxxxxxxx.js
/app.xxxxxxxx.js.map
/index.html
/template.yml
#html(</div>)
#html(<div style="display: inline-block; padding-left: 20px; vertical-align: top;">)
・・・ ビルド用シェル
・・・ Vue.jsプロジェクト
・・・ 環境変数(Vue.jsローカル動作確認用)
・・・ 環境変数(本番用)
・・・ Lambda用ソース
・・・ この配下にVue.js ビルド後のファイルをコピー ([[後述>#v3eb8199]])
・・・ Cloudformation テンプレート
#html(</div>)
#html(</div>)
* サーバ側 [#t9f8a647]
#html(<div style="padding-left: 10px;">)
#html(</div>)
** Cloudformationテンプレート [#b02b96c2]
#html(<div style="padding-left: 10px;">)
#mycode2(){{
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: "1LambdaでSPAのサンプル"
Globals:
Api:
Cors:
AllowMethods: "'PUT,GET,POST,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type'"
AllowOrigin: "'*'"
Resources:
ImageSupportSpaApi:
Type: AWS::Serverless::Api
Properties:
Name: ImageSupportSpaApi
StageName: Prod
BinaryMediaTypes:
- "image/*"
ImageSupportSpaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: ImageSupportSpaFunction
CodeUri: ./src
Handler: index.handler
Runtime: python3.7
Timeout: 60
MemorySize: 128
Role: !GetAtt ImageSupportSpaFunctionRole.Arn
Events:
Root:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /
Method: get
Favicon:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /favicon.ico
Method: get
StaticCss:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /css/{filename}
Method: get
StaticJs:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /js/{filename}
Method: get
StaticImage:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /img/{filename}
Method: get
ListActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}
Method: get
GetActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}/{id}
Method: get
PostActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}
Method: post
PutActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}/{id}
Method: put
DeleteActions:
Type: Api
Properties:
RestApiId: !Ref ImageSupportSpaApi
Path: /api/{resource}/{id}
Method: delete
ImageSupportSpaFunctionRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: ImageSupportSpaFunctionRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service: "lambda.amazonaws.com"
Action: "sts:AssumeRole"
Policies:
- PolicyName: "ImageSupportSpaFunctionPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: # 必要に応じて権限を付与
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- s3:GetObject
Resource: "*"
Outputs:
ImageSupportSpaApiUrl:
Value: !Sub "https://${ImageSupportSpaApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
Export:
Name: ImageSupportSpaApiUrl
}}
#html(</div>)
** src/myenv.py [#oe07cc72]
#html(<div style="padding-left: 10px;">)
#mycode2(){{
"""共通処理(PATHのセットなど)."""
import sys
import os
def add_path(path):
"""PATH追加."""
sys.path.append(os.path.abspath(os.path.dirname(__file__)) + path)
def is_local(event):
"""ローカルでの動作確認用かどうかの判断."""
return event["requestContext"]["identity"]["sourceIp"] == "127.0.0.1"
}}
#html(</div>)
** src/index.py [#d930bf49]
#html(<div style="padding-left: 10px;">)
#TODO(認証機能)
#mycode2(){{
"""メイン処理."""
import base64
import importlib
import json
import os
import re
import myenv
# PATH追加
myenv.add_path("/libs")
def handler(event, context):
"""メインハンドラ."""
print(json.dumps(event, indent=4))
status_code = 200
response_body = ""
headers = {"Content-Type": "text/html"}
if myenv.is_local(event):
headers["Access-Control-Allow-Origin"] = "*"
headers["Access-Control-Allow-Methods"] = "PUT,POST,DELETE,OPTIONS"
is_base64_encoded = False
if event["path"] == "/":
with open("client/index.html") as f:
response_body = f.read()
elif event["path"] == "/favicon.ico":
file_path = "client/favicon.ico"
if os.path.exists(file_path):
headers["Content-Type"] = "image/x-icon"
with open(file_path, "rb") as f:
response_body = base64.b64encode(f.read()).decode("utf-8")
is_base64_encoded = True
elif re.match(r"^/(css|js)/.+", event["path"]):
file_path = "client/" + event["path"][1:]
if re.match(r"^/css/.+", event["path"]):
headers["Content-Type"] = "text/css"
else:
headers["Content-Type"] = "application/javascript"
if os.path.exists(file_path):
with open(file_path) as f:
response_body = f.read()
elif re.match(r"^/img/.+", event["path"]):
file_path = "client/" + event["path"][1:]
headers["Content-Type"] = "image/png"
if os.path.exists(file_path):
with open(file_path, "rb") as f:
response_body = base64.b64encode(f.read()).decode("utf-8")
is_base64_encoded = True
elif re.match(r"^/api/.+", event["path"]):
headers["Content-Type"] = "application/json"
resource = event["pathParameters"]["resource"]
# print("api call " + event["path"])
# print("./api/" + resource + ".py")
if os.path.exists("./api/" + resource + ".py"):
m = importlib.import_module("api." + resource)
result = m.handler(event, context)
if isinstance(result, dict):
if "statusCode" in result:
status_code = result["statusCode"]
if "headers" in result:
headers = {**headers, **result["headers"]}
if "body" in result:
response_body = result["body"]
if "isBase64Encoded" in result:
is_base64_encoded = result["isBase64Encoded"]
else:
status_code = 500
response_body = {"message": "Invalid API Response Format!"}
if response_body == "":
status_code = 501
response_body = {"message": "API Not Found!"}
else:
status_code = 501
response_body = {"message": "Not Implemented!"}
if isinstance(response_body, dict):
response_body = json.dumps(response_body)
return {
"statusCode": status_code,
"headers": headers,
"isBase64Encoded": is_base64_encoded,
"body": response_body
}
}}
#html(</div>)
** src/api/sample1.py [#za201d5e]
#html(<div style="padding-left: 10px;">)
#mycode2(){{
"""サンプル1."""
import json
def handler(event, context):
"""メインハンドラ."""
request_body = {}
if event["body"] and isinstance(event["body"], str):
request_body = json.loads(event["body"])
elif event["body"] and isinstance(event["body"], dict):
request_body = event["body"]
return {
"body": json.dumps(
{
"message": "This is sample1 api message!",
"httpMethod": event["httpMethod"],
"path": event["path"],
"resource": event["resource"],
"pathParameters": event["pathParameters"],
"queryStringParameters": event["queryStringParameters"],
"request_body": request_body,
}
)
}
}}
#html(</div>)
** src/api/sample2.py [#h3f8846f]
#html(<div style="padding-left: 10px;">)
#mycode2(){{
"""サンプル2."""
import json
def handler(event, context):
"""メインハンドラ."""
response_body = {
"items": [{"name": "item1", "price": 1000},
{"name": "item2", "price": 2000},
{"name": "item3", "price": 3000},
{"name": "item4", "price": 4000}]
}
return {
"body": json.dumps(response_body)
}
}}
#html(</div>)
** src/client/* [#sd6ecad8]
#html(<div style="padding-left: 10px;">)
vue.js のビルド済みファイルをこの配下にコピーする。(後述)
#html(</div>)
* クライアント周りの作成 [#p47adea4]
#html(<div style="padding-left: 10px;">)
** プロジェクトの作成 [#r959dd7a]
#myterm2(){{
vue create client-project
}}
** 設定ファイル [#o9bf2529]
** client-project/.env.development [#a024c309]
#mycode2(){{
NODE_ENV="development"
VUE_APP_API_ROOT="http://127.0.0.1:3000/api/"
}}
※Vue.js をローカル起動している場合は、SAMローカルで動作中のLambdaにアクセスさせる。
** client-project/.env.production [#kb182de2]
#mycode2(){{
NODE_ENV='production'
VUE_APP_API_ROOT='./api/'
}}
※ API Gateway & Lambda のデプロイ後は、ルート直下の api でアクセス。
※ ただし、API Gateweay の Stage 名がURLの頭に付加されるケースを想定し、相対PATHで記述。( &color(red){Vue Router を使用する場合は historyモード はOFFにする事}; )
** client-project/vue.config.js [#o086254c]
#mycode2(){{
module.exports = {
publicPath: './'
}
}}
※ API Gateweay の Stage 名がURLの頭に付加されるケースを想定し、相対PATHで記述。
** client-project/src/main.js [#h7b83b75]
#mycode2(){{
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
}}
** client-project/src/main.js [#qa90542d]
#mycode2(){{
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
}}
** client-project/src/App.vue [#i111bad6]
#mycode2(){{
<template>
<div id="app">
<Header />
<component v-bind:is="currentView"></component>
<Footer />
</div>
</template>
<script>
import EventHub from './EventHub.js'
import Header from './components/Header.vue'
import PageTop from './components/PageTop.vue'
import PageSample1 from './components/PageSample1.vue'
import PageSample2 from './components/PageSample2.vue'
import Footer from './components/Footer.vue'
export default {
name: 'app',
components: {
Header,
Footer,
PageTop,
PageSample1,
PageSample2,
},
data: function(){
return {
currentView: "PageTop"
}
},
methods: {
switchView(viewName) {
this.currentView = viewName;
}
},
created: function(){
EventHub.$on('switchView', this.switchView)
}
}
</script>
<style>
html, body {
margin: 0;
}
h1 {
margin: 0;
font-size: 120%;
}
#header {
padding: 2px 0;
}
#main {
padding: 5px 10px;
}
#footer {
position: fixed;
padding: 5px;
bottom: 0;
width: 100%;
background: #eee;
}
</style>
}}
** client-project/src/EventHub [#e9c2b819]
#mycode2(){{
import Vue from 'vue'
const EventHub = new Vue()
export default EventHub
}}
** client-project/src/Header.vue [#vd8484dd]
#mycode2(){{
<template>
<div id="header">
<h1>Header Content</h1>
<ul id="header-menu">
<li v-on:click="menuClicked('PageTop')">TOP</li>
<li v-on:click="menuClicked('PageSample1')">サンプル1</li>
<li v-on:click="menuClicked('PageSample2')">サンプル2</li>
</ul>
</div>
</template>
<script>
import EventHub from '../EventHub.js'
export default {
name: 'Header',
methods: {
"menuClicked" : function(page){
EventHub.$emit('switchView', page)
},
}
}
</script>
<style scoped>
#header h1 {
padding: 0 5px;
}
#header-menu {
list-style-type: none;
margin: 0;
padding: 0;
background: #333;
color: #fff;
}
#header-menu.ul,#header-menu li {
display: inline-block;
}
#header-menu li {
padding: 10px;
border-right: 1px dotted #fff;
cursor: pointer;
}
#header-menu li:hover {
opacity: 0.8;
text-decoration: underline;
}
</style>
}}
** client-project/src/Footer.vue [#o791a366]
#mycode2(){{
<template>
<div id="footer">
<div>Footer Content</div>
</div>
</template>
<script>
export default {
name: 'Footer',
}
</script>
}}
** client-project/src/PageTop.vue [#b93e3001]
#mycode2(){{
<template>
<div id="main">
<div>
Top Content<br />
<button v-on:click="listAction()">get</button>
<button v-on:click="postAction()">post</button>
<button v-on:click="putAction(1)">put</button>
<button v-on:click="deleteAction(1)">delete</button>
<hr/>
<img src="img/sample.png" width="500"/>
<hr/>
<pre>{{ result }}</pre>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'PageTop',
data: function(){
return {
msg: process.env.VUE_APP_API_ROOT,
result: ""
}
},
created: function(){
},
methods: {
listAction: function(){
this.result = "loading.."
axios
.get(process.env.VUE_APP_API_ROOT + "sample1" + "?param1=123")
.then(response => {
console.log(response)
this.result = response.data
})
},
postAction: function(){
this.result = "loading.."
axios
.post(process.env.VUE_APP_API_ROOT + "sample1", {"var1": "abc"})
.then(response => {
console.log(response)
this.result = response.data
})
},
putAction: function(){
this.result = "loading.."
axios
.put(process.env.VUE_APP_API_ROOT + "sample1/1", {"var2": "def"})
.then(response => {
console.log(response)
this.result = response.data
})
},
deleteAction: function(){
this.result = "loading.."
axios
.delete(process.env.VUE_APP_API_ROOT + "sample1/1")
.then(response => {
console.log(response)
this.result = response.data
})
},
}
}
</script>
}}
** client-project/src/PageSample1.vue [#d2b54f02]
#mycode2(){{
<template>
<div id="main">
<div>
サンプル1<br />
<hr/>
<button v-on:click="callApi()">API呼び出し</button>
<div style="margin: 20px;" v-if="status != ''">{{ status }}</div>
<div style="margin:20px;">{{ message }}</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Sample1',
data: function(){
return {
status: "",
message: "",
}
},
methods: {
callApi: function(){
this.status = "loading.."
this.message = ""
let url = process.env.VUE_APP_API_ROOT + "sample1"
let self = this
axios
.get(url)
.then(response => {
console.log(response)
self.status = ""
self.message = response.data["message"]
}).catch(error => {
console.log(error)
self.status = "api call error!"
})
},
}
}
</script>
}}
** client-project/src/PageSample2.vue [#sa5a98f2]
#mycode2(){{
<template>
<div id="main">
<div>
サンプル2<br />
<hr/>
<button v-on:click="callApi()">API呼び出し</button>
<div style="margin: 20px;" v-if="status != ''">{{ status }}</div>
<table id="sample2_table" v-if="items.length > 0">
<thead>
<tr>
<th>Name</th><th>Price</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items">
<td>{{ item.name }}</td><td>{{ item.price }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Sample2',
data: function(){
return {
status: "",
items: [],
}
},
methods: {
callApi: function(){
this.status = "loading.."
this.items = []
let url = process.env.VUE_APP_API_ROOT + "sample2"
let self = this
axios
.get(url)
.then(response => {
console.log(response)
self.status = ""
self.items = response.data["items"]
}).catch(function (error) {
self.status = "api call error!"
})
},
}
}
</script>
<style>
#sample2_table {
margin-top: 20px;
border-collapse: collapse;
}
#sample2_table th {
background: #ccc;
color: #333;
}
#sample2_table th,
#sample2_table td{
padding: 5px;
border: 1px solid #333;
}
#sample2_table tbody tr:nth-child(even) td{
background: #efe;
}
</style>
}}
#html(</div>)
* 動作確認 [#od9c1e95]
#html(<div style="padding-left: 10px;">)
** SAMローカル起動 [#ua42f476]
#myterm2(){{
sam local start-api --template template.yml
}}
** Vue.js をローカル起動 [#p9deaab9]
#myterm2(){{
cd client-project
npm run serve
}}
http://localhost:8080/ からアクセス確認
#html(</div>)
* デプロイ [#v3eb8199]
#html(<div style="padding-left: 10px;">)
Vue.js ビルド
#myterm2(){{
# ビルド
cd client-project
npm run build
# ビルド済みファイルを Lambda 用の src 配下にコピー
rm -rf ../src/client && cp -R dist ../src/client
}}
Lambdaデプロイ
#myterm2(){{
./awsbuild.sh deploy
}}
※ awsbuild.sh の内容は [[CloudFormation実行用のシェル]] を参照。
#html(</div>)
#html(<div style="height:400px;"></div>)