You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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均部署在私有子网,禁止公网访问。

三、优化后的部署流程

  1. 执行cdk deploy,自动创建所有资源(RDS、Secret、Lambda、Custom Resource、Fargate服务)。
  2. Custom Resource自动触发Lambda,完成数据库用户创建、授权,并更新Secret(可选)。
  3. Fargate服务在用户创建完成后启动,自动从Secrets Manager拉取凭证,无需手动重启。

内容的提问来源于stack exchange,提问作者tcatic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 05:55:56