TORANA TECH BLOG

株式会社トラーナのエンジニアチームの開発ブログ

redashのIPv6移行に失敗した話

SREのクラシマです。今回も失敗した話をば。

新着情報 – パブリック IPv4 アドレスの利用に対する新しい料金体系を発表 / Amazon VPC IP Address Manager が Public IP Insights の提供を開始 | Amazon Web Services ブログ 2024年2月よりPublic IPv4への課金が開始されるという話が出たので、社内でのPublic IPv4アドレスをカウントしたところ、金額がそれなりになりそうなので対策することにしました。
とはいえ、これまですべてのサービスでIPv4のみを利用、NAT GW費用もかけたくないのでPublic Subnetにサービスを構築しています。
また、AWSのパブリックIPv4の料金体系の変更とサイバーエージェントのIPv6活用推進事例 | CyberAgent Developers Blog を拝読すると、CloudFrontのオリジンにはまだIPv6が未対応とのこと。
弊社のプロダクトではAPIサーバの前にCloudFrontを置いているケースや、WordPressの前にCloudFrontを置いているケースがあり、移行対象外にせざるを得ないものも散見されます。
そんな中、先日引っ越しブログを書いたredashの引っ越しについて - TORANA TECH BLOGは独立したVPCで動作しており、CloudFrontなども配置しておらず、最初にIPv6移行を試して見るにはちょうど良さそう、ということで移行してみたのですが...。

ネットワークのIPv6移行

terraform管理しているVPC/Subnet/IGW/RouteTableをdualstackに切り替えます。 ざっくり、以下の対応を実施。

  1. VPCIPv6 CIDRを付与。特にこだわりがないのでAWSから自動でアサインされるものをそのまま使います
  2. 3AZ構成のサブネットそれぞれにIPv6 CIDRを付与、その際にDNS64/リソース作成時IPv6自動付与/起動時DNS AAAAレコード有効化をそれぞれONに
  3. route tableにIPv6でのIGWへのルート(::/0)を付与
module "subnet" {
  source = "../../"

  for_each = local.subnets

  vpc_id          = aws_vpc.vpc.id
  az              = each.value.az
  cidr_block      = each.value.cidr_block
  ipv6_cidr_block = each.value.ipv6_cidr_block
  gateway_route = {
    destination_cidr_block = "0.0.0.0/0"
    gateway_id             = aws_internet_gateway.gateway.id
  }
  gateway_route_ipv6 = {
    destination_cidr_block = "::/0"
    gateway_id             = aws_internet_gateway.gateway.id
  }
  name = each.key
}

locals {
  cidr_block = "192.168.0.0/16"
  subnets = {
    subnet-1a_1 = {
      az              = "ap-northeast-1a"
      cidr_block      = cidrsubnet(local.cidr_block, 8, 1)
      ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 1)
    }
    subnet-1c_1 = {
      az              = "ap-northeast-1c"
      cidr_block      = cidrsubnet(local.cidr_block, 8, 2)
      ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 2)
    }
    subnet-1d_1 = {
      az              = "ap-northeast-1d"
      cidr_block      = cidrsubnet(local.cidr_block, 8, 3)
      ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 3)
    }
  }
}

resource "aws_vpc" "vpc" {
  cidr_block                       = local.cidr_block
  assign_generated_ipv6_cidr_block = true
}
locals {
  ipv6_only   = var.cidr_block == null
  ipv6_enable = var.ipv6_cidr_block != null
}

resource "aws_subnet" "this" {
  cidr_block        = var.cidr_block
  vpc_id            = var.vpc_id
  availability_zone = var.az

  ipv6_cidr_block                                = var.ipv6_cidr_block
  ipv6_native                                    = local.ipv6_only
  enable_dns64                                   = local.ipv6_enable
  assign_ipv6_address_on_creation                = local.ipv6_enable
  enable_resource_name_dns_aaaa_record_on_launch = local.ipv6_enable

  tags = {
    Name = var.name
  }
}

