Closed3

TerraformでRDS Proxyを作成

keitaknkeitakn

やる事

タイトルの通りでTerraformでRDS Proxyを作成して、接続確認を行うところまで。

利用するTerraformのバージョン

  • Terraform 0.14.6
  • terraform-provider-aws 3.39.0

terraform-provider-aws のバージョンはこの記事を書いた 2021年5月11日時点で最新バージョン。

最新バージョンである必要はないがある程度新しいバージョンでないとRDS Proxyに対応していないので、基本的に最新安定版を利用する事を推奨。

はじめに

以下のPRが最終アウトプット。

https://github.com/keitakn/my-terraform/pull/52

これを見ただけで、理解出来る場合はこれ以降の章を読む必要はなし。

後で出てくる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 リソースだとそれらしい設定はなかったので、別途作成している。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_proxy

接続確認方法

接続確認用のEC2インスタンスからmysqlコマンドで接続出来るか確認するのが一番楽なので、その方法で確認します。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/rds-aurora-mysql-connect-proxy/

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に接続出来ない

下記の内容を元に解決。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/rds-proxy-connection-issues/

以下のコマンドで接続出来ない原因を調べる事が出来る。

aws rds describe-db-proxy-targets --db-proxy-name $作成したRDS Proxyの名前

レスポンスの TargetHealth の中身を見ると接続出来ない原因が特定可能。

下記の記事に詳しい見方が載っていたので参考にさせてもらいました。

https://qiita.com/mmmm/items/0486f6f4be44109b5e32

ちなみに自分はRDS Proxy → RDS Cluster への接続許可を出すのを忘れていて、接続出来ない状態になっていました。

接続確認コマンドはAWS CLIが使える必要があるので下記を参考に認証情報を設定しておくと良さそう。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config

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日間定義出来ない

詳細は下記の通り。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/delete-secrets-manager-secret/

色々と試行錯誤しているうちにリソースをスッキリさせたくなって一度 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つ追加してあげれば問題は解決する。

このスクラップは2021/10/21にクローズされました