TerraformでRDSを構築してみよう!

technologies

こんにちは株式会社ベンジャミンの市川です!

暑さが日ごとに加わってまいりましたが、お健やかにお過ごしでしょうか。

今回は、弊社の案件でも利用しているインフラをコード化できるIaCの1つであるTerraformを使って、Amazon RDSを作成してみましたので、その方法について書いていきます。

注意※ AWS無料利用枠の範囲外のリソースが含まれます。作成した後は必要に応じて、リソースを削除してください。

目次

環境情報

  • Terraform v1.4.4
  • macOS Monterey 12.5

前提

  • Terraform v1.4.4以上 がインストール済みであること
  • AWSアカウントを持っていること
  • アクセスキー、シークレットアクセスキーを発行済みであること

AWS構成図

ディレクトリ構成

今回のハンズオンで作成していく最終的なディレクトリ構成は以下のとおりです。

root/
├ main.tf # 全体設定および/modulesのresourceに渡す変数の値を定義・呼び出し
├ terraform.tfvars # 環境名、認証情報、リージョンを定義 ※機密情報であるため必ずgit管理から外すこと
└ modules/ # 各リソースのファイル
  ├ rds/ # リソースの性能・パラメータを定義したテンプレート。基本的にはいじらない(いじりたい値がある場合はvariableとして切り出し、main.tfから注入する)
  │ ├ cluster.tf
  │ ├ instance.tf
  │ ├ parameter.tf
  │ ├ variables.tf # modules/rds/の各.tfで使用する型情報、初期値などを定義
  │ └ outputs.tf # 他のリソースへ変数の値を公開
  └ vpc/ # リソースの性能・パラメータを定義したテンプレート。基本的にはいじらない(いじりたい値がある場合はvariableとして切り出し、main.tfから注入する)
    ├ vpc
    │ └ vpc.tf
    ├ private_subnet
    │ └ private_subnet.tf
    ├ security_group
    │ └ security_group.tf
    ├ route_table
    │ └ route_table.tf
    └ route_table_association
      └ route_table_association.tf

1.Terraformのバージョンを確認する

まず、Terraformがインストール済みであるか、かつバージョンがv1.4.4以上であるかを確認しておきます。次のコマンドを実行し、確認してみましょう!

terraform --version

実行後、以下の例のように出力されていればインストールできています。

xxxxxxxxx@xxxxxxxxMacBook-Pro rds % terraform --version
Terraform v1.4.4
on darwin_arm64

2.Terraformの初期設定をする

次に、TerraformからAWSリソースにアクセスするためのクレデンシャル情報(リソース作成・削除の実行権限があるアカウントの)やバージョンなどを設定していきましょう!

2-1. コードの作成

以下のとおりファイルを作成し、コードを書いてみてください。

  • terraform.tfvars
environment = "test"
access_key  = "XXXXXXXXXXXXXXXXXXXX" # ご自分のアカウントのアクセスキーに置き換えてください
secret_key  = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # ご自分のアカウントのシークレットキーに置き換えてください
region = "ap-northeast-1"
  • main.tf
variable "environment" { type = string }
variable "access_key" { type = string }
variable "secret_key" { type = string }
variable "region" { type = string }

locals {
  service = "rds-handson"
}

terraform {
  required_version = ">=1.4.4"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.60.0"
    }
  }
}

provider "aws" {
  access_key = var.access_key
  secret_key = var.secret_key
  region     = var.region

  default_tags {
    tags = {
      Environment = var.environment
      Service     = local.service
    }
  }
}

コード補足

`default_tags{…}` を書くことで、Terraformで作成する全てのリソースに自動的にタグを付加することができます。タグを付けるとリソースの管理や検索がしやすくなります!

  default_tags {
    tags = {
      Environment = var.environment
      Service     = local.service
    }
  }

2-2. 作業ディレクトリの初期化、必要なプラグインのインストール

コードをもとに初期化するため、ターミナルから以下のコマンドを実行しましょう。

terraform init

3.VPC関連を作成する

