使用CDK创建RDS数据库时如何自动创建多数据库用户?
CDK自动化RDS多用户创建与安全连接管理
一、自动创建独立数据库用户
通过CDK结合Custom Resource + Lambda实现自动化创建数据库用户,替代手动连接数据库的操作:
1. 核心思路
- 为每个服务(API/Hasura)生成独立的Secrets Manager密钥,存储用户名和随机密码。
- 编写Lambda函数,通过管理员凭证连接RDS,执行创建用户、授权的SQL命令。
- 用CDK的Custom Resource触发Lambda,确保RDS实例就绪后再执行用户创建逻辑。
2. 具体实现代码(TypeScript)
第一步:创建RDS实例与服务专用Secret
import * as cdk from 'aws-cdk-lib'; import * as rds from 'aws-cdk-lib/aws-rds'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as customresources from 'aws-cdk-lib/custom-resources'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'RdsMultiUserStack'); const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { vpcName: 'your-vpc-name' }); // 创建RDS PostgreSQL实例 const rdsInstance = new rds.DatabaseInstance(stack, 'PostgresDB', { engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_15 }), instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO), credentials: rds.Credentials.fromGeneratedSecret('db_admin'), vpc: vpc, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED }, publiclyAccessible: false, }); // 创建API服务专用Secret const apiDbSecret = new secretsmanager.Secret(stack, 'ApiDbSecret', { generateSecretString: { secretStringTemplate: JSON.stringify({ username: 'api_service_user' }), generateStringKey: 'password', passwordLength: 16, excludePunctuation: true, }, }); // 创建Hasura服务专用Secret const hasuraDbSecret = new secretsmanager.Secret(stack, 'HasuraDbSecret', { generateSecretString: { secretStringTemplate: JSON.stringify({ username: 'hasura_service_user' }), generateStringKey: 'password', passwordLength: 16, excludePunctuation: true, }, });
第二步:编写用户创建Lambda
在项目目录下创建lambda/create-db-user/index.js:
const { SecretsManager } = require('aws-sdk'); const { Client } = require('pg'); const secretsManager = new SecretsManager(); exports.handler = async (event) => { if (event.RequestType !== 'Create' && event.RequestType !== 'Update') { return sendResponse(event, 'SUCCESS', 'No action required for delete'); } try { // 获取管理员凭证 const adminSecretResp = await secretsManager.getSecretValue({ SecretId: process.env.DB_ADMIN_SECRET_ARN }).promise(); const adminCreds = JSON.parse(adminSecretResp.SecretString); // 获取当前服务的用户凭证 const userSecretResp = await secretsManager.getSecretValue({ SecretId: event.ResourceProperties.UserSecretArn }).promise(); const userCreds = JSON.parse(userSecretResp.SecretString); const { username, password } = userCreds; const dbName = event.ResourceProperties.DbName || 'postgres'; // 连接数据库执行用户创建与授权 const client = new Client({ host: process.env.DB_HOST, port: 5432, user: adminCreds.username, password: adminCreds.password, database: dbName, }); await client.connect(); // 创建用户(如果不存在则跳过) await client.query(`DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_user WHERE usename = '${username}') THEN CREATE USER ${username} WITH PASSWORD '${password}'; END IF; END $$;`); // 按服务类型分配权限 if (event.ResourceProperties.IsHasura) { // Hasura需要的权限(可根据实际需求调整) await client.query(`GRANT ALL PRIVILEGES ON DATABASE ${dbName} TO ${username};`); await client.query(`GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${username};`); await client.query(`GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${username};`); await client.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO ${username};`); } else { // API服务的最小权限 await client.query(`GRANT CONNECT ON DATABASE ${dbName} TO ${username};`); await client.query(`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${username};`); await client.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${username};`); } await client.end(); // 可选:更新Secret添加完整连接字符串 await secretsManager.putSecretValue({ SecretId: event.ResourceProperties.UserSecretArn, SecretString: JSON.stringify({ ...userCreds, connectionString: `postgres://${username}:${password}@${process.env.DB_HOST}:5432/${dbName}` }) }).promise(); return sendResponse(event, 'SUCCESS', `User ${username} configured successfully`); } catch (err) { console.error('Error configuring DB user:', err); return sendResponse(event, 'FAILED', err.message); } }; // 向CloudFormation发送响应 function sendResponse(event, status, message) { const https = require('https'); const url = require('url'); const responseBody = JSON.stringify({ Status: status, Reason: message, PhysicalResourceId: event.PhysicalResourceId || event.RequestId, StackId: event.StackId, RequestId: event.RequestId, LogicalResourceId: event.LogicalResourceId, Data: { Message: message } }); const parsedUrl = url.parse(event.ResponseURL); const options = { hostname: parsedUrl.hostname, port: 443, path: parsedUrl.path, method: 'PUT', headers: { 'Content-Type': '', 'Content-Length': responseBody.length } }; return new Promise((resolve, reject) => { const req = https.request(options, () => resolve()); req.on('error', reject); req.write(responseBody); req.end(); }); }
第三步:在CDK中配置Lambda与Custom Resource
// 创建Lambda函数 const createDbUserLambda = new lambda.Function(stack, 'CreateDbUserLambda', { runtime: lambda.Runtime.NODEJS_18_X, code: lambda.Code.fromAsset('lambda/create-db-user'), handler: 'index.handler', environment: { DB_HOST: rdsInstance.dbInstanceEndpointAddress, DB_ADMIN_SECRET_ARN: rdsInstance.secret!.secretArn }, vpc: vpc, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED }, timeout: cdk.Duration.seconds(30) }); // 授予Lambda必要权限 rdsInstance.secret!.grantRead(createDbUserLambda); apiDbSecret.grantRead(createDbUserLambda); hasuraDbSecret.grantRead(createDbUserLambda); rdsInstance.connections.allowFrom(createDbUserLambda, ec2.Port.tcp(5432)); // 为API用户创建Custom Resource new customresources.AwsCustomResource(stack, 'ProvisionApiDbUser', { onCreate: { service: 'Lambda', action: 'invoke', parameters: { FunctionName: createDbUserLambda.functionName, Payload: JSON.stringify({ RequestType: 'Create', ResourceProperties: { UserSecretArn: apiDbSecret.secretArn, DbName: 'your-db-name', IsHasura: false } }) }, physicalResourceId: customresources.PhysicalResourceId.of(`api-user-${Date.now()}`) }, policy: customresources.AwsCustomResourcePolicy.fromSdkCalls({ resources: customresources.AwsCustomResourcePolicy.ANY_RESOURCE }) }); // 为Hasura用户创建Custom Resource new customresources.AwsCustomResource(stack, 'ProvisionHasuraDbUser', { onCreate: { service: 'Lambda', action: 'invoke', parameters: { FunctionName: createDbUserLambda.functionName, Payload: JSON.stringify({ RequestType: 'Create', ResourceProperties: { UserSecretArn: hasuraDbSecret.secretArn, DbName: 'your-db-name', IsHasura: true } }) }, physicalResourceId: customresources.PhysicalResourceId.of(`hasura-user-${Date.now()}`) }, policy: customresources.AwsCustomResourcePolicy.fromSdkCalls({ resources: customresources.AwsCustomResourcePolicy.ANY_RESOURCE }) });
二、安全管理Hasura连接字符串
避免直接序列化密钥,通过ECS Secrets挂载动态获取凭证:
1. 挂载凭证到Fargate服务
// 配置Hasura Fargate服务 const hasuraCluster = ecs.Cluster.fromClusterAttributes(stack, 'HasuraCluster', { clusterName: 'your-ecs-cluster-name', vpc: vpc }); const hasuraTaskDef = new ecs.FargateTaskDefinition(stack, 'HasuraTaskDef', { cpu: 512, memoryLimitMiB: 1024 }); hasuraTaskDef.addContainer('HasuraContainer', { image: ecs.ContainerImage.fromRegistry('hasura/graphql-engine:latest'), secrets: { // 直接从Secret挂载连接字符串(前提是Lambda已更新Secret添加该字段) HASURA_GRAPHQL_DATABASE_URL: ecs.Secret.fromSecretsManager(hasuraDbSecret, 'connectionString'), // 或者挂载单独字段,在启动命令中拼接 // DB_USER: ecs.Secret.fromSecretsManager(hasuraDbSecret, 'username'), // DB_PASSWORD: ecs.Secret.fromSecretsManager(hasuraDbSecret, 'password'), // DB_HOST: ecs.Secret.fromSecretsManager(hasuraDbSecret, 'host') // 可选,也可直接用rdsInstance.dbInstanceEndpointAddress }, // 如果用单独字段,启动命令改为: // command: ['graphql-engine', 'serve', '--database-url', 'postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):5432/your-db-name'] }); const hasuraService = new ecs.FargateService(stack, 'HasuraService', { cluster: hasuraCluster, taskDefinition: hasuraTaskDef, desiredCount: 1, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED } }); // 确保服务在用户创建完成后启动 hasuraService.node.addDependency(stack.node.findChild('ProvisionHasuraDbUser'));
2. 关键安全要点
- 禁止在CDK代码中硬编码任何凭证或连接字符串。
- 为每个Fargate服务的执行角色配置最小权限,仅允许访问对应的Secret。
- RDS实例和Lambda均部署在私有子网,禁止公网访问。
三、优化后的部署流程
- 执行
cdk deploy,自动创建所有资源(RDS、Secret、Lambda、Custom Resource、Fargate服务)。 - Custom Resource自动触发Lambda,完成数据库用户创建、授权,并更新Secret(可选)。
- Fargate服务在用户创建完成后启动,自动从Secrets Manager拉取凭证,无需手动重启。
内容的提问来源于stack exchange,提问作者tcatic
相关产品推荐
相关产品推荐

