理系学生日記

おまえはいつまで学生気分なのか

TerraformでMultiAZ構成のAmazon Aurora Serverless v2を構築する

Amazon Aurora Serverless v2

Amazon Auroraはコンピューティングとストレージを分離した構成をとるデータベースです。 そして先月に、そのServerless版の新しいバージョンである、Amazon Aurora Serverless v2がGAになりました。

ここで言う「Serverless」の意味合いですが、特段AWS Lambda等と関係しているわけではなく、 アプリケーションニーズに応じて自動的に起動や停止、スケーリングをしてくれるAuroraになります。

そのユースケースはAurora Serverless v2 の使用にまとめられています。 代表的なものは以下と言えるでしょうか。

  1. ワークロードが変動するアプリケーションに対するスケーリングの即応
  2. テナントごとにクラスターを作成することによる、マルチテナント対応の容易化 (テナントごとのデータ量の変動や、繁忙期の違いに対応しやすくなる)
  3. スケールの予測ができない新しいアプリケーションへの対応
  4. 開発やテストにおける、断続的なトラフィックへの対応

この中の2.なんていうのは、VMに乗っていたアプリケーションが細分化してコンテナに乗る流れの相似形に見え、 スケールアップが主となっていたデータベースもスケールアウトの流れに向かうのかと楽しみになります。

とにもかくにも構築してみる

まずはざっくり構築してみました。 Terraformのコードは末尾に示しますが、わかってみればそれほど悩むところはないかもしれません。 Serverlessとはいえ、RDSと多くの設定は同一か類似しています。

ハマったところ: Multi-AZ構成

Multi-AZ構成をとるという定義を、「複数AZにAuroraクラスタの中のインスタンスを配置する」とした場合、それを実現するのに苦労しました。 rds_cluster_instanceにはavailability_zoneという パラメータがあります。これが未指定の場合、AuroraがAZをランダムに選んでくれるというのがマニュアルの記載なのですが。

Aurora automatically chooses an appropriate Availability Zone if you don't specify one.

Default: A random, system-chosen Availability Zone in the endpoint's AWS Region.

CreateDBInstance

実際に複数インスタンスをavailability_zone未指定で投入した場合、すべてがap-northeast-1aに配置されてしまい、なかなかMulti-AZになりませんでした。 「ランダム」と言っているからには、単にぼくの運が悪いという可能性もありますが。

このため、以下のようにAZを明示的に指定するようにしています。

   availability_zone = var.availability_zones[count.index % length(var.availability_zones)]

これを行うと、以下の画像のようにインスタンスが複数AZに配置される形になりました。

ハマったところ: インターネットからのアクセス

rds_cluster_instancepublicly_accessibletrueを指定すると、Aurora Serverlessへのインターネットごしの直接アクセスが可能になります。 ただ、これは当たり前ですがAuroraが接続可能なDBサブネットをVPCの「パブリックサブネット」で構成することが条件です。 ぼくはずっと「プライベートサブネット」を組み合わせたDBサブネットとAuroraを接続していていたので、インターネットアクセスができないできないと長い間悩みました。

クエリエディタはServerless v2では接続できない

きちんと構築できているのかを簡単に試そうとして、クエリエディタを利用しようとしていました。ただ、これはServerless v2には対応していないようですね。 自分が作ったはずのクラスタが接続先の選択肢に出てこないので、それなりに焦りました。

Terraformコード