RDSを作成する前に、RDSを立ち上げるためのVPC関連(VPC、サブネット、セキュリティグループ、ルートテーブル、ルート)を作成していきましょう。

また、今回のハンズオンではAWSリソースの再利用性を高めるため、ソースコードをテンプレート化できる機能「Terraform Module」を使って構築していきます。

この機能を使えば、moduleブロックからresourceブロックに変数(variable)の値を渡し、呼び出すことで、その箇所だけをカスタマイズしたリソースを好きな数だけ作成することができます!

3-1. コードの作成

以下のとおりファイルを作成し、コードを書いてみてください。※main.tfは追加で記述

  • main.tf
/* ~~~ 省略(これまでのmain.tfに以下のコードを追加してください) ~~~ */
module "vpc" {
  source = "./modules/vpc/vpc"

  vpc_config = {
    cidr_block = "10.100.0.0/16"
    name       = "${var.environment}-${local.service}-vpc"
  }
}

module "private_subnet_aurora_1a" {
  source = "./modules/vpc/private_subnet"

  vpc_subnets_private = {
    vpc_id            = module.vpc.vpc_id
    availability_zone = "ap-northeast-1a"
    cidr_block        = "10.100.20.0/24"
    name              = "${var.environment}-${local.service}-subnet-private-a1-aurora"
  }
}

module "private_subnet_aurora_1c" {
  source = "./modules/vpc/private_subnet"

  vpc_subnets_private = {
    vpc_id            = module.vpc.vpc_id
    availability_zone = "ap-northeast-1c"
    cidr_block        = "10.100.30.0/24"
    name              = "${var.environment}-${local.service}-subnet-private-c1-aurora"
  }
}

module "security_group_aurora" {
  source = "./modules/vpc/security_group"

  vpc_security_group = {
    vpc_id      = module.vpc.vpc_id
    name        = "${var.environment}-${local.service}-sg-aurora"
    description = "for Aurora"
  }

  # インバウンドルール
  vpc_security_group_ingress_rule = {
    security_groups = [] # 今回は指定していませんが、通常はFargate のセキュリティグループなどを指定します
    protocol        = "tcp"
    from_port       = 3306
    to_port         = 3306
  }

  # アウトバウンドルール
  vpc_security_group_egress_rule = {
    cidr_blocks = ["0.0.0.0/0"]
    protocol    = "tcp"
    from_port   = 0
    to_port     = 65535
    description = "Outbound ALL"
  }
}

module "route_table_private_aurora" {
  source = "./modules/vpc/route_table"

  vpc_route_table = {
    vpc_id = module.vpc.vpc_id
    name   = "${var.environment}-${local.service}-rtb-private-aurora"
  }
}

module "route_table_association_private_aurora_1a" {
  source = "./modules/vpc/route_table_association"

  vpc_route_table_association = {
    subnet_id      = module.private_subnet_aurora_1a.private_subnet_id
    route_table_id = module.route_table_private_aurora.route_table_id
  }
}

module "route_table_association_private_aurora_1c" {
  source = "./modules/vpc/route_table_association"

  vpc_route_table_association = {
    subnet_id      = module.private_subnet_aurora_1c.private_subnet_id
    route_table_id = module.route_table_private_aurora.route_table_id
  }
}
  • 【VPC】/modules/vpc/vpc.tf
variable "vpc_config" {
  type = object({
    cidr_block = string
    name       = string
  })
}

output "vpc_id" {
  value = aws_vpc.default.id
}

resource "aws_vpc" "default" {
  cidr_block           = var.vpc_config.cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = var.vpc_config.name
  }
}
  • 【プライベートサブネット】/modules/vpc/private_subnet.tf
variable "vpc_subnets_private" {
  type = object({
    vpc_id            = string
    availability_zone = string
    cidr_block        = string
    name              = string
  })
}

output "private_subnet_id" {
  value = aws_subnet.private.id
}

