TerraformでAmazon Auroraのクローンを作ってみた

TerraformでAmazon Auroraのクローンを作ってみた

terraform Advent Calendar 2024の5日目の記事です。TerraformでAmazon Auroraのクローンを作ってみました
Clock Icon2024.12.05

お疲れさまです。とーちです。

本ブログはterraform Advent Calendar 2024の5日目になります。

TerraformでAmazon Aurora MySQL(以下Aurora)のクローンを作る機会があったので、そのときに詰まったところなどを共有します。

Auroraのクローンについて

Amazon Auroraでデータベースのクローンを作成できる機能となっており、Copy-on-Writeプロトコルにより高速に本番環境等のデータを別環境に持ち込むことができます。ユースケースとしては、本番環境から開発環境へのクローンやデータ分析クエリを実行するためのクローン作成などがあるかと思います。

Terraformで作成する場合に必要なResource Blocks

AWS CLIからAuroraクローンを作成する際の手順としては以下となります。

  1. restore-db-cluster-to-point-in-time で元のクラスターと同じストレージデータを持つ Aurora DB クラスターを作成
  2. create-db-instance CLI コマンドを使用して、クラスターに含めるDB インスタンスを作成
  3. ご参考:https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Clone.html#Aurora.Managing.Clone.create

そのため基本的には上記で作成している通りに、AuroraクラスターとAuroraインスタンスを作成するためのResource Blocksを定義すればOKです。具体的には以下となります。

  • aws_rds_cluster
  • aws_rds_cluster_instance
  • aws_security_groupやaws_rds_cluster_parameter_groupなどの周辺リソース

クローン元Aurora作成のためのTerraformコード

それではまずクローン元となるAuroraを作成するためのTerraformコードを見ていきます。なお、今回ご紹介するコードはモジュールとして呼び出すことを想定して作っています。
クローン元となるAuroraは普通に作成しているだけなので特別、注意点等はありませんが、一点だけKMSについて触れておきます。Auroraクローンでは他アカウントにクローンする際にデフォルト暗号化キーで作成されたAuroraをクローンすることはできません。Auroraクローンはクロスアカウントで実施することが多いと思うので、KMSキーは作成しておいたほうがいいと思います。KMSキーの権限は必要に応じて絞ってください。

main.tf
data "aws_caller_identity" "current" {}
resource "aws_kms_key" "main" {
  description             = "KMS key"
  enable_key_rotation     = true
  deletion_window_in_days = 20
  policy = jsonencode({
    Version = "2012-10-17"
    Id      = "key-default-1"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        },
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "Grant necessary permissions to the destination AWS account for cloning"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.targetAccountID}:root"
        },
        Action   = "kms:*"
        Resource = "*"
      }
    ]
  })
}
resource "aws_kms_alias" "main" {
  name          = "alias/rds/${var.prefix}-cluster"
  target_key_id = aws_kms_key.main.key_id

}

# rds
resource "aws_rds_cluster" "main" {
  cluster_identifier              = "${var.prefix}-rds-aurora-mysql-cluster"
  engine                          = "aurora-mysql"
  engine_version                  = var.rds_cluster_info.rds_engine_version
  availability_zones              = var.rds_cluster_info.availability_zones
  database_name                   = var.rds_cluster_info.database_name
  master_username                 = var.rds_cluster_info.master_username
  manage_master_user_password     = true
  db_subnet_group_name            = aws_db_subnet_group.main.name
  vpc_security_group_ids          = [aws_security_group.main.id]
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.main.name
  backup_retention_period         = var.rds_cluster_info.backup_retention_period
  preferred_backup_window         = var.rds_cluster_info.preferred_backup_window
  delete_automated_backups        = var.rds_cluster_info.delete_automated_backups
  port                            = 3306
  storage_encrypted               = true
  storage_type                    = ""
  kms_key_id                      = aws_kms_key.main.arn
  enabled_cloudwatch_logs_exports = var.rds_cluster_info.cloudwatch_logs_exports
  preferred_maintenance_window    = var.rds_cluster_info.preferred_maintenance_window
  skip_final_snapshot             = false
  final_snapshot_identifier       = "${var.prefix}-cluster-final-snapshot-${formatdate("YYYYMMDDHHmmss", timestamp())}"
  apply_immediately               = var.rds_cluster_info.apply_immediately
  allow_major_version_upgrade     = var.rds_cluster_info.allow_major_version_upgrade
  deletion_protection             = var.rds_cluster_info.deletion_protection
  lifecycle {
    ignore_changes = [
      # 3AZ未満が設定されている場合、RDSは自動的に3AZを割り当てようとするため必ず差分が発生してしまう。そのため無視設定としている
      # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster
      availability_zones,
      # formatdateにより毎回変わるため
      final_snapshot_identifier
    ]
  }
}

