🗂

Terraform で RDS 構築して接続確認するまで

2021/11/24に公開

※2023/7/9更新 サンプル用にコードを簡素化しました。
※2023/12/30更新 Amazon RDS と AWS Secrets Manager の統合に対応しました。

この記事ではAmazon RDSをTerraformで構築し、EC2のセッションマネージャーを介してRDSに接続する方法について書いていこうと思います。

TerraformのバージョンとOSは以下の通りです。

% terraform -v
Terraform v1.5.5
on darwin_amd64

急いでる人向け

下記URLにコードがありますので、READMEの通りにすればできると思います。
suganuma3510/terraform-sample

コード

RDSはMySQLを使用します。
また今回はTerrafromのmoduleを使用して実装していきますが、ここでは細かいmoduleの使い方や各リソースの説明は省きます。

ディレクトリ構成

実装後のディレクトリ構成は以下の通りです。
実際に手を動かしながら実装する方は、以下のフォルダ構成を参考にファイルを作成すると詰まらず進めると思います。
各moduleフォルダにそれぞれmain.tfoutputs.tfvariables.tfを作成して、リソースを定義していきます。

.
├── README.md
├── main.tf
├── module
│   ├── ec2
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── network
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── rds
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── terraform.tfvars

Network

AWSのネットワークに関するリソースを定義します。

Terraformコード
./network/main.tf
#--------------------------------------------------------------
# VPC
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc
resource "aws_vpc" "default" {
  cidr_block           = var.vpc_cidr
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "${var.name}-vpc"
  }
}

#--------------------------------------------------------------
# Public subnet
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet
resource "aws_subnet" "public" {
  for_each = var.pub_cidrs

  vpc_id                  = aws_vpc.default.id
  cidr_block              = each.value
  availability_zone       = "${var.region}${each.key}"
  map_public_ip_on_launch = true
  tags = {
    Name = "${var.name}-public-${each.key}"
  }
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.default.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.default.id
  }
  tags = {
    Name = "${var.name}-public-rtb"
  }
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association
resource "aws_route_table_association" "public" {
  for_each = aws_subnet.public

  subnet_id      = each.value.id
  route_table_id = aws_route_table.public.id
}

#--------------------------------------------------------------
# Private subnet
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet
resource "aws_subnet" "private" {
  for_each = var.pri_cidrs

  vpc_id                  = aws_vpc.default.id
  cidr_block              = each.value
  availability_zone       = "${var.region}${each.key}"
  map_public_ip_on_launch = true
  tags = {
    Name = "${var.name}-private-${each.key}"
  }
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.default.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.default.id
  }
  tags = {
    Name = "${var.name}-private-rtb"
  }
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association
resource "aws_route_table_association" "private" {
  for_each = aws_subnet.private

  subnet_id      = each.value.id
  route_table_id = aws_route_table.private.id
}

#--------------------------------------------------------------
# Internet Gateway
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway
resource "aws_internet_gateway" "default" {
  vpc_id = aws_vpc.default.id
  tags = {
    Name = "${var.name}-igw"
  }
}

#--------------------------------------------------------------
# NAT
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip
resource "aws_eip" "nat" {
  domain     = "vpc"
  depends_on = [aws_internet_gateway.default]
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/nat_gateway
resource "aws_nat_gateway" "default" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public["a"].id
  depends_on    = [aws_internet_gateway.default]
}
./network/outputs.tf
output "vpc_id" {
  value = aws_vpc.default.id
}

output "vpc_cidr" {
  value = aws_vpc.default.cidr_block
}

output "pub_subnet_ids" {
  value = [for value in aws_subnet.public : value.id]
}

output "pri_subnet_ids" {
  value = [for value in aws_subnet.private : value.id]
}

output "pub_subnet_cidr_blocks" {
  value = [for value in aws_subnet.public : value.cidr_block]
}

output "pri_subnet_cidr_blocks" {
  value = [for value in aws_subnet.private : value.cidr_block]
}
./network/variables.tf
variable "name" {}

variable "region" {}

variable "vpc_cidr" {}

variable "pub_cidrs" {
  type = map(string)
}

variable "pri_cidrs" {
  type = map(string)
}

EC2

EC2のリソースを定義します。
RDSには直接接続することができないので、セッションマネージャーを介しEC2を踏み台にする形でRDSに接続します。

