【AWS】ALBのみでメンテナンスページを表示させる by CloudFormation

#1. はじめに
Lightsailで個人ブログをホスティングしているのですが、メンテナンス時はWordPressの標準機能でメンテナンスページを表示させていました。
今後、LightsailをALBと連携させたいと考えています。
その際、ALB側でメンテナンスページをコントロールできた方がページコントロールの観点から統一性があって良いかなと思い、ALBのリスナールールでメンテナンスページを表示させるようにしようと思いました。

メンテナンスページを表示させる方法は色々ありますが、(CloudFront+S3, 単純にS3から引っ張るetc..)ALBのリスナールールで表示させる方が一番簡単な気がするので、その方法を採用しました。

本記事では単純にALBのリスナールールでメンテナンスページを表示させる方法を公開したいと思います。

#2. 概要
ALBのリスナールールを利用し、静的なメンテナンスページを作成します。
また、リソース作成にはCloudFormationを使用します。

#3. ALBのみで実装するメンテナンスページに関して
##3.1 メリット
- 運用が楽<br>
メンテナンスページを表示させたい時は、単純にリスナールールの順位を変更すればいいだけ
- 実装が簡単<br>
ALBに新規リスナールールを追加すればいいだけ

##3.2 デメリット
- html/cssの最大文字数が1024文字<br>
1024文字以内だとかなり簡易的なページしか作成できない・・(本番利用には向いていない)

#4. メンテナンスページの作成
基本的なVPC環境やセキュリティグループの設定に関しては、既に構築済である前提とします。

### CloudFormationテンプレート
```yaml:alb.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: "Launch a SorryPage to ALB"


# ------------------------------------------------------------
#  Metadate
# ------------------------------------------------------------
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Parameters:
          - PJPrefix
          - ALBSubnetIDs
          - ACM
          - VPCID
          
          
# ------------------------------------------------------------
#  Input Parameters
# ------------------------------------------------------------
Parameters:
### Share Param ###
  PJPrefix:
    Type: "String"
  ALBSubnetIDs:
    Type: "List<AWS::EC2::Subnet::Id>"
    Description: "Select a subnet from two or more availability-zones."
  VPCID:
    Type: "AWS::EC2::VPC::Id"
    Description: "Select at your VPC."
  ACM:
    Type: "String"
    Description: "Select at your ACM ARN."


### Resources ###
Resources: 
# ------------------------------------------------------------
#  TargetGroup
# ------------------------------------------------------------
  TargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: !Ref VPCID
      Name: !Sub "${PJPrefix}-tg"
      Protocol: HTTP
      Port: 80
      HealthCheckProtocol: HTTP
      HealthCheckPath: "/"
      HealthCheckPort: "traffic-port"
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      HealthCheckTimeoutSeconds: 5
      HealthCheckIntervalSeconds: 10
      Matcher: 
        HttpCode: 200
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-tg"
      TargetGroupAttributes: 
        - Key: "deregistration_delay.timeout_seconds"
          Value: 300
        - Key: "stickiness.enabled"
          Value: false
        - Key: "stickiness.type"
          Value: lb_cookie
        - Key: "stickiness.lb_cookie.duration_seconds"
          Value: 86400
          
          
# ------------------------------------------------------------
#  ALB
# ------------------------------------------------------------
### ALB ###
  ALB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name: !Sub "${PJPrefix}-ALB"
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-ALB"
      Scheme: "internet-facing"
      LoadBalancerAttributes:
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 60
        - Key: "access_logs.s3.enabled"
          Value: false
      SecurityGroups:
        - !Ref "ALBSG"
      Subnets: !Ref "ALBSubnetIDs"
        
        
### HTTP通信のリスナー設定 ### 
  ALBListenerHTTP:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      # HTTP通信でのアクセスはHTTPSへリダイレクトさせる
      DefaultActions:
        - Type: "redirect"
          RedirectConfig:
            Protocol: "HTTPS"
            Port: "443"
            Host: "#{host}"
            Path: "/#{path}"
            Query: "#{query}"
            StatusCode: "HTTP_301"
      LoadBalancerArn: !Ref "ALB"
      Port: "80"
      Protocol: "HTTP"
      
      
### HTTPS通信のリスナー設定 ###
  ALBListenerHTTPS:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      Port: 443
      Protocol: HTTPS
      DefaultActions: 
         - Type: "forward"
           TargetGroupArn: !Ref TargetGroup
      Certificates:
        - CertificateArn: !Ref ACM
      LoadBalancerArn: !Ref ALB
      
      
### 通常パターンのリスナールール ###
  ALBListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      Conditions:
        - Field: path-pattern
          Values:
            - '*'
      ListenerArn: !Ref ALBListenerHTTPS
      Priority: 1
      
### メンテナンスモードのリスナールール ###
  ALBListenerRule503:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - Type: "fixed-response"
          FixedResponseConfig:
            ContentType: 'text/html'
            MessageBody: |
                <!DOCTYPE html>
                <html lang="ja">
                <head>
                <meta charset="UTF-8">
                <title>メンテナンスのお知らせ</title>
                <style>
                  html {
                    text-align: center;
                  }
                  body {
                    margin: 0;
                    padding: 0;
                    background: #eee;
                  }
                  #conainer {
                    margin: 0 auto;
                    padding: 20px;
                    width: 600px;
                    max-width: 600px;
                  }
                  h1 {
                    font-size: 2em;
                  }
                  .warn {
                    padding: 1em;
                    background: #ff7;
                  }
                  #footer {
                    text-align: center;
                  }
                </style>
                </head>
                
                <body>
                  <div id="container">
                    <h1>ただいまメンテナンス中です</h1>
                    <p>
                      システムアップデートのためサービスを停止しています。<br>
                      ユーザーの皆様にはご不便をおかけしますが、メンテナンス終了まで今しばらくお待ち下さい。<br>
                    </p>
                    <h2>メンテナンス期間</h2>
                    <p class="warn">
                      <strong>202x年x月x日（x） xx:xx 〜 xx月xx日（x） xx:xx</strong><br>
                    </p>
                    <h2>
                     お問い合わせ
                    </h2>
                    <p>
                      メンテナンスに関するお問い合わせは<a href="xxxxxxxx@gmail.com">xxxxxxxx@gmail.com</a>までお願いします。
                    </p>
                    <hr>
                    <div id="footer">
                      &copy; 2021~2022 
                    </div>
                  </div>
                </body>
                </html>
            StatusCode: 503
      Conditions:
        - Field: path-pattern
          Values:
            - '*'
      ListenerArn: !Ref ALBListenerHTTPS
      Priority: 2
```

#5. 動作確認
とりあえずメンテナンスページを表示させたい。
しかし、作成時のリスナールールの順位だと、443の方にアクセスが流れてしまうため、リスナールールの順位を変更する必要があります。

作成したリスナールールの優先順位を入れ替えましょう。
参照：https://blog.serverworks.co.jp/alb-fixed-response

入れ替え後、ALBのDNS名へアクセスし、無事以下のような画面が表示されれば動作確認完了。
※ 1024文字だとこのレベルが限界(汗)
![Sorrypage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570694/febcfb7f-5586-ed63-f163-e602d094b371.png)


#6. まとめ
とても簡易的で便利な方法ですが、やはり1024文字制限が痛すぎる・・
個人利用レベルならともかく、現場で本番を想定したような場面には向かないでしょう。。