resource "aws_rds_cluster_instance" "main" {
  for_each = var.rds_instance_info

  identifier                            = each.key
  cluster_identifier                    = aws_rds_cluster.main.cluster_identifier
  instance_class                        = each.value.rds_instance_class
  engine                                = aws_rds_cluster.main.engine
  engine_version                        = aws_rds_cluster.main.engine_version
  db_parameter_group_name               = aws_db_parameter_group.main[each.key].name
  publicly_accessible                   = false
  performance_insights_enabled          = each.value.performance_insights_enabled
  performance_insights_retention_period = each.value.performance_insights_retention_period
  auto_minor_version_upgrade            = each.value.auto_minor_version_upgrade
  preferred_maintenance_window          = each.value.preferred_maintenance_window
  apply_immediately                     = each.value.apply_immediately
  ca_cert_identifier                    = each.value.ca_cert_identifier
  promotion_tier                        = each.value.promotion_tier

}

## security group for rds
resource "aws_security_group" "main" {
  name        = "${var.prefix}-sg-aurora-mysql"
  description = "${var.prefix}-sg-aurora-mysql"
  vpc_id      = var.vpc_id
  tags = {
    Name = "${var.prefix}-sg-aurora-mysql"
  }
}
# security group outbound rule for rds
resource "aws_vpc_security_group_egress_rule" "main" {
  for_each          = var.rds_outbounds_rules
  security_group_id = aws_security_group.main.id

  cidr_ipv4   = each.value.cidr_ipv4
  from_port   = each.value.from_port
  to_port     = each.value.to_port
  ip_protocol = each.value.ip_protocol
}

# security group inbound rule for rds
resource "aws_vpc_security_group_ingress_rule" "main" {
  for_each          = var.rds_inbounds_rules
  security_group_id = aws_security_group.main.id

  referenced_security_group_id = each.value.referenced_security_group_id
  cidr_ipv4                    = each.value.cidr_ipv4
  from_port                    = each.value.from_port
  to_port                      = each.value.to_port
  ip_protocol                  = each.value.ip_protocol
}

## subnet group
resource "aws_db_subnet_group" "main" {
  name       = "${var.prefix}-subnetgroup"
  subnet_ids = var.isolated_subnet_ids

  tags = {
    Name = "DB subnet group"
  }
}

