[AWS CDK] AWS Secrets ManagerでAmazon Auroraのパスワードをローテーションしてみた

AWS CDKを使ってAWS Secrets ManagerでAmazon Auroraのパスワードをローテーションさせる設定をしてみました。
2022.03.11

AWS Secrets Managerのパスワードローテーション設定もAWS CDKでやりたいな

こんにちは、のんピ(@non____97)です。

AWS Secrets Managerのパスワードローテーション設定もAWS CDKでやりたいなと思ったことはありますか? 私はあります。

「AWS CloudFormation × Amazon RDS」の組み合わせは以下記事で紹介されているので、「AWS CloudFormation × Amazon Aurora」もできるだろうと思いました。

なんでも本気を出せばAWS CDKで出来ると思っているので、チャレンジします。

こちらのコードのリポジトリは以下になります。

作成されるリソースの構成

作成されるリソースの構成は以下の通りです。

AWS CDKで定義したリソースの構成図

AWS CDKのコードの確認

AWS CDKのコードのディレクトリ構成は以下の通りです。「既にVPCは作成されている状態でDBを作る」というパターンが多いと思うので、VPCとDB周りとでスタックを分割しました。

> tree
.
├── .gitignore
├── .npmignore
├── README.md
├── bin
│   └── database.ts
├── cdk.context.json
├── cdk.json
├── jest.config.js
├── lib
│   ├── database-stack.ts
│   └── vpc-stack.ts
├── package-lock.json
├── package.json
├── src
│   └── ec2
│       └── user_data_amazon_linux2.sh
├── test
│   └── database.test.ts
└── tsconfig.json

5 directories, 14 files

DBのマスターユーザー名とパスワードの設定は以下のように行います。パスワードに含めない文字列はEXCLUDE_CHARACTERSとして定義しています。なお、PostgreSQLの場合adminというユーザーはシステム側で予約されているので使用できません。

./src/database-stack.ts

// DB Admin User Secret
const dbAdminSecret = new secretsmanager.Secret(this, "DbAdminSecret", {
  secretName: "prd-db-cluster/AdminLoginInfo",
  generateSecretString: {
    excludeCharacters: EXCLUDE_CHARACTERS, // EXCLUDE_CHARACTERS = :@/\" '
    generateStringKey: "password",
    passwordLength: 32,
    requireEachIncludedType: true,
    secretStringTemplate: '{"username": "postgresAdmin"}',
  },
});

DBクラスター作成時に上述のシークレットを指定してあげます。

./src/database-stack.ts

// DB Cluster
const dbCluster = new rds.DatabaseCluster(this, "DbCluster", {
  engine: rds.DatabaseClusterEngine.auroraPostgres({
    version: rds.AuroraPostgresEngineVersion.VER_13_4,
  }),
  instanceProps: {
    vpc: props.vpc,
    allowMajorVersionUpgrade: false,
    autoMinorVersionUpgrade: true,
    deleteAutomatedBackups: false,
    enablePerformanceInsights: true,
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.BURSTABLE3,
      ec2.InstanceSize.MEDIUM
    ),
    parameterGroup: dbParameterGroup,
    performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT,
    publiclyAccessible: false,
    securityGroups: [dbSg],
  },
  backup: {
    retention: Duration.days(7),
    preferredWindow: "16:00-16:30",
  },
  cloudwatchLogsExports: ["postgresql"],
  cloudwatchLogsRetention: logs.RetentionDays.ONE_YEAR,
  clusterIdentifier: "prd-db-cluster",
  copyTagsToSnapshot: true,
  credentials: rds.Credentials.fromSecret(dbAdminSecret),
  defaultDatabaseName: "testDB",
  deletionProtection: true,
  iamAuthentication: false,
  instanceIdentifierBase: "prd-db-instance",
  instances: 1,
  monitoringInterval: Duration.minutes(1),
  parameterGroup: dbClusterParameterGroup,
  preferredMaintenanceWindow: "Sat:17:00-Sat:17:30",
  storageEncrypted: true,
  subnetGroup,
});

パスワードのローテーションの設定は以下のように行います。今回はシングルユーザーローテーションとしました。

./src/database-stack.ts

