🗂

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

2021/11/23に公開約20,000字

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

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

$ terraform -v
Terraform v1.0.11
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
│   ├── iam
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── network
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── rds
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── secrets
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── terraform.tfvars

Network

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

./network/main.tf
#--------------------------------------------------------------
# VPC
#--------------------------------------------------------------

resource "aws_vpc" "vpc" {
  cidr_block           = var.vpc_cidr
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "${var.name}-vpc"
  }
}

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

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "${var.name}-igw"
  }
}

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

resource "aws_subnet" "pub-sub" {
  count = length(var.pub_cidrs)

  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = element(var.pub_cidrs, count.index)
  availability_zone       = element(var.azs, count.index)
  map_public_ip_on_launch = true
  tags = {
    Name = "${var.name}-pub-${element(var.azs, count.index)}"
  }
}

resource "aws_route_table" "pub-rtb" {
  vpc_id = aws_vpc.vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
  tags = {
    Name = "${var.name}-pub-rtb"
  }
}

resource "aws_route_table_association" "pub-rtb-as" {
  count = length(var.pub_cidrs)

  subnet_id      = element(aws_subnet.pub-sub.*.id, count.index)
  route_table_id = aws_route_table.pub-rtb.id
}

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

resource "aws_subnet" "pri-sub" {
  count = length(var.pri_cidrs)

  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(var.pri_cidrs, count.index)
  availability_zone = element(var.azs, count.index)
  tags = {
    Name = "${var.name}-pri-${element(var.azs, count.index)}"
  }
}

resource "aws_route_table" "pri-rtb" {
  vpc_id = aws_vpc.vpc.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat-gw.id
  }
  tags = {
    Name = "${var.name}-pri-rtb"
  }
}

resource "aws_route_table_association" "pri-rtb-as" {
  count = length(var.pri_cidrs)

  subnet_id      = element(aws_subnet.pri-sub.*.id, count.index)
  route_table_id = aws_route_table.pri-rtb.id
}

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

resource "aws_eip" "nat-eip" {
  vpc        = true
  depends_on = [aws_internet_gateway.igw]
}

resource "aws_nat_gateway" "nat-gw" {
  allocation_id = aws_eip.nat-eip.id
  subnet_id     = aws_subnet.pub-sub[0].id
  depends_on    = [aws_internet_gateway.igw]
}
./network/outputs.tf
output "vpc_id" { value = aws_vpc.vpc.id }

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

output "pub_subnet_ids" { value = aws_subnet.pub-sub.*.id }

output "pri_subnet_ids" { value = aws_subnet.pri-sub.*.id }
./network/variables.tf
variable "name" {}

variable "vpc_cidr" {}

variable "azs" {}

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

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

IAM

EC2のIAMリソースを定義します。
EC2にはセッションマネージャーを使用して接続するため、それ用のIAMロールをアタッチします。

./iam/main.tf
#--------------------------------------------------------------
# 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.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.name}-ec2-instance-profile"
  role = aws_iam_role.ec2.name
}
./iam/outputs.tf
output "iam_role_arn" { value = aws_iam_role.ec2.arn }

output "iam_instance_profile_name" { value = aws_iam_instance_profile.systems-manager.name }
./iam/variables.tf
variable "name" {}

EC2

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

./ec2/main.tf
#--------------------------------------------------------------
# EC2
#--------------------------------------------------------------

resource "aws_instance" "ec2" {
  ami                         = "ami-011facbea5ec0363b"
  instance_type               = "t2.micro"
  subnet_id                   = var.pub_subnet_ids[0]
  associate_public_ip_address = "true"
  key_name                    = aws_key_pair.ec2-key.key_name
  vpc_security_group_ids      = [aws_security_group.ec2-sg.id]
  iam_instance_profile        = var.iam_instance_profile_name
}

resource "aws_eip" "ec2-eip" {
  vpc      = true
  instance = aws_instance.ec2.id
}

#--------------------------------------------------------------
# Key Pair
#--------------------------------------------------------------

resource "aws_key_pair" "ec2-key" {
  key_name   = "common-ssh"
  public_key = tls_private_key._.public_key_openssh
}

resource "tls_private_key" "_" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

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