Terraformコード
./ec2/main.tf
#--------------------------------------------------------------
# EC2
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami
data "aws_ami" "amazon2_amd64" {
  most_recent = true
  filter {
    name   = "name"
    values = ["amzn2-ami-kernel*"]
  }
  filter {
    name   = "architecture"
    values = ["x86_64"]
  }
  owners = ["amazon"]
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
resource "aws_instance" "ec2" {
  ami                         = data.aws_ami.amazon2_amd64.id
  instance_type               = "t2.micro"
  subnet_id                   = var.subnet_ids[0]
  associate_public_ip_address = "false"
  vpc_security_group_ids      = [aws_security_group.ec2.id]
  iam_instance_profile        = aws_iam_instance_profile.systems-manager.name

  user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y mysql
              EOF
}

#--------------------------------------------------------------
# Security group
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group
resource "aws_security_group" "ec2" {
  name = "${var.app_name}-ec2-sg"

  description = "EC2 service security group for ${var.app_name}"
  vpc_id      = var.vpc_id
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule
resource "aws_security_group_rule" "ec2_egress_all" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.ec2.id
}

#--------------------------------------------------------------
# IAM Role
#--------------------------------------------------------------

data "aws_iam_policy_document" "ec2" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

data "aws_iam_policy" "systems-manager" {
  arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role" "ec2" {
  name               = "${var.app_name}-ec2-role"
  assume_role_policy = data.aws_iam_policy_document.ec2.json
}

resource "aws_iam_role_policy_attachment" "ec2" {
  role       = aws_iam_role.ec2.name
  policy_arn = data.aws_iam_policy.systems-manager.arn
}

resource "aws_iam_instance_profile" "systems-manager" {
  name = "${var.app_name}-ec2-instance-profile"
  role = aws_iam_role.ec2.name
}
./ec2/outputs.tf
output "ec2_security_group_id" {
  value = aws_security_group.ec2.id
}
./ec2/variables.tf
variable "app_name" {}

variable "vpc_id" {}

variable "subnet_ids" {}

RDS

RDSのリソースを定義していきます。
今回使用するデータベースはMySQLです。
DBのパスワードはmanage_master_user_passwordを指定して、Secrets Managerへ自動作成します。
こうすることでデプロイ後、terraform.tfstateにパスワードが残らないため安全です。

Terraformコード
./rds/main.tf
#--------------------------------------------------------------
# RDS
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance
resource "aws_db_instance" "rds" {
  allocated_storage           = 10
  storage_type                = "gp2"
  engine                      = var.engine
  engine_version              = var.engine_version
  instance_class              = var.db_instance
  identifier                  = var.db_name
  username                    = var.db_username
  manage_master_user_password = true
  skip_final_snapshot         = true
  vpc_security_group_ids      = [aws_security_group.rds.id]
  db_subnet_group_name        = aws_db_subnet_group.rds.name
}

#--------------------------------------------------------------
# Subnet group
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_subnet_group
resource "aws_db_subnet_group" "rds" {
  name        = var.app_name
  description = "rds subnet group for ${var.db_name}"
  subnet_ids  = var.subnet_ids
}

#--------------------------------------------------------------
# Security group
#--------------------------------------------------------------

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group
resource "aws_security_group" "rds" {
  name        = "${var.app_name}-rds-sg"
  description = "RDS service security group for ${var.app_name}"
  vpc_id      = var.vpc_id

  tags = {
    Name = "${var.app_name}-rds-sg"
  }
}

# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule
resource "aws_security_group_rule" "rds_ingress_mysql" {
  type              = "ingress"
  from_port         = 3306
  to_port           = 3306
  protocol          = "tcp"
  cidr_blocks       = var.subnet_cidr_blocks
  security_group_id = aws_security_group.rds.id
}

resource "aws_security_group_rule" "rds_ingress_app" {
  count = length(var.source_security_group_ids)

  type                     = "ingress"
  from_port                = 0
  to_port                  = 0
  protocol                 = "-1"
  source_security_group_id = element(var.source_security_group_ids, count.index)
  security_group_id        = aws_security_group.rds.id
}

resource "aws_security_group_rule" "rds_egress_all" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.rds.id
}
./rds/outputs.tf
output "db_instance_id" {
  value = aws_db_instance.rds.id
}

output "db_address" {
  value = aws_db_instance.rds.address
}

output "db_security_group_id" {
  value = aws_security_group.rds.id
}
./rds/variables.tf
variable "app_name" {}

variable "db_name" {}

variable "db_username" {}

variable "vpc_id" {}

variable "subnet_ids" {}

variable "subnet_cidr_blocks" {
  type = list(string)
}

variable "source_security_group_ids" {
  type = list(string)
}

variable "engine" {
  default = "mysql"
}

variable "engine_version" {
  default = "8.0.33"
}

variable "db_instance" {
  default = "db.t2.micro"
}

main

次にそれぞれ定義したモジュールを呼び出すためのルートモジュールを定義します。
ここではmain.tfに全てまとめて呼び出しています。

Terraformコード
./main.tf
variable "region" {}
variable "name" {}
variable "vpc_cidr" {}
variable "public_subnet_cidrs" { type = map(string) }
variable "private_subnet_cidrs" { type = map(string) }
variable "db_name" {}
variable "db_username" {}

terraform {
  required_version = "=v1.5.5"
}

provider "aws" {
  region = var.region
}

module "network" {
  source = "./module/network"