// Rotate DB Admin user secret
new secretsmanager.SecretRotation(this, "DbAdminSecretRotation", {
  application:
    secretsmanager.SecretRotationApplication.POSTGRES_ROTATION_SINGLE_USER,
  secret: dbAdminSecret,
  target: dbCluster,
  vpc: props.vpc,
  automaticallyAfter: Duration.days(3),
  excludeCharacters: EXCLUDE_CHARACTERS,
  securityGroup: rotateSecretsLambdaFunctionSg,
  vpcSubnets: props.vpc.selectSubnets({
    subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
  }),
});

パスワードをローテーションさせるLambda関数の作成も自動でしてくれます。

ポイントはVPCとサブネットの設定です。

今回作成するDBクラスターではパブリックアクセスを有効化していないため、Lambda関数が動作するVPCとサブネットを指定する必要があります。正常にパスワードをローテーションさせるためには、Lambda関数からAWS Secrets Managerのエンドポイントにアクセスできる必要があります。そのためには以下のどちらかの対応が必要です。

  • NAT Gatewayを介してインターネットにルーティングがある
  • AWS Secrets ManagerのVPCエンドポイントがある

正しく設定がされていなければ、パスワードのローテーションができません。手動でローテーションさせようとした時も、An error occurred (InvalidRequestException) when calling the RotateSecret operation: A previous rotation isn’t complete. That rotation will be reattempted.と表示され、失敗します。

ちなみに、シングルユーザー版のPostgreSQLのローテーション用Lambda関数のソースコードは以下の通りです。

動作確認

シークレットの確認

npx cdk deploy --allでAWS CDKで定義した各種リソースをデプロイします。

デプロイ後、AWS Secrets Managerから作成されたシークレットを確認します。

作成されたシークレットの確認

コードで定義した通り3日でローテーションするように設定されています。

シークレットの値を取得する場合は、シークレットの値を取得するをクリックします。

シークレットの値

プレーンテキストタブをクリックすれば以下のようにJSON形式で確認することもできます。

{
  "dbClusterIdentifier": "prd-db-cluster",
  "password": "pi81PTAPj&&aE=YIjRbZ+k5a=Ji_JyAZ",
  "dbname": "testDB",
  "engine": "postgres",
  "port": 5432,
  "host": "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com",
  "username": "postgresAdmin"
}

DBクライアントからの接続確認

次に、こちらのシークレットを使ってDBクライアントから接続できるか確認してみます。

まず、AWS CLIでシークレットを取得します。

$ aws secretsmanager get-secret-value \
    --secret-id prd-db-cluster/AdminLoginInfo \
    --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "bf764736-f06c-49ca-9932-b1474812f28a",
    "SecretString": "{\"dbClusterIdentifier\":\"prd-db-cluster\",\"password\":\"pi81PTAPj&&aE=YIjRbZ+k5a=Ji_JyAZ\",\"dbname\":\"testDB\",\"engine\":\"postgres\",\"port\":5432,\"host\":\"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\",\"username\":\"postgresAdmin\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1646951946.291,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-zDAmV7"
}

マネージメントコンソールと同じパスワードを確認できました。

それでは、DBにアクセスします。シークレットの情報を環境変数に入れて、psqlコマンドを叩くと、正常にDBにアクセスできました。