resource "aws_subnet" "private" {
  vpc_id                  = var.vpc_subnets_private.vpc_id
  map_public_ip_on_launch = false # プライベート

  availability_zone = var.vpc_subnets_private.availability_zone
  cidr_block        = var.vpc_subnets_private.cidr_block
  tags = {
    Name = var.vpc_subnets_private.name
  }
}
  • 【セキュリティグループ】/modules/vpc/security_group.tf
variable "vpc_security_group" {
  type = object({
    name        = string
    vpc_id      = string
    description = optional(string)
  })
}

variable "vpc_security_group_ingress_rule" {
  type = object({
    security_groups = optional(list(string))
    cidr_blocks     = optional(list(string))
    protocol        = string
    from_port       = number
    to_port         = number
    description     = optional(string)
  })
}

variable "vpc_security_group_egress_rule" {
  type = object({
    cidr_blocks = list(string)
    protocol    = string
    from_port   = number
    to_port     = number
    description = optional(string)
  })
}

output "security_group_id" {
  value = aws_security_group.default.id
}

resource "aws_security_group" "default" {
  name        = var.vpc_security_group.name
  vpc_id      = var.vpc_security_group.vpc_id
  description = var.vpc_security_group.description

  // インバウンドトラフィック
  ingress {
    security_groups = var.vpc_security_group_ingress_rule.security_groups
    cidr_blocks     = var.vpc_security_group_ingress_rule.cidr_blocks
    protocol        = var.vpc_security_group_ingress_rule.protocol
    from_port       = var.vpc_security_group_ingress_rule.from_port
    to_port         = var.vpc_security_group_ingress_rule.to_port
    description     = var.vpc_security_group_ingress_rule.description
  }

  // アウトバウンドトラフィック
  egress {
    cidr_blocks = var.vpc_security_group_egress_rule.cidr_blocks
    protocol    = var.vpc_security_group_egress_rule.protocol
    from_port   = var.vpc_security_group_egress_rule.from_port
    to_port     = var.vpc_security_group_egress_rule.to_port
    description = var.vpc_security_group_egress_rule.description
  }
}
  • 【ルートテーブル】/modules/vpc/route_table.tf
variable "vpc_route_table" {
  type = object({
    vpc_id = string
    name   = string
  })
}

output "route_table_id" {
  value = aws_route_table.default.id
}

resource "aws_route_table" "default" {
  vpc_id = var.vpc_route_table.vpc_id

  tags = {
    Name = var.vpc_route_table.name
  }
}
  • 【ルート】/modules/vpc/route_table_association.tf
variable "vpc_route_table_association" {
  type = object({
    subnet_id      = string
    route_table_id = string
  })
}

resource "aws_route_table_association" "default" {
  subnet_id      = var.vpc_route_table_association.subnet_id
  route_table_id = var.vpc_route_table_association.route_table_id
}

3-2. TerraformからAWS環境にリソースを作成

VPCの各ファイルを作成してきたところで、一度リソースをAWS環境へ作成してみましょう!
次の手順に沿って進めてみてください。

  • 依存関係を更新する

moduleブロックを追加したとき、/modules配下の子モジュール(.tfファイル)をimportするためには`terraform init` で依存関係を更新する必要があります。
moduleブロックを追加するたびに更新が必要ですので注意しましょう

ターミナルから以下のコマンドを実行しましょう。

terraform init
  • Terraform による実行計画を確認する

AWS環境にリソースを作成する前の確認作業として、`terraform plan` で.tf ファイルに記載された情報を元に、どのようなリソースが作成 / 修正 / 削除されるかを確認しておきましょう。

terraform plan

実行後、以下のように出力されているはずです。「Plan: 7 to add」で分かる通り、main.tfに記載されたVPC関連のリソース(moduleブロック)の作成が計画されていることが分かります。

# 出力結果
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

~~~省略~~~

Plan: 7 to add, 0 to change, 0 to destroy.
  • ③TerraformのコードからAWS環境にリソースを作成する

実行計画を確認できたところで、実際にAWS環境にリソースを作成してみましょう。
次のコマンドを実行してください。
-auto-approve オプションを付けることで、コマンド実行前の確認「yes」の入力が不要になります

