TerraformでAWS WAFを使ったAPI側のメンテナンスモードを実装した
はじめに
WebサービスのAPI側のメンテナンスモードをTerraformで実装しましたのでその方法をまとめました。
今回のアーキテクチャ構成は、上の図のようにフロントエンドはSPAをAmplifyでホスティング、バックエンドはAPI Gatewayがフロントへのコンテンツ提供を集約しています。
バックエンドのAPIがフロントのクライアントアプリに503レスポンスを返すことで、クライアントアプリはメンテナンスページを表示する実装にする予定ですが、今回はそのバックエンド部分のみの実装になります。
Amazon API GatewayのREST APIではAWS WAFを使えるので、メンテナンスモードのときだけ、AWS WAFが503を返すようにTerraformで実装します。
Terraform
ディレクトリ構造
以下のようなファイル構成にしました。apigatewayモジュールにAPI GatewayとWAFの定義を書いていきます。
.
├── environments
│ └── dev
│ ├── main.tf
│ ├── terraform.tf
│ └── variables.tf
└── modules
└── apigateway
├── main.tf
└── variables.tf
API Gateway
resource "aws_api_gateway_rest_api" "main" {
...
endpoint_configuration {
types = ["REGIONAL"]
}
}
ALBやCloudFrontにWAFをつける場合もありますが、今回はAPI Gateway(REST API)になります。
※HTTP APIにはまだWAFはつけられないようです。
WAF IP Set
# ******************************
# WAF IP Set
# ******************************
resource "aws_wafv2_ip_set" "developer" {
name = "developer"
description = "Developer IP set"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = []
lifecycle {
ignore_changes = [addresses]
}
}
これは、メンテナンスモード中も開発者だけは503にならないように、開発者のIPのホワイトリストを登録するための箱です。 address
は空にしたままで、 ignore_changes
にしている理由はIPアドレスだけはTerraformの管理から除外するためです。あとでAWSコンソールから手動で登録するようにしています。
WAF Rule Group
# ******************************
# WAF Rule Group
# ******************************
resource "aws_wafv2_rule_group" "maintenance" {
name = "rulegrp-maintenance"
scope = "REGIONAL"
capacity = 1
custom_response_body {
key = "maintenance"
content_type = "APPLICATION_JSON"
content = jsonencode(
{
status = "maintenance",
message = "サービスは現在メンテナンス中です。お手数をおかけしますが、しばらくお待ちください。"
}
)
}
rule {
name = "block-non-developers"
priority = 0
action {
block {
custom_response {
response_code = 503
custom_response_body_key = "maintenance"
}
}
}
statement {
not_statement {
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.developer.arn
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "BlockNonDevelopers"
sampled_requests_enabled = false
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "maintenance"
sampled_requests_enabled = false
}
}
このルールグループは、平常時はWAFにはアタッチされないルールグループです。 not_statement
を使って開発者のIPではなかった場合503になるようにしています。レスポンス内容もカスタマイズしており、bodyはJSONで以下が返るようにしています。
{
"message": "サービスは現在メンテナンス中です。お手数をおかけしますが、しばらくお待ちください。",
"status": "maintenance"
}
WAF ACL
# ******************************
# WAF ACL
# ******************************
resource "aws_wafv2_web_acl" "main" {
name = "acl-rest-api"
description = "Apply OWASP rules to API Gateway."
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 0
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "OWASP"
sampled_requests_enabled = false
}
}
dynamic "rule" {
for_each = var.is_maintenance ? [1] : []
content {
name = "maintenance"
priority = 1
override_action {
none {}
}
statement {
rule_group_reference_statement {
arn = aws_wafv2_rule_group.maintenance.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "maintenance-rule"
sampled_requests_enabled = false
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "waf-for-apigateway"
sampled_requests_enabled = false
}
}
resource "aws_wafv2_web_acl_association" "main" {
resource_arn = aws_api_gateway_stage.main.arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}
variable "is_maintenance" {
type = bool
default = false
}
これらの定義では普段はAWSのコアルールセットで一般的なリスクの高い脆弱性に対応しますが、変数 is_maintenance
が true
だった場合は、追加でメンテナンス用のルールグループがアタッチされるように書いています。dynamicとfor_eachを使うことで実現しています。
エントリーポイント
variable "apigateway_is_maintenance" {
type = bool
default = false
}
...(略)
module "apigateway" {
source = "../../modules/apigateway"
is_maintenance = var.apigateway_is_maintenance
}
エントリーポイントで apigateway_is_maintenance
変数を指定することで、apigatewayモジュールの is_maintenance
フラグを上書きできるようにしてあるので、
Terraform CloudのWorkspace Variablesで、以下のように apigateway_is_maintenance
変数をtrueにしてapplyするだけでメンテナンスモードにすることができます。
IP設定
AWSコンソールから手動でメンテナンスモード中もアクセスしたいIPのリストを設定します。
アクセス結果
許可されていないIPからPOSTMANでリクエストをしてみたところ503 Service Unavailableと、JSONメッセージが返ってきました。(許可されているIPの場合はいつも通りアクセスできました。)
本格的にフロントとの連携を考え始めるなら時間に関するメッセージを追加してもいいかなと思っています。
例えば以下のような感じです。
{
"message": "サービスは現在メンテナンス中です。お手数をおかけしますが、しばらくお待ちください。",
"status": "maintenance",
"start_time": "2023-10-02T08:00:00Z",
"end_time": "2023-10-02T10:00:00Z",
"estimated_duration_minutes": 120
}
このような情報があれば、フロントでメンテナンス予定時刻を表示することができますね。Terraformの変数で時間も入れられるようにしておいたら、使いやすくなりますかね。色々考えるのが楽しいです。
メンテナンスモード解除
メンテナンスモードを戻す時は、Terraform Cloudでフラグをfalseにして、terraform applyをすれば元に戻せます。
フラグがtrueになった時に初めてルールグループを作る方法でもよかったのですが、terraform applyをしなくても、ワンチャン手動でAWSコンソールのWAF ACLのルールでルールグループをアタッチするだけでもメンテナンスモードにできるように、フラグでの制御は、アタッチのON/OFFだけにしました。
まとめ
API GatewayのWAFでメンテナンスモードを実装しました。次はフロントの実装をしたら記事を書こうかなと思っています。
Discussion