Home > other >  How to pass a LambdaStack down to a PipelineStage with AWS-CDK
How to pass a LambdaStack down to a PipelineStage with AWS-CDK

Time:12-31

How would I pass a Stack down to a pipeline stage when using AWS-CDK?

I'm currently trying to create a pipeline which could take a stack as an input.

I've followed the aws-cdk workshop and have a pipeline which self-updates and can deploy a pre-packaged lambda but I'm trying to create a pipeline construct library so that my team can just create a new instance of a pipeline and pass in a later created stack which has relevant roles and event rules added.

My current code is below:

pipelines-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),
      );
    }
  }
}

pipeline-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));
  }
}

Proposed solution is below:

pipelines-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),
      );

pipeline-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
  }
}

But doing this results in a The given Stage construct ('Default/BymilesLambdaPipelineStack/dev') should contain at least one Stack error

The final goal would be for the team to import an npm package from our registry and do something similar to below:

lambda-deployment-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();

CodePudding user response:

My understnding of the proposed solution: implementers should pass a LambdaStack to a shared PipelineStack. PipelineStage is an implementation detail of the pipeline.

In lambda-deployment-stack.ts, implementers pass a function with the signature (scope: cdk.Stage) => void to the common PipelineStack. This dependency injection pattern has two purposes: (1) it defers stack construction until the stage scope is available and (2) it encapsulates the policy and other details of no concern to the pipeline.

// 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);
};

Instead of accepting the LambdaStack, policies and paths to the PipelineStack, have PipelineStack take the wrapper function as a stackMaker prop.

// 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...

Pipelinein turn passes makeLambdaStack down to the PipelineStage, where the function is called and the Lambda actually constructed.

// 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
  }
}
  • Related