AWS RDS Proxy using Terraform


Terraform을 이용하여 AWS RDS Proxy 적용하기

RDS Proxy 소개 및 장단점

AWS RDS의 경우 DBInstanceClassMemory값에서 계산된 값에 의하여 max_connections이 결정된다.

max_connections = DBInstanceClassMemory / 12582880

대부분 API Server에서 DB 연결에 Connection Pool을 활용한다. 그러면 실제 DB 사용량과는 무관하게 Server가 많이 떠 있을려면 그만큼 DB Instance도 큰걸 사용해야 한다. CPU는 20% 미만으로 계속 사용하지만 max_connection 때문에 더 큰 RDS를 사용해야 한다. RDS는 가격이 비싸기 때문에 실제 사용보다 비싼 비용을 내야만 하는 상황이 된다.

그리고 AWS Lambda를 사용할 경우 connection 관리에 신경을 써야한다. 계속해서 connection을 맺었다가 끊어야 하는 경우도 많고, 이걸 API Server로 활용하는 경우 갑자기 request가 몰릴 경우 동시에 여러 Lambda Function이 실행될 경우 DB connection수가 갑자기 증가할 경우 장애가 발생할 수도 있다.

이런 경우 AWS RDS Proxy를 사용하면 좋다. RDS ProxyRDS 서비스용 HA Proxy라고 이해하면 된다. ClientConnection (API Server)를 RDS Proxy가 받아서 그 중 실제로 RDS에 호출을 할때만 DatabaseConnection을 연결해서 요청한다. 그래서 RDS에 API Server가 직접 연결하는 경우보다 DatabaseConnection가 많이 줄어든다.

그럼 RDS Proxy의 과금은 어떻게 될까? 공식문서를 보면 시간당 얼마라고 나오는데 이걸 월 별로 계산해 봤다.

RDS의 vCPU 1개당 30일에 $ 12.96

예를 들어서 r6g.xlarge의 경우 vCPU가 4개인 Instance이기에 이것에 연결된 RDS Proxy 비용은 12.96 x 4 = $ 51.84 이다. 만약 2대로 writer, reader를 cluster로 사용한다면 51.84 x 2 = $ 103.8 이다. r6g.xlarge의 경우 default max_connection이 2000 으로 설정되어 있다. 위에 있는 수식으로 최대 연결수를 계산하더라도 2730 이다. 실제로 Connection Pool이 2000개 연결되어 있으나, 동시에 2000개의 요청이 오는 상황이 아닌 경우에도 Instance를 Scale Up 해야한다. 하지만 RDS Proxy를 사용하면 CPU에 대한 부하때문에 Scale Up을 하는 것이 아니라 이렇게 connection 때문에는 Scale Up을 할 필요없으니깐 비용을 많이 절약할 수 있다.

RDS Proxy의 장점은 크게 3가지로 정리된다.

  1. RDS max_connection 때문에 Scale Up 할 필요가 없어져서 비용절감을 할 수 있다.
  2. API Server에서 connection pool size를 정교하게 설정하기 위해서 노력하지 않아도 된다.
  3. RDS Scale Up 할때 connection을 새로 맺을 필요가 없다.

RDS Cluster에서 Scale Up을 할 경우 더 큰 Instance를 띄운 다음 Fail Over를 하면 되는데, 그 경우 기존 connection들을 재연결해줘야 한다. RDS Proxy로 연결을 한 경우 직접적으로 RDS에 연결된 것이 아니므로 신경쓰지 않아도 된다.

장점만 있는건 아니다. RDS Proxy의 경우 외부에서 접근이 안된다. 2022년 11월 기준으로는 안된다. (추후 public access가 될 수도 있다. 하지만 아직은 안된다.) 그러기 때문에 개발자가 직접 연결을 하고자 할땐 RDS로 직접 연결하거나 SSH Tunneling Instance를 통해서 접속해야 한다.

RDS Proxy 생성과정

먼저 연결할 RDS가 있어야 하는건 당연한 것이니깐 이건 생략하겠다.

  1. Key Management Service에서 암호화에 사용할 key 생성
  2. RDS연결 정보를 관리하는 Secret Manager를 생성 (KMS 필요)
  3. Secret ManagerKMS에 접근권한이 있는 IAM Policy 생성
  4. 해당 Policy를 사용할 IAM Role 생성
  5. RDS Proxy 생성

Terraform을 이용해서 생성해 보겠다. Console이나 CLI로 생성하실 분들은 공식문서를 참고하면 된다.

https://docs.aws.amazon.com/ko_kr/AmazonRDS/latest/UserGuide/rds-proxy-setup.html

Aurora MySQL에 연결하는 예제로 제작하였다.

1. Key Managerment Service에서 암호화에 사용할 키 생성

resource aws_kms_key secret_key {
  is_enabled          = true
  enable_key_rotation = false
  multi_region        = true
  description         = "Key for RDS Proxy"
}

resource aws_kms_alias secret_key_alias {
  name          = "alias/secret-key"
  target_key_id = aws_kms_key.secret_key.key_id
}

2. RDS 연결 정보를 암호화하여 저장할 Secret Manager생성

variable cluster_name {
  type = string
}

variable cluster_master_password {
  type = string
}