resource "aws_security_group" "ec2-sg" {
  name = "${var.app_name}-ec2-sg"

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

  dynamic "ingress" {
    for_each = { for i in var.ingress_config : i.port => i }

    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
./ec2/outputs.tf
output "private_key_pem" {
  value     = tls_private_key._.private_key_pem
  sensitive = true
}

output "public_key_openssh" {
  value = tls_private_key._.public_key_openssh
}
./ec2/variables.tf
variable "app_name" {}

variable "vpc_id" {}

variable "pub_subnet_ids" {}

variable "iam_instance_profile_name" {}

variable "ingress_config" {
  type = list(object({
    port        = string
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      port        = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      port        = 3306
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/16"]
    }
  ]
  description = "list of ingress config"
}

Secrets Manager

DBのパスワード管理に利用するSecrets Managerのリソースを定義します。
注意点としては、DBのパスワードはSecrets Managerで管理しますが、terraform.tfstateファイルにはランダムで生成したパスワードが残っています。
terraform.tfstateファイル自体が秘匿情報なので、本番環境では暗号化しアクセスが制限されたS3などで管理する必要があります。

./secrets/main.tf
#--------------------------------------------------------------
# Secrets Manager
#--------------------------------------------------------------

resource "random_password" "db-password" {
  length           = 16
  special          = true
  override_special = "_!%^"
}

resource "aws_secretsmanager_secret" "db-password" {
  name = "${var.name}-db-password"
}

resource "aws_secretsmanager_secret_version" "db-password" {
  secret_id     = aws_secretsmanager_secret.db-password.id
  secret_string = random_password.db-password.result
}

data "aws_secretsmanager_secret" "db-password" {
  name       = "${var.name}-db-password"
  depends_on = [aws_secretsmanager_secret.db-password]
}

data "aws_secretsmanager_secret_version" "db-password" {
  secret_id  = data.aws_secretsmanager_secret.db-password.id
  depends_on = [aws_secretsmanager_secret_version.db-password]
}
./secrets/outputs.tf
output "db_password" { value = data.aws_secretsmanager_secret_version.db-password.secret_string }
./secrets/variables.tf
variable "name" {}

RDS

RDSのリソースを定義していきます。
今回使用するデータベースはMySQLです。

./rds/main.tf
#--------------------------------------------------------------
# RDS
#--------------------------------------------------------------

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
  password               = var.db_password
  skip_final_snapshot    = true
  vpc_security_group_ids = [aws_security_group.rds-sg.id]
  db_subnet_group_name   = aws_db_subnet_group.rds-subnet-group.name
}

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

resource "aws_db_subnet_group" "rds-subnet-group" {
  name        = var.db_name
  description = "rds subnet group for ${var.db_name}"
  subnet_ids  = var.pri_subnet_ids
}

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

resource "aws_security_group" "rds-sg" {
  name        = "${var.app_name}-rds-sg"
  description = "RDS service security group for ${var.app_name}"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.app_name}-rds-sg"
  }
}
./rds/outputs.tf
output "db_address" { value = aws_db_instance.rds.address }
./rds/variables.tf
variable "app_name" {}

variable "db_name" {}

variable "db_username" {}

variable "db_password" {}

variable "vpc_id" {}

variable "pri_subnet_ids" {}

variable "engine" { default = "mysql" }

variable "engine_version" { default = "8.0.20" }

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

main

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

./main.tf
variable "region" {}
variable "name" {}
variable "vpc_cidr" {}
variable "azs" {}
variable "public_subnet_cidrs" { type = list(string) }
variable "private_subnet_cidrs" { type = list(string) }
variable "db_name" {}
variable "db_username" {}

terraform {
  required_version = "=v1.0.11"
}

provider "aws" {
  region = var.region
}

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

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

module "iam" {
  source = "./module/iam"

  name = var.name
}

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

  app_name                  = var.name
  vpc_id                    = module.network.vpc_id
  pub_subnet_ids            = module.network.pub_subnet_ids
  iam_instance_profile_name = module.iam.iam_instance_profile_name
}

module "secrets" {
  source = "./module/secrets"

  name = var.name
}

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

  app_name       = var.name
  db_name        = var.db_name
  db_username    = var.db_username
  db_password    = module.secrets.db_password
  vpc_id         = module.network.vpc_id
  pri_subnet_ids = module.network.pri_subnet_ids
}

tfvar

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

./terraform.tfvars
#--------------------------------------------------------------
# General
#--------------------------------------------------------------

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

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

vpc_cidr             = "10.0.0.0/16"
azs                  = ["ap-northeast-1a", "ap-northeast-1c"]
public_subnet_cidrs  = ["10.0.0.0/24", "10.0.1.0/24"]
private_subnet_cidrs = ["10.0.2.0/24", "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. セッションマネージャーでEC2にログイン
  2. sudo yum install mysqlを実行しMySQLをインストール
  3. mysql -u admin -p -h [RDSのエンドポイント]を実行後、パスワードを入力しMySQLにログイン

1. Secrets Managerにアクセスし作成されたDBのパスワードを控える

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

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

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

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

3. sudo yum install mysqlを実行しMySQLをインストール

sudo yum install mysqlコマンドを実行してMySQLをインストールします。
MySQLインストール

4. 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

ログインするとコメントできます