CDK で RDS Proxy を追加しようとした時にターゲットグループが登録できなくてハマった話

こんにちは。ライクル事業部エンジニアの寺戸です。

前回の記事でも書いたとおり、ライクルではサービスのインフラに AWS を利用しており、各インフラの管理には AWS CDK を使用しています。

今回は、 CDK を使用して既存の RDS インスタンスに RDS Proxy を追加しようとした際にハマってしまったポイントについてまとめていきます。

RDS Proxy 導入前後の状態

ライクルではメインのデータストアとして RDS を利用しており、 Lambda や ECS で稼働している各アプリケーションが RDS へアクセスしているごく普通な構成です。

すごくざっくりとした図を書くと現状はこんな感じ。
Lambda や ECS へアクセスするための経路はちゃんとありますが割愛。

ただし、一般的に Lambda + RDS の構成はアンチパターンと言われており、この点はいつか改善しないとねとチーム内では認識しており、その改善を今回行いました。

また、 RDS に接続するための認証情報を各リポジトリで管理している状態だったので、これらも AWS Secrets Manager で管理する改善も合わせて実施しました。

参考: qiita.com

最終的に以下の構成を目指します。

何が起こったか

公式リファレンスを参考に、 RDS Proxy を作成する CDK のコードを実装しました。

import * as cdk from 'aws-cdk-lib'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as rds from 'aws-cdk-lib/aws-rds'
import * as secrets from 'aws-cdk-lib/aws-secretsmanager'

// 中略

protected newRDSInstance(
  props: rds.DatabaseInstanceProps,
  parameters: { [key: string]: string },
): [rds.IDatabaseInstance, ec2.ISecurityGroup] {
  // セキュリティグループを作成
  const securityGroup = new ec2.SecurityGroup(
    this,
    'rds-security-group',
    {
      vpc: props.vpc,
      securityGroupName: `${props.instanceIdentifier}`,
    },
  )

  securityGroup.addIngressRule(
    ec2.Peer.ipv4(props.vpc.vpcCidrBlock),
    ec2.Port.tcp(props.port ? props.port : 3306),
  )

  const parameterGroup = new rds.ParameterGroup(
    this,
    'rds-parameter-group',
    {
      engine: rds.DatabaseInstanceProps.engine,
      parameters: {
        time_zone: 'Asia/Tokyo',
        ...parameters,
      },
    },
  )

  const instance = new rds.DatabaseInstance(
    this,
    'rds-instance',
    {
      parameterGroup: parameterGroup,
      securityGroups: [securityGroup],
      deletionProtection: true,
      ...props,
    },
  )

  return [instance, securityGroup]
}

// AWS Secrets Manager の作成
protected newRDSSecretsManager(
  name: string,
  userName: string
): secrets.Secret {
  return new secrets.Secret(this, `secrets-manager-${userName}`, {
    secretName: name,
    generateSecretString: {
      secretStringTemplate: JSON.stringify({
        userName: userName,
      }),
      excludePunctuation: true,
      includeSpace: false,
      generateStringKey: 'password',
    },
  })
}
// RDS Proxy の作成
protected newRDSProxy(props: rds.DatabaseProxyProps): rds.DatabaseProxy {
  return new rds.DatabaseProxy(this, 'rds-proxy', {
    ...props,
  })
}

実際に上記の関数を呼び出している箇所がこちら。
今回、AWS Secrets Manager で複数の認証情報を管理し、それらを RDS Proxy で使用する構成にするため、以下のように CDK を実装しました。

// RDS 用の Secrets Manager を作成
const rdsSecrets: secrets.ISecret[] = [];
context.rds.proxy.secrets.forEach((secret) => {
  rdsSecrets.push(this.newRDSSecretsManager(secret.name, secret.userName));
});

// RDS Proxy を作成
const proxy = this.newRDSProxy({
  proxyTarget: rds.ProxyTarget.fromInstance(instance),
  secrets: rdsSecrets,
  vpc,
  dbProxyName: context.rds.proxy.name,
  securityGroups: [securityGroup],
  maxConnectionsPercent: context.rds.proxy.maxConnectionsPercent,
});

このコードを cdk deploy すると、ターゲットグループの作成で処理が止まってしまい、最終的に 2 時間経っても何も変わらず静かにタイムアウトする、という状況に陥りました。

マネジメントコンソールを確認すると、 RDS Proxy 自体は作成されているものの、ターゲットグループが利用不可の状態になっていました。

調査開始

最終的にタイムアウトした cdk コマンドの実行ログを見ても、特に何が原因でタイムアウトをしたかは出力されていないため、完全に情報ゼロの状態で調査開始です。
この辺はもう少し分かりやすい失敗理由を出力してもらえると原因調査が捗るので頑張ってもらいたいですね…。

それらしいキーワードで検索すると、色々と情報が出てきました。

参考: qiita.com

また、ターゲットグループが利用不可の状態で RDS Proxy の状態をコマンドで確認してみると、以下のような結果でした。