terraform apply -auto-approve

実行後、以下のように出力されていれば無事にリソースの作成ができています。

Plan: 7 to add, 0 to change, 0 to destroy.
module.vpc.aws_vpc.default: Creating...
module.vpc.aws_vpc.default: Still creating... [10s elapsed]
module.vpc.aws_vpc.default: Creation complete after 12s [id=vpc-xxxxxxxxxxxxxxxxx]
module.route_table_private_aurora.aws_route_table.default: Creating...
module.private_subnet_aurora_1c.aws_subnet.private: Creating...
module.private_subnet_aurora_1a.aws_subnet.private: Creating...
module.security_group_aurora.aws_security_group.default: Creating...
module.route_table_private_aurora.aws_route_table.default: Creation complete after 1s [id=rtb-xxxxxxxxxxxxxxxxx]
module.private_subnet_aurora_1c.aws_subnet.private: Creation complete after 1s [id=subnet-xxxxxxxxxxxxxxxxx]
module.private_subnet_aurora_1a.aws_subnet.private: Creation complete after 1s [id=subnet-xxxxxxxxxxxxxxxxx]
module.route_table_association_private_aurora_1c.aws_route_table_association.default: Creating...
module.route_table_association_private_aurora_1a.aws_route_table_association.default: Creating...
module.route_table_association_private_aurora_1c.aws_route_table_association.default: Creation complete after 0s [id=rtbassoc-xxxxxxxxxxxxxxxxx]
module.route_table_association_private_aurora_1a.aws_route_table_association.default: Creation complete after 0s [id=rtbassoc-xxxxxxxxxxxxxxxxx]
module.security_group_aurora.aws_security_group.default: Creation complete after 3s [id=sg-xxxxxxxxxxxxxxxxx]

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

念のため、AWSコンソールにアクセスし、作成できているか確認しておきましょう。

【VPC】https://ap-northeast-1.console.aws.amazon.com/vpc/home?region=ap-northeast-1#vpcs:search=rds-handson
【セキュリティグループ】https://ap-northeast-1.console.aws.amazon.com/vpc/home?region=ap-northeast-1#SecurityGroups:search=rds-handson

  • 表示されたVPC ID、セキュリティグループIDをクリック
  • applyした内容で実際に作成されていることが確認できる

4.RDSを作成する

これまでTerraformの初期設定や、RDSを作成するためのVPCを準備してきました。
本題のRDSを作成していきましょう!

4-1. コードの作成

以下のとおりファイルを作成し、コードを書いてみてください。※main.tfは追加で記述

  • main.tf
/* ~~~ 省略(これまでのmain.tfに以下のコードを追加してください) ~~~ */
module "aurora" {
  source = "./modules/rds"

  aurora_subnet_group = {
    subnet_ids  = [module.private_subnet_aurora_1a.private_subnet_id, module.private_subnet_aurora_1c.private_subnet_id]
    name        = "${var.environment}-${local.service}-aurora-subnet-group"
    description = "${var.environment}-${local.service}-aurora-subnet-group"
  }

  aurora_parameter_group = {
    famiily = "aurora-mysql8.0"
    cluster = {
      name        = "${var.environment}-${local.service}-aurora-param-group-mysql-80-cluster"
      description = "${var.environment}-${local.service}-aurora-param-group-mysql-80-cluster"
    }
    instance = {
      name        = "${var.environment}-${local.service}-aurora-param-group-mysql-80"
      description = "${var.environment}-${local.service}-aurora-param-group-mysql-80"
    }
  }

  aurora_config = {
    engine         = "aurora-mysql"
    engine_version = "8.0.mysql_aurora.3.03.1"
    engine_mode    = "provisioned"
  }

  aurora_cluster = {
    identifier = "${var.environment}-${local.service}-aurora-cluster"
    vpc_security_group_ids = [
      module.security_group_aurora.security_group_id
    ]
    availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
    database_name      = "${var.environment}_rds_handson_db"
    master_username    = "${var.environment}_rds_handson_user"
  }

