Amazon API Gatewayで静的サイト公開 / Serverless Frameworkで外部ファイルを参照する

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パターンの使い方がある。

  • AWS連携:Lambda、SNS、S3、KinesisなどのAWSサービスと連携
  • HTTP連携:AWS内外のHTTPエンドポイントと連携
  • モック:静的コンテンツを生成

 

FAQを見ると、API GatewayAWS連携やHTTP連携が本来の使い方で、モックはあくまで「バックエンドの準備が整う前」などに利用する仮の実装という位置付けがされている。

また、Gateway では、マッピングテンプレートを指定して、返る静的コンテンツを生成することもできるため、バックエンドの準備が整う前に API をモックすることができます。

aws.amazon.com

 

静的コンテンツを生成するということは、静的サイトとして利用することもできる*3

 

Serverless Frameworkのドキュメントを参照すると、 モック統合の実装方法が以下のページに記載されている。

serverless.com

 

上記ドキュメントの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で静的サイトを公開することができた。

f:id:kidani_a:20200219030506p:plain

 

[Serverless Frameworkで外部ファイルを参照する]

先ほどの実装では、serverless.yml内に直接静的コンテンツのhtmlを記載した。しかし、html部分が通常はもっと巨大になることに加えて、他のAPIエンドポイントの実装を含めたserverless.yml全体の実装の見通しが悪くなることから、この方法は通常望ましい形ではない。

 

そこで、ここからはServerless Frameworkの変数機能を利用して、html部分を外部ファイルに切り出していく。

serverless.com

 

他のファイルに記載された変数を参照するには、${file(ファイルへの相対パス):変数名}という形で実装する。

ファイル内で変数を定義する場合は参照されるファイルの拡張子がjsonかymlでなくてはいけないとあるが、:変数名の部分を省略して${file(ファイルへの相対パス)}という形でファイル全体を参照する場合は、htmlファイルでも問題なく動作した。

 

serverless.yml
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)}
v4.html
<html>
<body>
  <h1>Sample Page v4</h1>
</body>
</html>

f:id:kidani_a:20200219032727p:plain

  

[画像やJS/CSSを利用する]

流石にh1要素だけでは味気なさすぎるので、CSSを追加する。serverless.ymlはほぼ同じなので省略する。

 

v6.html
<html>
<head>
  <style type="text/css">
    ${file(v6.css)}
  </style>
</head>
<body>
  <h1>Sample Page v6</h1>
</body>
</html>
v6.css
h1 {
  border-bottom: solid 4px black;
}

このバージョン6をデプロイするとCSSファイルのおかげで次のように下線が引かれる。

f:id:kidani_a:20200219033539p:plain

 

ただし、このままだとCSSファイルが肥大化した際に、レスポンス速度や描画速度が劣化する恐れがあるので、cssoライブラリを利用して、CSSを圧縮する実装を追加する。例によってserverless.ymlは省略する。

package.json
{
  "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"
  }
}
v8.html
<html>
<head>
  <style type="text/css">
    ${file(v8css.js)}
  </style>
</head>
<body>
  <h1>Sample Page v8</h1>
</body>
</html>
v8css.js
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;
};
v8.css
h1 {
  border-bottom: solid 4px blue;
}

デプロイ後の画面はこんな感じになる。

f:id:kidani_a:20200219034131p:plain

 

ここに、さらにJavaScriptを追加していく。圧縮にはuglify-jsを利用する。CSS部分はバージョン8と変わり映えしないので省略。

package.json
{
  "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"
  }
}
v9.html
<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>
v9js.js
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;
}
v9.js
var h1 = document.getElementsByTagName('h1')[0];
h1.textContent += '+'; 

このバージョン9をブラウザで開くと次のようになる。JavaScriptが動作して、h1要素のテキストに「+」が付加されているのが分かる。

f:id:kidani_a:20200219034647p:plain

 

最後に、画像を追加する。画像もHTMLと同じレスポンスに含めるために、Base64エンコードを利用する。CSSJavaScript部分は省略。

v10.html
<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>
v10img.js
var fs = require('fs');
var imgFile = fs.readFileSync('./ka.jpg');
module.exports.img = () => {
  return imgFile.toString('Base64');
}

f:id:kidani_a:20200219032956j:plain

ka.jpg

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

f:id:kidani_a:20200219035240p:plain

 

 

[まとめ]

  • Amazon API GatewayとServerless Frameworkを組み合わせて静的サイトを公開した
  • Serverless Frameworkはserverless.yml内で${file(ファイルパス)}を利用して外部ファイルを参照できる
  • JS/CSSファイルを圧縮して画像ファイルもBase64エンコードして静的サイトの表示に必要なリソースを1レスポンスにまとめた
  • サンプルコードは以下のリポジトリにある

github.com

*1:ただし、ドメインAPI Gatewayのものをそのまま露出している。

*2:2019年のre:Inventで発表されたHTTP APIは2020年2月現在まだベータ版なのでここでは触れない。

*3:料金的にはS3の静的ホスティングを利用した方が安上がりなハズだが、それはそれ、これはこれ。