如何动态承担角色以使用 appConfig 从 Lambda 访问 DynamoDB?

how to dynamically assume a role to access DynamoDB from a Lambda using appConfig?

我有两个 AWS 堆栈:

一个有 dynamoDB table 并“导出”(到 appConfig)tableArn、tableName 和 tableRoleArn(理想情况下应该允许访问table).

import { App, Stack, StackProps } from '@aws-cdk/core';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import * as cdk from '@aws-cdk/core';
import * as appconfig from '@aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam';

export class ExportingStack extends Stack {
    constructor(scope: App, id: string, props: StackProps) {
        super(scope, id, props);

        const table = new dynamodb.Table(this, id, {
            billingMode: dynamodb.BillingMode.PROVISIONED,
            readCapacity: 1,
            writeCapacity: 1,
            removalPolicy: cdk.RemovalPolicy.DESTROY,
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
            sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
            pointInTimeRecovery: true
        });

        const tablePolicy = new PolicyStatement({
            effect: Effect.ALLOW,
            resources: [table.tableArn],
            actions: ['*']
        });
        const role = new Role(this, 'tableRoleArn', {
            assumedBy: new ServicePrincipal('lambda.amazonaws.com')
        });
        role.addToPolicy(
            tablePolicy
        );

        const app = '***';
        const environment = '***';
        const profile = '***';
        const strategy = 'v';

        const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'ConfigurationName', {
            applicationId: app,
            configurationProfileId: profile,
            contentType: 'application/json',
            content: JSON.stringify({
                tableArn: table.tableArn,
                tableName: table.tableName,
                tableRoleArn: role.roleArn
            }),
            description: 'table config'
        });

        const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
            applicationId: app,
            configurationProfileId: profile,
            environmentId: environment,
            configurationVersion: newConfig.ref,
            deploymentStrategyId: strategy
        });
    }
}

第二个有一个功能,我希望能够使用 appConfig 配置来动态访问 table。

import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core';
import { LayerVersion, Runtime } from '@aws-cdk/aws-lambda';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
import { Effect, PolicyStatement } from '@aws-cdk/aws-iam';

export class ConsumingStack extends Stack {
    constructor(scope: App, id: string, props: StackProps) {
        super(scope, id, props);

        const fn = new NodejsFunction(this, 'foo', {
            runtime: Runtime.NODEJS_12_X,
            handler: 'foo',
            entry: `stack/foo.ts`
        });

        fn.addToRolePolicy(
            new PolicyStatement({
                effect: Effect.ALLOW,
                resources: ['*'],
                actions: [
                    'ssm:*', 
                    'appconfig:*',
                    'sts:*',
                ]
            })
        );

        new CfnOutput(this, 'functionArn', { value: fn.functionArn});

        // https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html
        // https://github.com/aws-samples/aws-appconfig-codepipeline-cdk/blob/main/infrastructure/src/main/kotlin/com/app/config/ServerlessAppStack.kt
        const appConfigLayer = LayerVersion.fromLayerVersionArn(
            this,
            'appconfigLayer',
            'arn:aws:lambda:eu-west-2:282860088358:layer:AWS-AppConfig-Extension:47'
        );

        fn.addLayers(appConfigLayer);
    }
}

和处理程序

import type { Context } from 'aws-lambda';
import fetch from 'node-fetch';
import { DynamoDB, STS } from 'aws-sdk';
import { Agent } from 'https';

export const foo = async (event: any, lambdaContext: Context): Promise<void> => {
    const application = '*****';
    const environment = '*****';
    const configuration = '*****';

    const response = await fetch(
        `http://localhost:2772/applications/${application}/environments/${environment}/configurations/${configuration}`
    );

    const configurationData = await response.json();

    console.log(configurationData);
    
    const credentials = await assumeRole(configurationData.tableRoleArn);

    const db = new DynamoDB({
        credentials: {
            sessionToken: credentials.sessionToken,
            secretAccessKey: credentials.secretAccessKey,
            accessKeyId: credentials.accessKeyId
        },
        apiVersion: '2012-08-10',
        region: '*****',
        httpOptions: {
            agent: new Agent({ keepAlive: true }),
            connectTimeout: 1000,
            timeout: 5000
        },
        signatureVersion: 'v4',
        maxRetries: 3
    });

    const item = await db
        .getItem({ TableName: configurationData.tableName, Key: { id: { S: 'coolPeople' }, createdAt: { N: '0' } } }, (e) => {
            console.log('e', e);
        })
        .promise();

    console.log('item:', item?.Item?.value?.L);

   
};

/**
 * Assume Role for cross account operations
 */
export const assumeRole = async (tableRoleArn: string): Promise<any> => {

    let params = {
        RoleArn: tableRoleArn,
        RoleSessionName: 'RoleSessionName12345'
    };

    console.info('Assuming Role with params:', params);

    let sts = new STS();

    return new Promise((resolve, reject) => {
        sts.assumeRole(params, (error, data) => {
            if (error) {
                console.log(`Could not assume role, error : ${JSON.stringify(error)}`);
                reject({
                    statusCode: 400,
                    message: error['message']
                });
            } else {
                console.log(`Successfully Assumed Role details data=${JSON.stringify(data)}`);
                resolve({
                    statusCode: 200,
                    body: data
                });
            }
        });
    });
};


问题是我在 lambda 中尝试 assumeRole 时遇到此错误。

Could not assume role, error : {"message":"User: arn:aws:sts::****:assumed-role/ConsumingStack-fooServiceRole****-***/ConsumingStack-foo****-*** is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::****:role/ExportingStack-tableRoleArn****-***","code":"AccessDenied","time":"2022-02-21T16:06:44.474Z","requestId":"****-***-****-****","statusCode":403,"retryable":false,"retryDelay":26.827985116659757}

那么 Lambda 是否有可能动态承担一个角色以从不同的堆栈访问 table?

我通过将 table 角色的信任关系更改为 arn:aws:iam::${Stack.of(this).account}:root

使其正常工作
import { App, Stack, StackProps } from '@aws-cdk/core';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import * as cdk from '@aws-cdk/core';
import * as appconfig from '@aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ArnPrincipal } from '@aws-cdk/aws-iam';

export class ExportingStack extends Stack {
    constructor(scope: App, id: string, props: StackProps) {
        super(scope, id, props);

        const table = new dynamodb.Table(this, id, {
            billingMode: dynamodb.BillingMode.PROVISIONED,
            readCapacity: 1,
            writeCapacity: 1,
            removalPolicy: cdk.RemovalPolicy.DESTROY,
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
            sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
            pointInTimeRecovery: true
        });

        const tablePolicy = new PolicyStatement({
            effect: Effect.ALLOW,
            resources: [table.tableArn],
            actions: ['*']
        });

        const role = new Role(this, 'tableRoleArn', {
            assumedBy: new ArnPrincipal(`arn:aws:iam::${Stack.of(this).account}:root`)
        });
        role.addToPolicy(tablePolicy);

        const app = '***';
        const environment = '***';
        const profile = '****';
        const strategy = '****';

        const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'myConfiguration', {
            applicationId: app,
            configurationProfileId: profile,
            contentType: 'application/json',
            content: JSON.stringify({
                tableArn: table.tableArn,
                tableName: table.tableName,
                tableRoleArn: role.roleArn
            }),
            description: 'table config'
        });

        const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
            applicationId: app,
            configurationProfileId: profile,
            environmentId: environment,
            configurationVersion: newConfig.ref,
            deploymentStrategyId: strategy
        });
    }
}