  aurora_instance = {
    identifier         = "${var.environment}-${local.service}-aurora-instance"
    availability_zone  = "ap-northeast-1a"
    instance_class     = "db.t3.medium"
    number_of_instance = 1
  }
}
  • 【クラスター】/modules/rds/cluster.tf
resource "aws_rds_cluster" "default" {
  cluster_identifier = var.aurora_cluster.identifier

  engine         = var.aurora_config.engine
  engine_version = var.aurora_config.engine_version
  engine_mode    = var.aurora_config.engine_mode

  database_name = var.aurora_cluster.database_name

  master_username = var.aurora_cluster.master_username
  master_password = random_password.DB_Password.result

  db_subnet_group_name = aws_db_subnet_group.default.name

  vpc_security_group_ids              = var.aurora_cluster.vpc_security_group_ids
  availability_zones                  = var.aurora_cluster.availability_zones
  port                                = 3306
  db_cluster_parameter_group_name     = aws_rds_cluster_parameter_group.default.name
  backup_retention_period             = 7
  storage_encrypted                   = true
  backtrack_window                    = 0
  preferred_backup_window             = "18:30-19:00"
  preferred_maintenance_window        = "tue:16:00-tue:16:30"
  deletion_protection                 = false # 保護モード(強制的に削除を許可しない or 許可する)
  iam_database_authentication_enabled = false
  skip_final_snapshot                 = true
  copy_tags_to_snapshot               = true
  lifecycle {
    ignore_changes = [
      master_password
    ]
  }
}

resource "random_password" "DB_Password" {
  length  = 25
  special = false
}
  • 【インスタンス】/modules/rds/instance.tf
resource "aws_rds_cluster_instance" "default" {
  count                   = var.aurora_instance.number_of_instance
  cluster_identifier      = aws_rds_cluster.default.id
  db_parameter_group_name = aws_db_parameter_group.default.name
  db_subnet_group_name    = aws_db_subnet_group.default.name

  publicly_accessible        = false
  promotion_tier             = 1
  monitoring_interval        = 0
  auto_minor_version_upgrade = false

  identifier        = "${var.aurora_instance.identifier}-${count.index + 1}" // 配列base0を1に変換
  engine            = var.aurora_config.engine
  engine_version    = var.aurora_config.engine_version
  availability_zone = var.aurora_instance.availability_zone
  instance_class    = var.aurora_instance.instance_class
}
  • 【パラメータ】/modules/rds/parameter.tf
resource "aws_db_subnet_group" "default" {
  name        = var.aurora_subnet_group.name
  description = var.aurora_subnet_group.description
  subnet_ids  = var.aurora_subnet_group.subnet_ids
}

resource "aws_rds_cluster_parameter_group" "default" {
  name        = var.aurora_parameter_group.cluster.name
  description = var.aurora_parameter_group.cluster.description
  family      = var.aurora_parameter_group.famiily
}

resource "aws_db_parameter_group" "default" {
  name        = var.aurora_parameter_group.instance.name
  description = var.aurora_parameter_group.instance.description
  family      = var.aurora_parameter_group.famiily
}

resource "aws_ssm_parameter" "Aurora_Username" {
  name  = "/rds/${var.aurora_cluster.identifier}/username"
  value = aws_rds_cluster.default.master_username
  type  = "SecureString"
}

resource "aws_ssm_parameter" "Aurora_Password" {
  name  = "/rds/${var.aurora_cluster.identifier}/password"
  value = aws_rds_cluster.default.master_password
  type  = "SecureString"
}
  • 【変数定義ファイル】/modules/rds/variables.tf
variable "aurora_subnet_group" {
  type = object({
    name        = string
    description = string
    subnet_ids  = list(string)
  })
}

variable "aurora_parameter_group" {
  type = object({
    famiily = string
    cluster = object({
      name        = string
      description = string
    })
    instance = object({
      name        = string
      description = string
    })
  })
}

