如何使用 AWS-CDK 将 LambdaStack 向下传递到 PipelineStage

How to pass a LambdaStack down to a PipelineStage with AWS-CDK

使用 AWS-CDK 时,如何将 Stack 向下传递到管道阶段?

我目前正在尝试创建一个可以将堆栈作为输入的管道。

我参加了 aws-cdk 研讨会并拥有一个可以自我更新并可以部署预打包 lambda 的管道,但我正在尝试创建一个管道构造库,以便我的团队可以创建一个新的管道实例并传入后来创建的堆栈,其中添加了相关角色和事件规则。

我当前的代码如下:

流水线-stack.ts

import {
  Stack,
  StackProps,
  pipelines,
  DefaultStackSynthesizer,
  SecretValue,
  aws_codebuild as codebuild,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { PipelineStage } from './pipeline-stage';
export type PipelineStackProps = StackProps;

export interface Environment {
  readonly name: string;
  readonly accountNumber: string;
}

export class PipelineStack extends Stack {
  constructor(scope: Construct, id: string, props: PipelineStackProps,
    environments: Environment[], repoName: string, serviceName: string, lambdaPath: string, policies: Array<string>) {
    super(scope, id, props);

    new DefaultStackSynthesizer({
      deployRoleArn: 'arn:aws:iam::{ACCOUNTID}:role/service-role/ops-codepipeline-role',
    });

    const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
      pipelineName: serviceName,
      synth: new pipelines.CodeBuildStep('Synth', {
        input: pipelines.CodePipelineSource.gitHub(`company_repo/${repoName}`, 'main', {
          authentication: SecretValue.secretsManager('github-oauth-token', { jsonField: 'OAUTH_TOKEN' }),
        }),
        buildEnvironment: {
          environmentVariables: {
            NPM_TOKEN: {
              value: '/codepipeline/npm_token',
              type: codebuild.BuildEnvironmentVariableType.PARAMETER_STORE,
            },
          },
        },

        commands: [
          'echo "@company:registry=https://npm.pkg.github.com/" > .npmrc',
          'echo "//npm.pkg.github.com/:_authToken=\${NPM_TOKEN}" >> .npmrc',
          'npm ci',
          'npm run build',
          'npm test',
          'npx cdk synth',
          'pip install -r lambda/requirements.txt -t lambda',
        ],
      }),
      crossAccountKeys: true,
    });

    for (const environment of environments) {
      const environmentName = environment.name;
      const envAccountNumber = environment.accountNumber;
      pipeline.addStage(
        new PipelineStage(this, environmentName, {
          env: {
            account: envAccountNumber,
            region: 'eu-west-2',
          },
        }, lambdaPath, policies),
      );
    }
  }
}

管道-stage.ts

import { Stage, StageProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { LambdaStack } from './lambda-stack';
export class PipelineStage extends Stage {
  constructor(scope: Construct, id: string, props: StageProps, lambdaPath: string, policies: Array<string>) {
    super(scope, id, props);

    new LambdaStack(this, 'LambdaStack', lambdaPath, policies);
  }
}

lambda-stack.ts

import {
  Stack,
  StackProps,
  aws_lambda as lambda,
  aws_events as events,
  aws_events_targets as targets,
  aws_iam as iam,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';


export class LambdaStack extends Stack {
  constructor(scope: Construct, id: string, lambdaPath: string, policies: Array<string>, props?: StackProps) {
    super(scope, id, props);

    const role = new iam.Role(this, 'LambdaCleanupRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });

    for (const policy of policies) {
      role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName(policy));
    }
    // The code that defines your stack goes here
    const fn = new lambda.Function(this, 'lambda-cleanup', {
      runtime: lambda.Runtime.PYTHON_3_8,
      handler: 'app.handler',
      code: lambda.Code.fromAsset(lambdaPath),
      role,
    });

    const rule = new events.Rule(this, 'Schedule Rule', {
      schedule: events.Schedule.expression('rate(1 day)'),
    });

    rule.addTarget(new targets.LambdaFunction(fn));
  }
}

建议的解决方案如下:

流水线-stack.ts

export class PipelineStack extends Stack {
  constructor(scope: Construct, id: string, props: PipelineStackProps,
    environments: Environment[], repoName: string, serviceName: string, lambdaPath: string, policies: Array<string>, lambdaStack: Stack) {
    super(scope, id, props);

// pipeline implementation here

const stage = pipeline.addStage(
        new PipelineStage(this, environmentName, {
          env: {
            account: envAccountNumber,
            region: 'eu-west-2',
          },
        }, lambdaPath, policies, lambdaStack),
      );

管道-stage.ts

export class PipelineStage extends Stage {
  constructor(scope: Construct, id: string, props: StageProps, lambdaPath: string, policies: Array<string>, lambdaStack: Stack) {
    super(scope, id, props);
    lambdaStack
  }
}

但是这样做会导致 The given Stage construct ('Default/BymilesLambdaPipelineStack/dev') should contain at least one Stack 错误

团队的最终目标是从我们的注册表中导入一个 npm 包并执行类似于以下的操作:

lambda-部署-stack.ts

import { LambdaStack } from './lambda-stack.ts'
import { PipelineStack } from '@company-register/cdk-pipeline-python'

const app = new App();

const policies: string[] = [
      'service-role/AWSLambdaBasicExecutionRole',
      'AWSLambda_FullAccess',
    ];

new PipelineStack(app, 'LambdaCleanupPipeline', {
  env: {
    region: 'eu-west-2',
  },
}, [
  { name: 'Test', accountNumber: '11111111' },
  { name: 'Ops', accountNumber: '22222222' }],
  'bymiles-lambda-cleanup-cdk',
  'bymiles-lambda-cleanup-cdk',
  new LambdaStack(stack, 'BymilesLambdaStack', 'test/test_lambda', policies)
);

app.synth();

我对提议的解决方案的理解:实施者应该将 LambdaStack 传递给共享的 PipelineStackPipelineStage 是一个实现细节 管道。

lambda-deployment-stack.ts 中,实现者将带有签名 (scope: cdk.Stage) => void 的函数传递给通用 PipelineStack。 此 dependency injection 模式有两个目的:(1) 它将堆栈构造推迟到阶段范围可用,以及 (2) 它封装了与管道无关的策略和其他细节。

// lambda-deployment-stack.ts
// PipelineStack accepts this function signature as a prop
// defers the lambda stack creation until the stage scope is available
const makeLambdaStack = (scope: cdk.Stage): void => {
  new LambdaStack(scope, 'BymilesLambdaStack', 'test/test_lambda', policies);
};

不接受 PipelineStack 的 LambdaStack、策略和路径,PipelineStack 将包装函数作为 stackMaker 道具。

// pipeline-stack.ts
export class PipelineStack extends Stack {
  constructor(scope: Construct, id: string, props: PipelineStackProps,
    environments: Environment[], repoName: string, serviceName: string,
    stackMaker: (scope: cdk.Stage) => void) {
        super(scope, id, props);
        // etc...

Pipeline 依次将 makeLambdaStack 向下传递到 PipelineStage,在该处调用函数并实际构建 Lambda。

// pipeline-stage.ts
export class PipelineStage extends Stage {
  constructor(
    scope: Construct,
    id: string,
    props: StageProps,
    stackMaker: (scope: cdk.Stage) => void) {
      super(scope, id, props);
      props.stackMaker(this)  // <- actually call the maker function, in the sceope of the Pipeline Stage
  }
}