resource "aws_rds_cluster" "this" {
  cluster_identifier = var.name

  availability_zones = var.availability_zones

  engine_mode = "provisioned" # Serverless v2
  engine      = "aurora-postgresql"

  engine_version = var.engine_version
  database_name  = var.database_name
  port           = var.port

  master_username = var.master_username
  master_password = var.master_password

  apply_immediately = var.apply_immediately

  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.this.name

  db_subnet_group_name = aws_db_subnet_group.this.name

  # Major Version Up は自動的に行わない
  allow_major_version_upgrade = false

  preferred_backup_window      = var.preferred_backup_window_in_utc
  preferred_maintenance_window = var.preffered_maintenance_window_in_utc
  backup_retention_period      = var.backup_retention_period_in_days
  final_snapshot_identifier    = join("", [var.name, "-", formatdate("YYYY-MM-DD-HH-mm-ss", timestamp())]) # undersocre は利用不可
  copy_tags_to_snapshot        = true

  # DB を削除可能とするか
  deletion_protection = var.deletion_protection

  # まずはログに出せるものは全て出力。問題があれば減らしていく。
  # 実質的に設定できる値は "postgresql" のみ
  enabled_cloudwatch_logs_exports = ["postgresql"]

  enable_http_endpoint = true

  iam_database_authentication_enabled = false

  vpc_security_group_ids = [aws_security_group.allow_db_client.id]

  serverlessv2_scaling_configuration {
    min_capacity = var.minimum_capacity
    max_capacity = var.maximum_capacity
  }

  tags = {
    Name = var.name
  }

  lifecycle {
    ignore_changes = [
      master_password,
      # 2 つの AZ しか設定しない場合であっても、AWS が 3 つを指定したことにするため、
      # 必ず差分が発生してしまう
      # see: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster#availability_zones
      availability_zones
    ]
  }
}

resource "aws_rds_cluster_instance" "example" {
  count = var.replica_number + 1 # Master インスタンスで +1

  availability_zone = var.availability_zones[count.index % length(var.availability_zones)]

  identifier           = "${var.name}-instance-${count.index}"
  cluster_identifier   = aws_rds_cluster.this.id
  db_subnet_group_name = aws_rds_cluster.this.db_subnet_group_name

  engine         = aws_rds_cluster.this.engine
  engine_version = aws_rds_cluster.this.engine_version

  apply_immediately = var.apply_immediately

  publicly_accessible = var.publicly_accessible

  # メモリはみたいので、拡張モニタリングを有効化
  monitoring_role_arn = aws_iam_role.monitoring.arn
  monitoring_interval = 60 # CloudWatch の課金額を小さくするため最大間隔を指定

  instance_class = "db.serverless" # Aurora Serverless v2

  tags = {
    Name = "${var.name}-instance-${count.index}"
  }
}

resource "aws_db_subnet_group" "this" {
  subnet_ids = var.subnet_ids

  tags = {
    Name = "${var.name}-db-subnet-group"
  }
}

resource "aws_security_group" "allow_db_client" {
  name        = "allow db client"
  description = "Allow From Postgresql DB Client"
  vpc_id      = var.vpc_id

  tags = {
    Name = "${var.name} DB Client Access Rule"
  }
}

resource "aws_security_group_rule" "allow_db_client" {
  security_group_id = aws_security_group.allow_db_client.id
  type              = "ingress"

  description = "Allow From Postgresql DB Client"
  protocol    = "tcp"
  from_port   = 5432
  to_port     = 5432
  cidr_blocks = var.allow_db_access_cidr_blocks
}

resource "aws_rds_cluster_parameter_group" "this" {
  name        = "aurora-parameter-group"
  family      = "aurora-postgresql13"
  description = "Cluster Parameter Group"

  # デフォルト 1 秒だが、1 秒ごとにロック獲得待ちの tx に対してチェックするのは重い
  # see: https://soudai.hatenablog.com/entry/2017/12/26/080000
  parameter {
    name  = "deadlock_timeout"
    value = "10000"
  }

  # いわゆるスロークエリログの出力。デフォルトは -1 (off)
  # 1 秒以上のクエリをログ出力するよう設定
  parameter {
    name  = "log_min_duration_statement"
    value = "1000"
  }

  tags = {
    Name = "Aurora Parameter Group"
  }
}

# 拡張モニタリング用ロール
# see: https://dev.classmethod.jp/articles/rds-enhanced-monitoring-manual/
resource "aws_iam_role" "monitoring" {
  name        = "EnhancedMonitoringRole"
  description = "Role to enable enhanced monitoring"

  assume_role_policy  = data.aws_iam_policy_document.assume_role_policy.json
  managed_policy_arns = [data.aws_iam_policy.enhanced_monitoring.arn]

  tags = {
    Name = "EnhancedMonitoringRole"
  }
}

data "aws_iam_policy" "enhanced_monitoring" {
  name = "AmazonRDSEnhancedMonitoringRole"
}

# マネジメントコンソールから作成された信頼ポリシーをそのまま記述
data "aws_iam_policy_document" "assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["monitoring.rds.amazonaws.com"]
    }
  }
}