  name      = var.name
  region    = var.region
  vpc_cidr  = var.vpc_cidr
  pub_cidrs = var.public_subnet_cidrs
  pri_cidrs = var.private_subnet_cidrs
}

module "jump-ec2" {
  source = "./module/ec2"

  app_name   = var.name
  vpc_id     = module.network.vpc_id
  subnet_ids = module.network.pri_subnet_ids
}

module "rds" {
  source = "./module/rds"

  app_name                  = var.name
  db_name                   = var.db_name
  db_username               = var.db_username
  vpc_id                    = module.network.vpc_id
  subnet_ids                = module.network.pri_subnet_ids
  subnet_cidr_blocks        = module.network.pri_subnet_cidr_blocks
  source_security_group_ids = [module.jump-ec2.ec2_security_group_id]
}

tfvar

変数を定義するファイルを作成します。
ここはお好きなように変えていただいて構いません。

Terraformコード
./terraform.tfvars
#--------------------------------------------------------------
# General
#--------------------------------------------------------------

name   = "rds-sample"
region = "ap-northeast-1"

#--------------------------------------------------------------
# Network
#--------------------------------------------------------------

vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = {
  "a" = "10.0.0.0/24",
  "c" = "10.0.1.0/24"
}
private_subnet_cidrs = {
  "a" = "10.0.2.0/24",
  "c" = "10.0.3.0/24"
}

#--------------------------------------------------------------
# RDS
#--------------------------------------------------------------

db_name     = "testdb"
db_username = "admin"

構築

手順は以下の通りです。

  1. terraform init して初期化
  2. terraform validate してエラーチェック
  3. terraform plan して構成チェック
  4. terraform apply してリソース作成

1. terraform init して初期化

すでにinitしていた方も実行する必要があります。
moduleを使用している場合はterraform initterraform getを実行することでルートモジュールに記載されているモジュールをダウンロードおよび更新することができます。

$ terraform init
Initializing modules...
- ec2 in module/ec2
- network in module/network
- rds in module/rds

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Finding latest version of hashicorp/tls...
- Installing hashicorp/aws v3.66.0...
- Installed hashicorp/aws v3.66.0 (signed by HashiCorp)
- Installing hashicorp/tls v3.1.0...
- Installed hashicorp/tls v3.1.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

2. terraform validate してエラーチェック

$ terraform validate
Success! The configuration is valid.

3. terraform plan して構成チェック

$ terraform plan

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:

  # module.ec2.aws_eip.ec2-eip will be created
  + resource "aws_eip" "ec2-eip" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = (known after apply)
~~~~~~~~~~~~~~~長いので割愛~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      + self                     = false
      + source_security_group_id = (known after apply)
      + to_port                  = 3306
      + type                     = "ingress"
    }

Plan: 26 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

4. terraform apply してリソース作成

Plan: 26 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.ec2.tls_private_key._: Creating...
module.network.aws_vpc.vpc: Creating...
~~~~~~~~~~~~~~~長いので割愛~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
module.rds.aws_db_instance.db: Still creating... [3m20s elapsed]
module.rds.aws_db_instance.db: Still creating... [3m30s elapsed]
module.rds.aws_db_instance.db: Creation complete after 3m37s [id=testdb]

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

RDS接続

手順は以下の通りです。

  1. AWS Secrets ManagerでDBのパスワードを取得(後述のDBログイン時に使用します)
  2. セッションマネージャーでEC2にログイン
  3. mysql -u admin -p -h [RDSのエンドポイント]を実行後、DBパスワードを入力しMySQLにログイン

1. AWS Secrets ManagerでDBのパスワードを取得

マネジメントコンソールでAWS Secrets Manager > シークレット > rdsXXXXX(自動で作成されたシークレットの名称)の順でアクセスします。
シークレットの値を取得するを押下すると作成されたパスワードが表示されます。(後述のDBログイン時に使用します)

2. セッションマネージャーでEC2にログイン

AWSのマネジメントコンソールでEC2に接続します。
作成したEC2インスタンスにチェックを入れ、接続ボタンを押下します。
EC2接続1

次にセッションマネージャーから接続の順で押下すると、コンソール画面に遷移します。
EC2接続2

3. mysql -u admin -p -h [RDSのエンドポイント]を実行後、パスワードを入力しMySQLにログイン

エンドポイントはマネジメントコンソールの以下の場所で確認することができます。
RDS > データベース > [作成したDB名]
エンドポイント確認

そして、コンソール画面でmysql -u admin -p -h [RDSのエンドポイント]を実行します。
パスワードを聞かれますが、先ほどSecrets Managerから控えたパスワードを入力します。
これでRDSに接続することができると思います。
InkedDB接続_LI

念のため、マネジメントコンソールでRDSのモニタリングを確認してみると、DB接続(カウント)が0から1に変わっていることが分かると思います。
Inked接続確認_LI

最後に

terraform destroyを実行しリソースを削除します。

以上になります。見ていただきありがとうございました。

参考

Discussion