variable "aurora_config" {
  type = object({
    engine         = string
    engine_version = string
    engine_mode    = string
  })
}

variable "aurora_cluster" {
  type = object({
    identifier             = string
    vpc_security_group_ids = list(string)
    availability_zones     = list(string)
    database_name          = string
    master_username        = string
  })
}

variable "aurora_instance" {
  type = object({
    identifier         = string
    availability_zone  = string
    instance_class     = string
    number_of_instance = number
  })
}
  • 【出力変数ファイル】/modules/rds/outputs.tf
output "db_host" {
  value = aws_rds_cluster.default.endpoint
}

output "db_port" {
  value = aws_rds_cluster.default.port
}

output "db_name" {
  value = aws_rds_cluster.default.database_name
}

コード補足

  • cluster.tf
    Terraformで作成した全リソースを削除するコマンド `terraform destroy` を実行するとき、保護モードがオンになっているとエラーとなり削除できないため “オフ” にしています。
deletion_protection = false # 保護モード(強制的に削除を許可しない or 許可する)
  • parameter.tf
    将来的にアプリ側のサーバ(ECSなど)からデータベースのユーザー名やパスワードを利用できるようにしておくため、AWS Systems Manager(SSM)を作成し、そこへ値を格納しています。
    ※AWS Secrets Managerを使うケースもあります
resource "aws_ssm_parameter" "Aurora_Username" {
  name  = "/rds/${var.aurora_cluster.identifier}/username"
  value = aws_rds_cluster.default.master_username
  type  = "SecureString"
}

resource "aws_ssm_parameter" "Aurora_Password" {
  name  = "/rds/${var.aurora_cluster.identifier}/password"
  value = aws_rds_cluster.default.master_password
  type  = "SecureString"
}

4-2. TerraformからAWS環境にリソースを作成

RDSの各ファイルを作成したあとは、VPCの作成で解説した「4-2. TerraformからAWS環境にリソースを作成」と同様に、各コマンドを1つずつ実行し、実際にリソースを作成しましょう!

terraform init
terraform plan
terraform apply -auto-approve

実行後、以下のように出力されていれば無事にリソースの作成ができています。
※ 作成完了まで数分以上かかります

Plan: 8 to add, 1 to change, 0 to destroy.
module.aurora.random_password.DB_Password: Creating...
module.aurora.random_password.DB_Password: Creation complete after 0s [id=none]
module.aurora.aws_rds_cluster_parameter_group.default: Creating...
~~~省略~~~

Apply complete! Resources: 8 added, 1 changed, 0 destroyed.

念のため、AWSコンソールにアクセスし、作成できているか確認しておきましょう。

【RDS】https://ap-northeast-1.console.aws.amazon.com/rds/home?region=ap-northeast-1#databases:

  • applyした内容で実際に作成されていることが確認できる

5.リソースを削除する

Terraformでリソースを作成してきました。必要がなければ、稼働している間の料金がかかってしまわないように、全て削除してしまいましょう。以下のコマンドを実行してください。

terraform destroy -auto-approve

実行後、次のように表示されていれば無事に削除できています!

Plan: 0 to add, 0 to change, 15 to destroy.
module.route_table_association_private_aurora_1a.aws_route_table_association.default: Destroying... [id=rtbassoc-xxxxxxxxxxxxxxxxx]
module.route_table_association_private_aurora_1c.aws_route_table_association.default: Destroying... [id=rtbassoc-xxxxxxxxxxxxxxxxx]
module.aurora.aws_ssm_parameter.Aurora_Username: Destroying... [id=/rds/rds-handson-aurora-cluster/username]
~~~省略~~~

Destroy complete! Resources: 15 destroyed.

あとがき

いかがでしたでしょうか?Terraformを使うとインフラをコードで管理することができ、構築と削除をコマンドからすばやく行うことができます!

また、Terraform Moduleを使うことで、必要なリソースを好きな分だけ、変数を渡して簡単にカスタマイズ & 再利用することができます。

以上、この記事が皆様のお役に立てれば幸いです。それでは。

Related posts