$ aws rds describe-db-proxy-targets --db-proxy-name {対象の RDS Proxy 名}
{
  "Targets": [
    {
      "Endpoint": "********.********.ap-northeast-1.rds.amazonaws.com",
      "RdsResourceId": "********",
      "Port": 3306,
      "Type": "RDS_INSTANCE",
      "Role": "READ_WRITE",
      "TargetHealth": {
        "State": "UNAVAILABLE",
        "Reason": "AUTH_FAILURE",
        "Description": "Proxy does not have any registered credentials"
      }
    }
  ]
}

この状態の場合 IAM Role に問題があるようなのですが、どういう問題がある可能性があるのか、という情報は見つけることが出来ませんでした 😔

色々試行錯誤をしても埒が明かない状況だったので、AWS のソリューションアーキテクトの方に今回の事象を問い合わせてみたところ、色々と情報を提供していただきました。
※弊社では AWS のソリューションアーキテクトに相談できるのですが、私は今回初めて問い合わせをしました。

どうやら RDS で使用する認証情報と、 Secrets Manager のあたりに原因がありそうという見解をいただきました。

原因

結論から述べると、 Secrets Manager で生成した認証情報が RDS で使用できる状態になっていなかったことが原因でした。

原因について詳しく解説していきます。

以下は CDK の公式リファレンスにある RDS Proxy を生成するサンプルコードです。

このコードを見ると、データベースクラスター作成時の認証情報を RDS Proxy にもアタッチしています。
これはつまり、 RDS インスタンスで使用できる認証情報を RDS Proxy でも使用できるようにすることを指しています。

declare const vpc: ec2.Vpc;
const cluster = new rds.DatabaseCluster(this, 'Database', {
  engine: rds.DatabaseClusterEngine.AURORA,
  instanceProps: { vpc },
});

const proxy = new rds.DatabaseProxy(this, 'Proxy', {
  proxyTarget: rds.ProxyTarget.fromCluster(cluster),
  secrets: [cluster.secret!], // RDS インスタンスの認証情報をここでセットしている
  vpc,
});

const role = new iam.Role(this, 'DBProxyRole', {
  assumedBy: new iam.AccountPrincipal(this.account),
});
proxy.grantConnect(role, 'admin');

私が実装した Secrets Manager を生成するコードをもう一度確認してみると、

// RDS 用の Secrets Manager を作成
const rdsSecrets: secrets.ISecret[] = [];
context.rds.proxy.secrets.forEach((secret) => {
  rdsSecrets.push(this.newRDSSecretsManager(secret.name, secret.userName));
});

// RDS Proxy を作成
const proxy = this.newRDSProxy({
  proxyTarget: rds.ProxyTarget.fromInstance(instance),
  secrets: rdsSecrets,
  vpc,
  dbProxyName: context.rds.proxy.name,
  securityGroups: [securityGroup],
  maxConnectionsPercent: context.rds.proxy.maxConnectionsPercent,
});

としており、 Secrets Manager で認証情報を作成し、それを RDS Proxy に設定しているだけでした。

このままではこの認証情報を何に使用するかという情報が設定できていないため、ターゲットグループの登録が正常に完了しなかったようです。

結果的に、以下のように修正したことで解決しました。

// RDS 用の Secrets Manager を作成
const rdsSecrets: secrets.ISecret[] = [];
context.rds.proxy.secrets.forEach((secret) => {
  const cred = this.newRDSSecretsManager(secret.name, secret.userName);
  rdsSecrets.push(cred.attach(instance)); // attach メソッドで、生成した認証情報を RDS インスタンスに関連付けている
});

// RDS Proxy を作成
const proxy = this.newRDSProxy({
  proxyTarget: rds.ProxyTarget.fromInstance(instance),
  secrets: rdsSecrets,
  vpc,
  dbProxyName: context.rds.proxy.name,
  securityGroups: [securityGroup],
  maxConnectionsPercent: context.rds.proxy.maxConnectionsPercent,
});

この状態で cdk deploy を行うとようやく正常にデプロイが完了し、ターゲットグループもアクティブになり、無事に RDS Proxy 経由で RDS インスタンスに接続出来ることも確認できました!

ただ、 RDS Proxy 導入後から一部のデータが文字化けして返却されたり、ピン留め問題が発生したりしましたが、それはまた別のお話…。

終わりに

今回、導入前の状態として既に RDS インスタンスが存在している中に、新たに RDS Proxy を追加するという点が問題をややこしくしていたのと、 Secrets Manager で生成した認証情報は、 AWS のどのサービスで利用するかを明示的に関連付けないと使用できないということを分かっていなかったのでドツボにはまってしまいました。

よくよく考えれば、マネジメントコンソールから Secrets Manager を作成する時に何に利用する認証情報なのかを選択する箇所があるので、その設定を CDK に起こせていなかったという話でした。

どこかの誰かの参考になれば幸いです。