Serverless Frameworkを利用して、Amazon API Gateway経由で静的サイトを公開してみた*1。
利用したServerless Frameworkのバージョンは以下の通り。
$ sls version Framework Core: 1.62.0 Plugin: 3.3.0 SDK: 2.3.0 Components Core: 1.1.2
[Amazon API Gatewayのモック統合]
API GatewayではREST APIとWebsocket APIを作成することができる*2。そのうちREST APIの使い方としては、次の3パターンの使い方がある。
FAQを見ると、API GatewayはAWS連携やHTTP連携が本来の使い方で、モックはあくまで「バックエンドの準備が整う前」などに利用する仮の実装という位置付けがされている。
また、Gateway では、マッピングテンプレートを指定して、返る静的コンテンツを生成することもできるため、バックエンドの準備が整う前に API をモックすることができます。
静的コンテンツを生成するということは、静的サイトとして利用することもできる*3。
Serverless Frameworkのドキュメントを参照すると、 モック統合の実装方法が以下のページに記載されている。
上記ドキュメントのserverless.ymlの記載例は以下の通り。helloというパスでGETリクエストを受けつけ、モックのリクエストとして{"statusCode": 200}を渡して、レスポンステンプレートにしたがってリクエストをそのまま返すようになっている。
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
cors: true
method: get
integration: mock
request:
template:
application/json: '{"statusCode": 200}'
response:
template: $input.path('$')
statusCodes:
201:
pattern: ''
serverless.ymlとして必要最低限の部分を付け足し、シンプルなhtmlを表示するように実装を修正すると以下のようになる。handlerとなるLambda関数は今回必要ないので、ダミーの文字列を指定してある。実際にdummy.pyなど関数本体の実装は不要である。
service: mock-res provider: name: aws runtime: python3.8 functions: v1: handler: dummy events: - http: path: /v1 method: get cors: true integration: mock request: template: application/json: '{"statusCode":200}' response: headers: Content-Type: "'text/html'" statusCodes: 200: pattern: '' template: | <html> <body> <h1>Sample Page v1</h1> </body> </html>
上記内容のserverless.ymlファイルの存在するディレクトリで、sls deployコマンドを実行すると、API GatewayのエンドポイントとなるURLが表示される。
$ sls deploy Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service mock-res.zip file to S3 (X.XX MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... .................................................................... Serverless: Stack update finished... Service Information service: mock-res stage: dev region: us-east-1 stack: mock-res-dev resources: 12 api keys: None endpoints: GET - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/v1 functions: v1: mock-res-dev-v1 layers: None Serverless: Removing old service artifacts from S3... Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
今回の例でいうとhttps://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/v1をブラウザ開くと(XXXXXXXXXX部分にはランダムな英数字が入る)、以下のような画面が表示される。何の変哲もないh1要素があるだけであるが、無事API Gatewayで静的サイトを公開することができた。

[Serverless Frameworkで外部ファイルを参照する]
先ほどの実装では、serverless.yml内に直接静的コンテンツのhtmlを記載した。しかし、html部分が通常はもっと巨大になることに加えて、他のAPIエンドポイントの実装を含めたserverless.yml全体の実装の見通しが悪くなることから、この方法は通常望ましい形ではない。
そこで、ここからはServerless Frameworkの変数機能を利用して、html部分を外部ファイルに切り出していく。
他のファイルに記載された変数を参照するには、${file(ファイルへの相対パス):変数名}という形で実装する。
ファイル内で変数を定義する場合は参照されるファイルの拡張子がjsonかymlでなくてはいけないとあるが、:変数名の部分を省略して${file(ファイルへの相対パス)}という形でファイル全体を参照する場合は、htmlファイルでも問題なく動作した。
functions:
v4:
handler: dummy
events:
- http:
path: /v4
method: get
cors: true
integration: mock
request:
template:
application/json: '{"statusCode":200}'
response:
headers:
Content-Type: "'text/html'"
statusCodes:
200:
pattern: ''
template: ${file(v4.html)}
<html> <body> <h1>Sample Page v4</h1> </body> </html>

[画像やJS/CSSを利用する]
流石にh1要素だけでは味気なさすぎるので、CSSを追加する。serverless.ymlはほぼ同じなので省略する。
<html> <head> <style type="text/css"> ${file(v6.css)} </style> </head> <body> <h1>Sample Page v6</h1> </body> </html>
h1 {
border-bottom: solid 4px black;
}
このバージョン6をデプロイするとCSSファイルのおかげで次のように下線が引かれる。

ただし、このままだとCSSファイルが肥大化した際に、レスポンス速度や描画速度が劣化する恐れがあるので、cssoライブラリを利用して、CSSを圧縮する実装を追加する。例によってserverless.ymlは省略する。
{
"name": "mock-res",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"csso": "^4.0.2"
}
}
<html> <head> <style type="text/css"> ${file(v8css.js)} </style> </head> <body> <h1>Sample Page v8</h1> </body> </html>
var csso = require('csso');
var fs = require('fs');
var cssFile = fs.readFileSync('./v8.css', 'utf8');
var minifiedCss = csso.minify(cssFile).css;
module.exports.css = () => {
return minifiedCss;
};
h1 {
border-bottom: solid 4px blue;
}
デプロイ後の画面はこんな感じになる。

ここに、さらにJavaScriptを追加していく。圧縮にはuglify-jsを利用する。CSS部分はバージョン8と変わり映えしないので省略。
{
"name": "mock-res",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"csso": "^4.0.2",
"uglify-js": "^3.7.7"
}
}
<html> <head> <style type="text/css"> ${file(v9css.js)} </style> </head> <body> <h1>Sample Page v9</h1> <script type="text/javascript"> ${file(v9js.js):js} </script> </body> </html>
var uglify = require('uglify-js');
var fs = require('fs');
var jsFile = fs.readFileSync('./v9.js', 'utf8');
var opt = {
mangle: {
toplevel: true,
},
};
var minifiedJs = uglify.minify(jsFile, opt).code;
module.exports.js = () => {
return minifiedJs;
}
var h1 = document.getElementsByTagName('h1')[0];
h1.textContent += '+';
このバージョン9をブラウザで開くと次のようになる。JavaScriptが動作して、h1要素のテキストに「+」が付加されているのが分かる。

最後に、画像を追加する。画像もHTMLと同じレスポンスに含めるために、Base64エンコードを利用する。CSS、JavaScript部分は省略。
<html> <head> <style type="text/css"> ${file(v10css.js)} </style> </head> <body> <h1>Sample Page v10</h1> <img src="data:image/png;base64,${file(v10img.js):img}" /> <script type="text/javascript"> ${file(v10js.js):js} </script> </body> </html>
var fs = require('fs');
var imgFile = fs.readFileSync('./ka.jpg');
module.exports.img = () => {
return imgFile.toString('Base64');
}

バージョン10をブラウザで開くと次のよう画像が表示されている。