resource "aws_rds_cluster_parameter_group" "main" {
  name_prefix = var.rds_cluster_parameter_group_info.rds_cluster_parameter_group_name_prefix
  family      = var.rds_cluster_and_db_parameter_group_engine_family
  description = "${var.rds_cluster_parameter_group_info.rds_cluster_parameter_group_name_prefix} parameter group"

  dynamic "parameter" {
    for_each = var.rds_cluster_parameter_group_info.rds_cluster_parameters
    content {
      name         = parameter.value.name
      value        = parameter.value.value
      apply_method = parameter.value.apply_method
    }
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_db_parameter_group" "main" {
  for_each    = var.rds_instance_info
  name_prefix = each.key
  family      = var.rds_cluster_and_db_parameter_group_engine_family
  description = "${each.key} parameter group"
  dynamic "parameter" {
    for_each = each.value.rds_db_parameters
    content {
      name         = parameter.value.name
      value        = parameter.value.value
      apply_method = parameter.value.apply_method
    }
  }
  lifecycle {
    create_before_destroy = true
  }

}
variables.tf
variable "vpc_id" {
  type        = string
  description = "vpc id"
}

variable "isolated_subnet_ids" {
  type        = list(string)
  description = "isolated subnet ids"
}
variable "targetAccountID" {
  type        = string
  description = "target account id"
}
variable "prefix" {
  type        = string
  description = "prefix"
}
variable "rds_cluster_and_db_parameter_group_engine_family" {
  type        = string
  description = "rds parameter group engine family"

}
variable "rds_outbounds_rules" {
  type = map(object({
    from_port   = number
    to_port     = number
    cidr_ipv4   = string
    ip_protocol = string
  }))
  description = "rds security group rules for outbounds"
}

variable "rds_inbounds_rules" {
  type = map(object({
    referenced_security_group_id = string
    cidr_ipv4                    = string
    from_port                    = number
    to_port                      = number
    ip_protocol                  = string
  }))
  description = "rds security group rules for inbounds"
}

variable "rds_cluster_parameter_group_info" {
  type = object({
    rds_cluster_parameter_group_name_prefix = string
    rds_cluster_parameters = map(object({
      name         = string
      value        = string
      apply_method = string
    }))
  })
  description = "rds cluster parameter group related"
}

variable "rds_cluster_info" {
  type = object({
    rds_engine_version           = string
    availability_zones           = list(string)
    database_name                = string
    master_username              = string
    backup_retention_period      = number
    delete_automated_backups     = bool
    preferred_backup_window      = string
    preferred_maintenance_window = string
    kms_key_id                   = string
    cloudwatch_logs_exports      = list(string)
    apply_immediately            = bool
    allow_major_version_upgrade  = bool
    deletion_protection          = bool
  })
  description = "rds instance parameter group related"

}
variable "rds_instance_info" {
  type = map(object({
    rds_instance_class                    = string
    monitoring_interval                   = number
    performance_insights_enabled          = bool
    performance_insights_retention_period = number
    apply_immediately                     = bool
    auto_minor_version_upgrade            = bool
    preferred_maintenance_window          = string
    promotion_tier                        = number
    ca_cert_identifier                    = string
    rds_db_parameters = map(object({
      name         = string
      value        = string
      apply_method = string
    }))

  }))
  description = "rds instance parameter group related"

}
outputs.tf
output "aws_rds_cluster_id" {
  value       = aws_rds_cluster.main.id
  description = "rds_cluster_id"
}

Auroraクローン作成のためのTerraformコード

次にAuroraクローンを作成するためのTerraformコードを見ていきます。
クローン元Auroraとはライフサイクルが異なると思うので、別モジュールとしています。

main.tf
resource "aws_rds_cluster" "clone" {
  cluster_identifier = "${var.prefix}-rds-aurora-mysql-clone"
  engine             = "aurora-mysql"
  engine_version     = var.rds_cluster_info.rds_engine_version
  availability_zones = var.rds_cluster_info.availability_zones
  #database_name      = var.rds_cluster_info.database_name
  #master_username    = var.rds_cluster_info.master_username
  manage_master_user_password     = true
  db_subnet_group_name            = aws_db_subnet_group.main.name
  vpc_security_group_ids          = [aws_security_group.main.id]
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.main.name
  backup_retention_period         = var.rds_cluster_info.backup_retention_period
  preferred_backup_window         = var.rds_cluster_info.preferred_backup_window
  delete_automated_backups        = var.rds_cluster_info.delete_automated_backups
  port                            = 3306
  #storage_encrypted               = true
  storage_type                    = ""
  kms_key_id                      = var.rds_cluster_info.kms_key_id
  enabled_cloudwatch_logs_exports = var.rds_cluster_info.cloudwatch_logs_exports
  preferred_maintenance_window    = var.rds_cluster_info.preferred_maintenance_window
  skip_final_snapshot             = false
  final_snapshot_identifier       = "${var.prefix}-cluster-final-snapshot-${formatdate("YYYYMMDDHHmmss", timestamp())}"
  apply_immediately               = var.rds_cluster_info.apply_immediately
  allow_major_version_upgrade     = var.rds_cluster_info.allow_major_version_upgrade
  deletion_protection             = var.rds_cluster_info.deletion_protection
  restore_to_point_in_time {
    source_cluster_identifier  = var.source_cluster_identifier
    restore_type               = "copy-on-write"
    use_latest_restorable_time = true
  }
  lifecycle {
    ignore_changes = [
      # 3AZ未満が設定されている場合、RDSは自動的に3AZを割り当てようとするため必ず差分が発生してしまう。そのため無視設定とする
      # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster
      availability_zones,
      # formatdateにより毎回変わるため
      final_snapshot_identifier
    ]
  }
}

resource "aws_rds_cluster_instance" "clone" {
  for_each = var.rds_instance_info

  identifier                            = "${each.key}-clone"
  cluster_identifier                    = aws_rds_cluster.clone.cluster_identifier
  instance_class                        = each.value.rds_instance_class
  engine                                = aws_rds_cluster.clone.engine
  engine_version                        = aws_rds_cluster.clone.engine_version
  db_parameter_group_name               = aws_db_parameter_group.main[each.key].name
  publicly_accessible                   = false
  performance_insights_enabled          = each.value.performance_insights_enabled
  performance_insights_retention_period = each.value.performance_insights_retention_period
  auto_minor_version_upgrade            = each.value.auto_minor_version_upgrade
  preferred_maintenance_window          = each.value.preferred_maintenance_window
  apply_immediately                     = each.value.apply_immediately
  ca_cert_identifier                    = each.value.ca_cert_identifier
  promotion_tier                        = each.value.promotion_tier

}

## security group for rds
resource "aws_security_group" "main" {
  name        = "${var.prefix}-sg-aurora-mysql-clone"
  description = "${var.prefix}-sg-aurora-mysql-clone"
  vpc_id      = var.vpc_id
  tags = {
    Name = "${var.prefix}-sg-aurora-mysql-clone"
  }
}
# security group outbound rule for rds
resource "aws_vpc_security_group_egress_rule" "main" {
  for_each          = var.rds_outbounds_rules
  security_group_id = aws_security_group.main.id

  cidr_ipv4   = each.value.cidr_ipv4
  from_port   = each.value.from_port
  to_port     = each.value.to_port
  ip_protocol = each.value.ip_protocol
}

# security group inbound rule for rds
resource "aws_vpc_security_group_ingress_rule" "main" {
  for_each          = var.rds_inbounds_rules
  security_group_id = aws_security_group.main.id

  referenced_security_group_id = each.value.referenced_security_group_id
  cidr_ipv4                    = each.value.cidr_ipv4
  from_port                    = each.value.from_port
  to_port                      = each.value.to_port
  ip_protocol                  = each.value.ip_protocol
}

## subnet group
resource "aws_db_subnet_group" "main" {
  name       = "${var.prefix}-sbgp-aurora-mysql-clone"
  subnet_ids = var.isolated_subnet_ids

  tags = {
    Name = "DB subnet group"
  }
}

resource "aws_rds_cluster_parameter_group" "main" {
  name_prefix = var.rds_cluster_parameter_group_info.rds_cluster_parameter_group_name_prefix
  family      = var.rds_cluster_and_db_parameter_group_engine_family
  description = "${var.rds_cluster_parameter_group_info.rds_cluster_parameter_group_name_prefix} parameter group"

  dynamic "parameter" {
    for_each = var.rds_cluster_parameter_group_info.rds_cluster_parameters
    content {
      name         = parameter.value.name
      value        = parameter.value.value
      apply_method = parameter.value.apply_method
    }
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_db_parameter_group" "main" {
  for_each    = var.rds_instance_info
  name_prefix = each.key
  family      = var.rds_cluster_and_db_parameter_group_engine_family
  description = "${each.key} parameter group"
  dynamic "parameter" {
    for_each = each.value.rds_db_parameters
    content {
      name         = parameter.value.name
      value        = parameter.value.value
      apply_method = parameter.value.apply_method
    }
  }
  lifecycle {
    create_before_destroy = true
  }

}
variables.tf
variable "vpc_id" {
  type        = string
  description = "vpc id"
}

variable "isolated_subnet_ids" {
  type        = list(string)
  description = "isolated subnet ids"
}

variable "prefix" {
  type        = string
  description = "prefix"
}
variable "rds_cluster_and_db_parameter_group_engine_family" {
  type        = string
  description = "rds parameter group engine family"

}
variable "rds_outbounds_rules" {
  type = map(object({
    from_port   = number
    to_port     = number
    cidr_ipv4   = string
    ip_protocol = string
  }))
  description = "rds security group rules for outbounds"
}

variable "rds_inbounds_rules" {
  type = map(object({
    referenced_security_group_id = string
    cidr_ipv4                    = string
    from_port                    = number
    to_port                      = number
    ip_protocol                  = string
  }))
  description = "rds security group rules for inbounds"
}

variable "rds_cluster_parameter_group_info" {
  type = object({
    rds_cluster_parameter_group_name_prefix = string
    rds_cluster_parameters = map(object({
      name         = string
      value        = string
      apply_method = string
    }))
  })
  description = "rds cluster parameter group related"
}

variable "rds_cluster_info" {
  type = object({
    rds_engine_version           = string
    availability_zones           = list(string)
    database_name                = string
    master_username              = string
    backup_retention_period      = number
    delete_automated_backups     = bool
    preferred_backup_window      = string
    preferred_maintenance_window = string
    kms_key_id                   = string
    cloudwatch_logs_exports      = list(string)
    apply_immediately            = bool
    allow_major_version_upgrade  = bool
    deletion_protection          = bool
  })
  description = "rds instance parameter group related"

}
variable "rds_instance_info" {
  type = map(object({
    rds_instance_class                    = string
    monitoring_interval                   = number
    performance_insights_enabled          = bool
    performance_insights_retention_period = number
    apply_immediately                     = bool
    auto_minor_version_upgrade            = bool
    preferred_maintenance_window          = string
    promotion_tier                        = number
    ca_cert_identifier                    = string
    rds_db_parameters = map(object({
      name         = string
      value        = string
      apply_method = string
    }))

  }))
  description = "rds instance parameter group related"

}

variable "source_cluster_identifier" {
  type        = string
  description = "source cluster identifier"

}

ポイント

  • database_name, master_username, storage_encryptedについて
  • restore_to_point_in_time
    • 一番の違いは以下のrestore_to_point_in_timeというパラメータの有無です。こちらのパラメータはドキュメント上ではポイントインタイム復元用のパラメータとされているので私はこのパラメータでクローンが作成できると気づくのに少し時間がかかりました。それぞれのパラメータの意味は以下の通りです。
    • source_cluster_identifier:復元元Auroraのクラスター識別子を指定するのですが、ドキュメント上は別のAWSアカウントのクラスターから復元する場合はARNを指定と書いてあるのですが、Auroraクローンについては同一アカウントでもARNを指定する必要がありました。
    • restore_type:copy-on-writeを選びます
    • use_latest_restorable_time:データベース クラスターを最新の復元可能なバックアップ時刻に復元するには、true に設定 とのこと。基本的にtrueで良いと思います。
    •   restore_to_point_in_time {
            #source_cluster_identifier  = "arn:aws:rds:ap-northeast-1:<クローン元AWSアカウントID>:cluster:rds-cluster"
            source_cluster_identifier  = var.source_cluster_identifier
            restore_type               = "copy-on-write"
            use_latest_restorable_time = true
        }
      
      

まとめ

Terraformで作るAuroraクローンについてお届けしました。この記事が誰かのお役に立てば幸いです。

以上、とーちでした。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.