resource "aws_route_table" "this" {
  vpc_id = var.vpc_id

  tags = {
    Name = var.name
  }
}

resource "aws_route_table_association" "this" {
  route_table_id = aws_route_table.this.id
  subnet_id      = aws_subnet.this.id
}

resource "aws_route" "gateway" {
  count = var.gateway_route == null ? 0 : 1

  route_table_id         = aws_route_table.this.id
  destination_cidr_block = var.gateway_route.destination_cidr_block
  gateway_id             = var.gateway_route.gateway_id
}

resource "aws_route" "gateway_ipv6" {
  count = var.gateway_route_ipv6 == null ? 0 : 1

  route_table_id              = aws_route_table.this.id
  destination_ipv6_cidr_block = var.gateway_route_ipv6.destination_cidr_block
  gateway_id                  = var.gateway_route_ipv6.gateway_id
}

切り替え自体はサービスダウンなしにうまくいきましたが、terraform-provider-aws v5.13.0以前だとIPv6 CIDRのアサインとIPv6関連フラグの有効化の順序が逆でapplyで落ちました。
Fix: Enabling IPv6 apply error by watarukura · Pull Request #32896 · hashicorp/terraform-provider-aws · GitHub ちんまりとしたコントリビューションですが、自分が使っているツールに貢献できて嬉しいところ。

ALB/ECS FargateのIPv6移行

ALBをにIPアドレスタイプをdualstackに切り替え、セキュリティグループもIPv6からの接続に対応します。
ターゲットグループもIPアドレスタイプをIPv6に切り替えるのですが、こちらは再作成になってしまうので、BLUE/GREEN DEPLOYMENT構成の場合は片側ずつ切り替えてやるのが良さそうです。

...と、ここまでは想定して手順書を組み立てていたのですが、「ターゲットグループの再作成めんどいなぁ」と日程調整しているある日...。

redashが沈黙

...稀によくあるやつで、重いクエリを投げつけたところredashが沈黙、CPUが100%に張り付いてうんともすんとも言わなくなりました。
EC2の頃はサーバ再起動用のGHAを用意していたのですが、Fargate移行後は手作業で再デプロイが必要です。(自動化しないとなぁ)
また、現行のredashが使用しているRQ Schedulerのバージョンが古く、scheduler更新に失敗する場合があります。
RedashでScheduler更新時に発生するエラー「There’s already an active RQ scheduler」について原因を調べて解消してみた | DevelopersIO
(最近、redashリポジトリの更新が盛んになっていて、RQ Schedulerのバージョンも上がっているので、次期バージョンでは直るはず!)

ということで、デプロイして次のschedulerが起動する前にredashが利用しているredisに接続してFLUSHDBを実行します。
さて、と待っているとworker、schedulerは正常起動したものの、serverが起動しません。WORKER_TIMEOUTが発生します。
Gunicornのタイムアウトは120秒に設定しており、前回のデプロイ時から特に変更してないのにおかしい、と数回デプロイを繰り返しますが起動せず。
redash停止から1時間ほどして、IPv6を無効化してみたところserverが起動してことなきを得ました。

ポストモーテム

根本原因として、cannot bind Gunicorn to IPv6 link local addresses · Issue #1628 · benoitc/gunicorn · GitHub どうやらGunicornがIPv6未対応のようです。
Subnetの設定で、リソース作成時にIPv6を付与するようにしたため、既存のインスタンスにはIPv4のみがアタッチされていましたが、新規のインスタンスにはIPv6がアタッチされたために発生したものと推定しています。
教訓: WebサーバがIPv6対応しているかを事前調査しましょう

ということで、改めて他のプロダクトも調査しましたがIPv6移行できそうな対象がほぼないことがわかりました...。
ステージング環境は3AZ -> 2AZにしてALBのIPアドレスを削減したり、Internal ALBへの切り替えなどで対応することを検討しようと思います。