🗂

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

19 min read

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

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

$ terraform -v
Terraform v1.0.11
on windows_amd64
+ provider registry.terraform.io/hashicorp/aws v3.66.0
+ provider registry.terraform.io/hashicorp/tls v3.1.0

急いでる人向け

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

コード

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

ディレクトリ構成

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

.
├── 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のネットワークに関するリソースを定義します。

./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) }

rds

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

./rds/main.tf
#--------------------------------------------------------------
# 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
egress {
  from_port   = 0
  to_port     = 0
  protocol    = "-1"
  cidr_blocks = ["0.0.0.0/0"]
}
tags = {
  Name = "${var.app_name}-sg of mysql"
}
}

resource "aws_security_group_rule" "rds-sg-rule" {
security_group_id = aws_security_group.rds-sg.id
type              = "ingress"
from_port         = 3306
to_port           = 3306
protocol          = "tcp"
cidr_blocks       = ["10.0.0.0/16"]
}

#--------------------------------------------------------------
# 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
}

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

resource "aws_db_instance" "db" {
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
}
./rds/outputs.tf
output "db_address" { value = aws_db_instance.db.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" }

ec2

EC2のリソースを定義します。
RDSには直接接続することができないので、EC2を踏み台にする形でRDSに接続します。
EC2にはセッションマネージャーを使用して接続するため、それ用のIAMロールをアタッチしたEC2を構築します。

./ec2/main.tf
#--------------------------------------------------------------
# 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

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

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Allow all outbound traffic.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

#--------------------------------------------------------------
# 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        = aws_iam_instance_profile.systems_manager.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
}

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

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

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

resource "aws_iam_role" "role" {
  name               = "MyRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy" "systems_manager" {
  arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role_policy_attachment" "default" {
  role       = aws_iam_role.role.name
  policy_arn = data.aws_iam_policy.systems_manager.arn
}

resource "aws_iam_instance_profile" "systems_manager" {
  name = "MyInstanceProfile"
  role = aws_iam_role.role.name
}
./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" {}

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" {}
variable "db_password" {}

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 "ec2" {
  source = "./module/ec2"

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

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

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

tfvar

変数を定義するファイルを作成します。
ここはお好きなように変えていただいて構いません。
RDSのパスワードなどはベタ書きしていますが、本番で構築する際などは注意してください。

./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"
db_password = "jg405jyWoeWr8gWF49tFj08q2p3jgtEE8w4U58"    # [要変更]

構築

手順は以下の通りです。

  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. セッションマネージャーでEC2にログイン

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

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

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

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

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

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

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

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

最後に

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

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

参考

Discussion

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