$ get_secrets_value=$(aws secretsmanager get-secret-value \
    --secret-id prd-db-cluster/AdminLoginInfo \
    --region us-east-1 \
    | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=>

環境変数の参考:

試しにタイムゾーンを確認したところ、DBクラスターのパラメーターグループで設定した通り、Asia/Tokyoになっていました。

testDB=> show timezone;
  TimeZone
------------
 Asia/Tokyo
(1 row)

パスワードのローテーション

それでは、パスワードのローテーションを試します。

パスワードのローテーションをする際は、シークレットのすぐにシークレットをローテーションさせるをクリックします。

ローテーションの構成

注意書きの内容を確認してローテーションさせるをクリックします。

パスワードのローテーション

正常にローテーションのスケジューリングができれば、ローテーションのスケジュールが設定されたシークレットと表示されます。

ローテーションのスケジュールが設定されたシークレット

パスワードをローテーションさせるLambda関数のCloudWatch Logsを確認します。

パスワードのローテーションのログが正常に出力されていました。パスワードの作成から検証、入れ替えと一瞬でローテーションの処理が完了されています。

START RequestId: de40ce14-8fa0-4799-b95c-713bcdaeca7b Version: $LATEST
[INFO]	2022-03-11T00:06:14.209Z	de40ce14-8fa0-4799-b95c-713bcdaeca7b	Found credentials in environment variables.
[INFO]	2022-03-11T00:06:15.466Z	de40ce14-8fa0-4799-b95c-713bcdaeca7b	createSecret: Successfully put secret for ARN arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-zDAmV7 and version 59ab42e5-77bb-483b-9422-9b85b8c9159c.
END RequestId: de40ce14-8fa0-4799-b95c-713bcdaeca7b
REPORT RequestId: de40ce14-8fa0-4799-b95c-713bcdaeca7b	Duration: 1523.71 ms	Billed Duration: 1524 ms	Memory Size: 128 MB	Max Memory Used: 72 MB	Init Duration: 390.79 ms	

START RequestId: 11da92c7-a9fe-4390-92bd-53a647bdd7d2 Version: $LATEST
[INFO]	2022-03-11T00:06:16.081Z	11da92c7-a9fe-4390-92bd-53a647bdd7d2	setSecret: Successfully set password for user postgresAdmin in PostgreSQL DB for secret arn arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-zDAmV7.
END RequestId: 11da92c7-a9fe-4390-92bd-53a647bdd7d2
REPORT RequestId: 11da92c7-a9fe-4390-92bd-53a647bdd7d2	Duration: 598.02 ms	Billed Duration: 599 ms	Memory Size: 128 MB	Max Memory Used: 74 MB	

START RequestId: cd6c405f-8895-4d17-bc34-d4560d8cc11e Version: $LATEST
[INFO]	2022-03-11T00:06:16.610Z	cd6c405f-8895-4d17-bc34-d4560d8cc11e	testSecret: Successfully signed into PostgreSQL DB with AWSPENDING secret in arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-zDAmV7.
END RequestId: cd6c405f-8895-4d17-bc34-d4560d8cc11e
REPORT RequestId: cd6c405f-8895-4d17-bc34-d4560d8cc11e	Duration: 504.39 ms	Billed Duration: 505 ms	Memory Size: 128 MB	Max Memory Used: 74 MB	

START RequestId: 048458a8-3fbe-454b-8753-3e4e44615039 Version: $LATEST
[INFO]	2022-03-11T00:06:16.886Z	048458a8-3fbe-454b-8753-3e4e44615039	finishSecret: Successfully set AWSCURRENT stage to version 59ab42e5-77bb-483b-9422-9b85b8c9159c for secret arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-zDAmV7.
END RequestId: 048458a8-3fbe-454b-8753-3e4e44615039
REPORT RequestId: 048458a8-3fbe-454b-8753-3e4e44615039	Duration: 242.43 ms	Billed Duration: 243 ms	Memory Size: 128 MB	Max Memory Used: 74 MB

ローテーションの仕組みは以下ブログやAWS公式ドキュメントをご覧ください。

マネージメントコンソールから確認したところ、確かにパスワードが変更されていました。

ローテーションしたシークレットの値

ちなみに、パスワードローテーションの裏でDBクライアントからDBへのセッションは張りっぱなしでしたが、途中で切れることはありませんでした。

それでは、DBクライアントから再度接続してみます。

まずは、古いパスワードで接続を試してみます。psqlを実行したところ、認証に失敗しました。DB側で確かに新しいパスワードが反映されていそうです。

$ psql
psql: error: FATAL:  password authentication failed for user "postgresAdmin"
FATAL:  password authentication failed for user "postgresAdmin"

次にローテーション後の新しいパスワードで接続してみます。こちらは正しく接続することができました。

$ get_secrets_value=$(aws secretsmanager get-secret-value \
    --secret-id prd-db-cluster/AdminLoginInfo \
    --region us-east-1 \
    | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=>

AWS CDKで一撃

AWS CDKを使ってAWS Secrets ManagerでAmazon Auroraのパスワードをローテーションさせる設定をしてみました。

AWS CDKで一撃で設定できるので非常に楽ですね。

ただし、現時点の最新のAWS CDK v2.15.0ではローテーションのスケジュールをCron式で指定することはできないので注意が必要です。

また、AWS CDKで作成したDBを実際に運用をされる際は、誤ってDBが削除されないように、スタックの削除保護の有効化や、DeletionPolicyをやRetainSnapshotとすることをオススメします。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!