【ALB】さくっと5秒でメンテナンス画面を出す仕組みを作った

# 解決したい課題

* 複数の用途の WEB サーバ (EC2) が ELB (ALB) 配下に存在しているが、順番にメンテナンス画面を出すのに時間がかかってしまう (Jenkins などで順番にデプロイ)
    * 複数の環境のソースコード上にあるメンテナンス時間を修正して、都度デプロイしないといけない環境など
* ALB に存在する リクエストルーティング を使ってメンテナンス画面を出す方法もあるが、1024 文字の制限があるので、最小限の HTML かテキストの出力しかできない

# 解決案

* メンテナンス用の S3 + CloudFront + route53 (ここでは maintenance.hapitas.jp と定義) をたてる
    * CloudFront は `GET, HEAD, OPTIONS` 以上を許可
    * CloudFront は `Cache Based on Selected Request Headers` を `Whitelist` とし、`Access-Control-Request-Headers` `Access-Control-Request-Method` `Origin` を Whitelist Headers として登録 (同一ドメイン、スキームでアクセスするなら不要)
    * S3 上では CORS 制限をはずす
        * バケットの [Permissions] 設定の [CORS configuration] で下記の内容を登録 (同一ドメイン、スキームでアクセスするなら不要)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```

* S3 バケットに `maintenance.json` ファイルを以下の内容で配置

```json

{
  "start_date": "Fri, 13 Dec 2019 21:00:00 +0900",
  "end_date": "Fri, 13 Dec 2019 22:00:00 +0900"
}
```

* S3 バケットにこだわったデザインの HTML (mainte.html) を配置 (適当ですがｗ)

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">
    <title>こだわったメンテ画面</title>
</head>
<body>
<div>
    <h1>ただいまメンテナンス中です</h1>
    <div id="mainte-period"></div>
    <p>ご不便をおかけしてしまい申し訳ございませんが、<br>
        メンテナンス完了までしばらくお待ちください。</p>
</div>
<script id="template" type="text/x-jquery-tmpl">
<div id="mainte-period">
    <p>実施期間： ${start} ～ ${end}<br>
    期間中はすべてのサービスがご利用いただけません。</p>
</div>
</script>
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<script src="https://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>
<script>
"use strict";
$(function(){
    var maintenance = {
        template: $('#template'),
        targetHtml: $('#mainte-period'),
        init: function() {
            this.getJson().done(function(data) {
                if (typeof data.start_date == 'string' || typeof data.end_date == 'string') {
                    if (maintenance.isMainteNow(data.start_date, data.end_date)) {
                        maintenance.targetHtml.replaceWith(
                            maintenance.template.tmpl({
                                "start": maintenance.generateFormat(data.start_date),
                                "end": maintenance.generateFormat(data.end_date)
                            })
                        );
                    }
                }
            });
        },
        isMainteNow: function(start, end) {
            if (!start || !end) {
                return false;
            }
            var now = new Date();
            var start = new Date(start);
            var end = new Date(end);
            return start.getTime() <= now.getTime() && now.getTime() <= end.getTime();
        },
        generateFormat: function(string) {
            var dateObject = new Date(string);
            var year = dateObject.getFullYear();
            var month = dateObject.getMonth() + 1;
            var date = dateObject.getDate();
            var hours = dateObject.getHours();
            var minutes = dateObject.getMinutes();
            var week = [ "日", "月", "火", "水", "木", "金", "土" ][dateObject.getDay()];
            return year + '年' + month + '月' + date + '日 ' + ' (' + week + ') ' + ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2);
        },
        getJson: function() {
            var deferred = $.Deferred();
            var date = new Date();
            $.ajax({
                type: 'GET',
                url: 'https://maintenance.hapitas.jp/maintenance.json?' + date.getTime(),
                dataType: 'json',
                success: deferred.resolve
            });
            return deferred.promise();
        }
    };
    maintenance.init();
});
</script>
</body>
</html>
```

* ALB の リクエストルーティングの優先度は default action の手前の優先度で ルールを追加 (※ default action より優先度は下げられないため、手前に配置する。ただし、非メンテ時は default action に重要なアクションがあると困る → derault action と同じ内容のルールを Source IP 0.0.0.0/0 でメンテナンスのルールより手前に追加しておく)
    * IF に Source IP を 0.0.0.0/0 とする
    * THEN に Return fixed response をいれ Response code: 503, Content-Type: test/html, Response body に以下をいれる (なんと 301 文字)

```html

<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8"></head><body><script onload="$.ajax({type: 'GET',url: 'https://maintenance.hapitas.jp/mainte.html',dataType: 'html',success: function(data) {document.write(data);}});" src="https://code.jquery.com/jquery-latest.min.js"></script></body></html>
```

これで準備は完了！
メンテ時は json のメンテ開始日、終了日をいれ、上記レスポンスの優先度を一番上に上げるだけ
緊急メンテで、開始日、終了日が決まっていない場合は、 json の更新はせず、優先度を上げればOK