TerraformでRDS Proxyを作成

やる事
タイトルの通りでTerraformでRDS Proxyを作成して、接続確認を行うところまで。
利用するTerraformのバージョン
- Terraform
0.14.6
- terraform-provider-aws
3.39.0
terraform-provider-aws のバージョンはこの記事を書いた 2021年5月11日時点で最新バージョン。
最新バージョンである必要はないがある程度新しいバージョンでないとRDS Proxyに対応していないので、基本的に最新安定版を利用する事を推奨。
はじめに
以下のPRが最終アウトプット。
これを見ただけで、理解出来る場合はこれ以降の章を読む必要はなし。
後で出てくるTerraformのサンプルコードは上記PRの内容を一部抜粋しているだけです。
事前準備
RDS Clusterの作成
コードの内容は割愛。(詳しくはPRを参照)
接続確認用のEC2インスタンス
キーペアの登録はしたくないので、SessionManagerを使って接続確認出来るようにする。
// 接続確認用のEC2の為のIAMロール
data "aws_iam_policy_document" "assume_bastion_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "bastion_role" {
name = lookup(var.bastion, "${terraform.workspace}.name", var.bastion["default.name"])
assume_role_policy = data.aws_iam_policy_document.assume_bastion_role.json
}
data "aws_iam_policy" "systems_manager" {
arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_role_policy_attachment" "bastion_role_attachment" {
role = aws_iam_role.bastion_role.name
policy_arn = data.aws_iam_policy.systems_manager.arn
}
resource "aws_iam_instance_profile" "bastion_instance_profile" {
name = lookup(var.bastion, "${terraform.workspace}.name", var.bastion["default.name"])
role = aws_iam_role.bastion_role.name
}
resource "aws_security_group" "bastion" {
name = "${terraform.workspace}-${lookup(
var.bastion,
"${terraform.workspace}.name",
var.bastion["default.name"],
)}"
description = "Security Group to ${lookup(
var.bastion,
"${terraform.workspace}.name",
var.bastion["default.name"],
)}"
vpc_id = var.vpc["vpc_id"]
tags = {
Name = "${terraform.workspace}-${lookup(
var.bastion,
"${terraform.workspace}.name",
var.bastion["default.name"],
)}"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "bastion" {
ami = lookup(
var.bastion,
"${terraform.workspace}.ami",
var.bastion["default.ami"],
)
associate_public_ip_address = false
instance_type = lookup(
var.bastion,
"${terraform.workspace}.instance_type",
var.bastion["default.instance_type"],
)
iam_instance_profile = aws_iam_instance_profile.bastion_instance_profile.name
ebs_block_device {
device_name = "/dev/xvda"
volume_type = lookup(
var.bastion,
"${terraform.workspace}.volume_type",
var.bastion["default.volume_type"],
)
volume_size = lookup(
var.bastion,
"${terraform.workspace}.volume_size",
var.bastion["default.volume_size"],
)
}
subnet_id = var.vpc["subnet_private_3"]
vpc_security_group_ids = [aws_security_group.bastion.id]
tags = {
Name = "${terraform.workspace}-${lookup(
var.bastion,
"${terraform.workspace}.name",
var.bastion["default.name"],
)}"
}
lifecycle {
ignore_changes = all
}
}
variable "bastion" {
type = map(string)
default = {
"default.name" = "prod-bastion"
"stg.name" = "stg-bastion"
"dev.name" = "dev-bastion"
"default.ami" = "ami-0ca38c7440de1749a"
"default.instance_type" = "t2.micro"
"default.volume_type" = "gp2"
"default.volume_size" = "30"
}
}
Session Manager を使ってインスタンスに接続。
MySQLクライアントを利用可能な状態にしておく。
# package最新化
sudo yum update
# MySQLと衝突するので最初から入っている mariadb-libs を削除
sudo yum remove mariadb-libs
# MySQL Yum Repositoryを追加
sudo yum localinstall https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
# MySQL Clientを追加
sudo yum install --enablerepo=mysql80-community mysql-community-client
RDS Proxyを作成
以下の通りです。
// RDS Proxy用のIAM
data "aws_iam_policy_document" "rds_proxy_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["rds.amazonaws.com"]
}
}
}
resource "aws_iam_role" "rds_proxy_role" {
name = "${terraform.workspace}-rds-proxy-role"
assume_role_policy = data.aws_iam_policy_document.rds_proxy_assume_role.json
}
resource "aws_iam_role_policy" "rds_proxy_policy" {
name = "${terraform.workspace}-rds-proxy-policy"
role = aws_iam_role.rds_proxy_role.id
policy = file("../../../../modules/aws/rds/files/policy/rds-proxy-policy.json")
}
// 接続確認用のLambda関数
resource "aws_security_group" "vpc_lambda" {
name = "${terraform.workspace}-vpc-lambda"
description = "vpc lambda security group"
vpc_id = var.vpc["vpc_id"]
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "rds_proxy" {
name = "${terraform.workspace}-rds-proxy"
description = "rds proxy security group"
vpc_id = var.vpc["vpc_id"]
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group_rule" "rds_proxy_from_bastion_server" {
security_group_id = aws_security_group.rds_proxy.id
type = "ingress"
from_port = "3306"
to_port = "3306"
protocol = "tcp"
source_security_group_id = aws_security_group.bastion.id
}
resource "aws_security_group_rule" "rds_proxy_from_vpc_lambda" {
security_group_id = aws_security_group.rds_proxy.id
type = "ingress"
from_port = "3306"
to_port = "3306"
protocol = "tcp"
source_security_group_id = aws_security_group.vpc_lambda.id
}
resource "aws_db_proxy" "rds_proxy" {
name = "${terraform.workspace}-rds-proxy"
debug_logging = false
engine_family = "MYSQL"
idle_client_timeout = 1800
require_tls = false
role_arn = aws_iam_role.rds_proxy_role.arn
vpc_security_group_ids = [aws_security_group.rds_proxy.id]
vpc_subnet_ids = [
var.vpc["subnet_private_1"],
var.vpc["subnet_private_2"],
var.vpc["subnet_private_3"],
]
auth {
auth_scheme = "SECRETS"
iam_auth = "DISABLED"
secret_arn = aws_secretsmanager_secret.rds_connection.arn
}
depends_on = [aws_rds_cluster.rds_cluster, aws_secretsmanager_secret_version.rds_connection]
}
resource "aws_db_proxy_default_target_group" "rds_proxy" {
db_proxy_name = aws_db_proxy.rds_proxy.name
connection_pool_config {
connection_borrow_timeout = 120
max_connections_percent = 100
max_idle_connections_percent = 50
}
}
resource "aws_db_proxy_target" "rds_proxy" {
db_cluster_identifier = aws_rds_cluster.rds_cluster.id
db_proxy_name = aws_db_proxy.rds_proxy.name
target_group_name = aws_db_proxy_default_target_group.rds_proxy.name
}
resource "aws_db_proxy_endpoint" "read_only" {
db_proxy_name = aws_db_proxy.rds_proxy.name
db_proxy_endpoint_name = "${terraform.workspace}-read-only"
vpc_subnet_ids = [
var.vpc["subnet_private_1"],
var.vpc["subnet_private_2"],
var.vpc["subnet_private_3"],
]
vpc_security_group_ids = [aws_security_group.rds_proxy.id]
target_role = "READ_ONLY"
depends_on = [aws_db_proxy.rds_proxy]
}
// RDS ClusterのセキュリティグループにRDS Proxyから3306ポートで通信出来る設定を追加
resource "aws_security_group_rule" "rds_from_rds_proxy" {
security_group_id = aws_security_group.rds_cluster.id
type = "ingress"
from_port = "3306"
to_port = "3306"
protocol = "tcp"
source_security_group_id = aws_security_group.rds_proxy.id
depends_on = [aws_security_group.rds_proxy]
}
modules/aws/rds/files/policy/rds-proxy-policy.json
の中身は下記の通りです。
RDS ProxyはRDSへの認証情報をSecretManagerから取得しているので、SecretManagerにアクセス出来る権限を与えています。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetResourcePolicy",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
],
"Resource": "arn:aws:secretsmanager:*:*:*"
}
]
}
aws_db_proxy_endpoint
で読み取り専用エンドポイントを作成している。
実運用を考慮するなら負荷分散の為、読み取り専用エンドポイントは必須だと思う。
ちなみにコンソールからだとRDS Proxyを作成する際に読み取り専用エンドポイントを作成するかどうかチェックを付けられるのだが、Terraformの aws_db_proxy
リソースだとそれらしい設定はなかったので、別途作成している。
接続確認方法
接続確認用のEC2インスタンスからmysqlコマンドで接続出来るか確認するのが一番楽なので、その方法で確認します。
AWSコンソール上から作成したRDS Proxyを確認しエンドポイントをコピーする。
mysql -h dev-rds-proxy.proxy-aaaaaaaaaaaa.ap-northeast-1.rds.amazonaws.com -u myusername -p
mysql -h dev-read-only.endpoint.proxy-aaaaaaaaaaaa.ap-northeast-1.rds.amazonaws.com -u myusername -p
ハマりポイント
RDS Proxyに接続出来ない
下記の内容を元に解決。
以下のコマンドで接続出来ない原因を調べる事が出来る。
aws rds describe-db-proxy-targets --db-proxy-name $作成したRDS Proxyの名前
レスポンスの TargetHealth
の中身を見ると接続出来ない原因が特定可能。
下記の記事に詳しい見方が載っていたので参考にさせてもらいました。
https://qiita.com/mmmm/items/0486f6f4be44109b5e32
ちなみに自分はRDS Proxy → RDS Cluster への接続許可を出すのを忘れていて、接続出来ない状態になっていました。
接続確認コマンドはAWS CLIが使える必要があるので下記を参考に認証情報を設定しておくと良さそう。
AWS Access Key ID [None]: AAAAAAAAAAAAAAAAAAAA(アクセスキーを入力)
AWS Secret Access Key [None]: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB(アクセスキーシークレットを入力)
Default region name [None]: ap-northeast-1
Default output format [None]: json
これらのコマンドがないとデバッグに時間がかかってしまうので、最初は接続確認用のEC2インスタンスを構築したほうが良さそう。
SecretManager、一度削除すると同じ名前を7日間定義出来ない
詳細は下記の通り。
色々と試行錯誤しているうちにリソースをスッキリさせたくなって一度 terraform destroy
を実行して、再度 terraform apply
を実行しようとしたらSecretManagerリソースが作成出来ない問題が発生した。
上記の記事の通り回避策はあるのだが、知らないと時間を無駄に溶かしそうな気がしたので、一応メモとして残しておく。
読み取り用のインスタンスがない場合、読み取り専用エンドポイントは使えない
mysqlコマンドで接続は可能だが実際にSQLを実行しようとすると下記のエラーが発生する。
mysql> show tables;
ERROR 9501 (HY000): Target group doesn't have any associated read-only instances
これはRDS Clusterに割り当てられているインスタンスが1つしかない時に起こる現象。
RDS Clusterにインスタンスを1つ追加してあげれば問題は解決する。

次にやる事メモ
RDS ProxyはLambdaから利用してその真価を発揮するので、Lambdaから接続するサンプルコードを作成する。
Lambdaのサンプル作成は下記の記事などを参考にしてみる。
本番運用時にこれが起こると怖いので内容確認しておく。

ピン留め回避の為の公式見解