data aws_rds_cluster cluster {
  cluster_identifier = var.cluster_name
}

resource aws_secretsmanager_secret rds_secret {
  name = "rds-${var.cluster_name}"
  kms_key_id = aws_kms_key.secret_key.key_id
}

resource aws_secretsmanager_secret_version rds_secret_password {
  secret_id = aws_secretsmanager_secret.rds_secret.id
	
  secret_string = jsonencode({
    dbClusterIdentifier = var.cluster_name
    host                = data.aws_rds_cluster.cluster.endpoint
    port                = data.aws_rds_cluster.cluster.port
    username            = data.aws_rds_cluster.cluster.master_username
    password            = var.cluster_master_password
    engine              = "mysql"
  })
}

다른 자료들에서는 data.aws_rds_cluster.cluster.master_password로 읽어오는 것으로 되어있었는데, 해당 attribute가 제공되지 않는다고 오류가 떠서 해당값을 입력받는 것으로 처리했다.

3. Secret ManagerKMS에 접근권한이 있는 IAM PolicyIAM Role 생성

resource aws_iam_policy rds_proxy {
  name = "rds-proxy-${var.cluster_name}"

  policy = jsonencode(
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                Effect = "Allow"
                Action = "secretsmanager:GetSecretValue"
                Resource = [aws_secretsmanager_secret.rds_secret.arn]
            },
            {
                Effect = "Allow"
                Action = "kms:Decrypt"
                Resource = aws_kms_key.secret_key.arn
                Condition = {
                    "StringEquals": {
                        "kms:ViaService": "secretsmanager.ap-northeast-2.amazonaws.com"
                    }
                }
            }
        ]
    })
}

resource aws_iam_role rds_proxy {
  name = "rds-proxy-${var.cluster_name}"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = "sts:AssumeRole",
        Principal = {
          Service = ["rds.amazonaws.com"]
        }
      }
    ]
  })
}

resource aws_iam_role_policy_attachment rds_proxy {
  role       = aws_iam_role.rds_proxy.name
  policy_arn = aws_iam_policy.rds_proxy.arn
}

4. RDS Proxy 생성

variable security_group_ids {
  type = list(string)
}

variable subnet_ids {
  type = list(string)
}

resource aws_db_proxy rds_proxy {
  name                   = "${var.cluster_name}-proxy"
  debug_logging          = false
  engine_family          = "MYSQL"
  idle_client_timeout    = 1800
  require_tls            = false
  role_arn               = aws_iam_role.rds_proxy.arn
  vpc_security_group_ids = var.security_group_ids
  vpc_subnet_ids         = var.subnet_ids

  auth {
    auth_scheme = "SECRETS"
    iam_auth    = "DISABLED"
    secret_arn  = aws_secretsmanager_secret.rds_secret.arn
  }

  tags = local.module_tags
}

resource aws_db_proxy_endpoint reader {
  db_proxy_name          = aws_db_proxy.rds_proxy.name
  db_proxy_endpoint_name = "${aws_db_proxy.rds_proxy.name}-reader"
  vpc_subnet_ids         = var.subnet_ids
  target_role            = "READ_ONLY"

  tags = local.module_tags
}

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
    session_pinning_filters      = []
  }
}

resource aws_db_proxy_target rds_proxy {
  db_cluster_identifier  = data.aws_rds_cluster.cluster.id
  db_proxy_name          = aws_db_proxy.rds_proxy.name
  target_group_name      = aws_db_proxy_default_target_group.rds_proxy.name
}

Console에서 생성할 때는 reader 추가하는 체크박스가 하나 있는데, Terraform으로 생성할 때는 해당 옵션이 없어서 aws_db_proxy_endpoint.reader 를 추가로 생성하였다. Console에서는 이것들이 한 번에 생성되는데 Terraform으로 생성할 경우 하나씩 생성되기 때문에 2배 이상의 시간이 소요되는 느낌이었다.

5. 연결

RDS Proxy 생성이 성공하면 Console에서 Proxy endpoint 확인이 가능하다. VPC안에서만 접속이 가능하기에 DataGrip 같은 툴로 접속 확인은 힘들다. VPC 내로 ssh tunneling을 이용하는 식의 방법이 필요하다. 그래서 해당 endpoint를 사용하도록 API를 수정 후 배포해 보았다.

먼저 이 스샷은 RDS의 Monitoring 탭에서 확인한 내용이다.

이 스샷은 RDS Proxy의 Monitoring 탭에서 확인한 내용이다.

API 배포 직후 RDS Monitoring에서 connection이 80가까이 되던것이 5이하로 내려간 것이 보인다. Locust라는 Load Test 툴을 이용해서 API에 Request를 보냈다. 초당 100 request 정보를 보내보았다. 그러니 API pod가 기존 3개에서 5개로 늘어났고, 그로 인해 RDS Proxy의 client connections에서도 조금 더 늘어나는 것을 볼 수 있다. 만약 RDS Proxy를 사용하지 않았으면 저 connection이 모두 RDS로 직접 연결되었을 것이고, RDS는 t4g.medium이어서 125 정도로 알고 있는데 그것을 초과하게 되어서 몇몇 API는 연결을 못하는 상황이 발생했을 것이다.

참고


이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)