aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.5k stars 3.85k forks source link

Creating a codepipeline.Pipeline with a codebuild.PipelineProject in a different stack results in a circular reference #3300

Open corymhall opened 5 years ago

corymhall commented 5 years ago

Note: for support questions, please first reference our documentation, then use Stackoverflow. This repository's issues are intended for feature requests and bug reports.

I am trying to follow the example on this feature https://github.com/aws/aws-cdk/pull/1924 to create a codepipeline with codebuild actions that are created in a separate stack. When I do, I get this error:

Error: 'BuildStack' depends on 'PipelineStack' (BuildStack/CodeBuildProject/Role/DefaultPolicy/Resource -> PipelineStack/Pipeline/ArtifactsBucket/Resource.Arn). Adding this dependency (PipelineStack/Pipeline/ArtifactsBucketEncryptionKey/Resource -> BuildStack/CodeBuildProject/Role/Resource.Arn) would create a cyclic reference.

Here is my sample code that I am working with

#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import commit = require('@aws-cdk/aws-codecommit');
import pipeline = require('@aws-cdk/aws-codepipeline');
import pipeline_actions = require('@aws-cdk/aws-codepipeline-actions');
import codebuild = require ('@aws-cdk/aws-codebuild');

const region = 'us-east-2';
const account = '12345678';

const app = new cdk.App();

const pipelineStack = new cdk.Stack(app, `PipelineStack`, {
  env: {
    account: account,
    region: region
  }
});

const repo = new commit.Repository(pipelineStack, 'Repo', {
  repositoryName: 'test'
});

const sourceOutput = new pipeline.Artifact();
const sourceAction = new pipeline_actions.CodeCommitSourceAction({
  actionName: 'CodeCommit',
  repository: repo,
  output: sourceOutput,
  branch: 'master'
});

const codePipeline = new pipeline.Pipeline(pipelineStack, 'Pipeline', {
  pipelineName: 'test',
  stages: [
    {
      stageName: 'Source',
      actions: [sourceAction],
    }
  ],
});

const buildStack = new cdk.Stack(app, `BuildStack`, {
  env: {
    account: account,
    region: region
  }
});

const buildProject = new codebuild.PipelineProject(buildStack, 'CodeBuildProject', {
  projectName: 'test-build',
});

const buildAction = new pipeline_actions.CodeBuildAction({
  project: buildProject,
  actionName: 'CodeBuild',
  input: sourceOutput,
});

codePipeline.addStage({
  stageName: `Deploy-Environment`,
  actions: [buildAction]
});

Based off the feature https://github.com/aws/aws-cdk/pull/1924, I would expect to be able to create a codepipeline in one stack and then add a codebuild project created in a separate stack as a stage. Currently I am trying everything within the same account.

The use case is to be able to create a codepipeline in one account with build/deploy actions in a separate account. Most of the motivation is described in the feature https://github.com/aws/aws-cdk/pull/1924

Full error:

/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/stack.ts:272
        throw new Error(`'${stack.node.path}' depends on '${this.node.path}' (${dep.join(', ')}). Adding this dependency (${reason}) would create a cyclic reference.`);
              ^
Error: 'BuildStack' depends on 'PipelineStack' (BuildStack/CodeBuildProject/Role/DefaultPolicy/Resource -> PipelineStack/Pipeline/ArtifactsBucket/Resource.Arn). Adding this dependency (PipelineStack/Pipeline/ArtifactsBucketEncryptionKey/Resource -> BuildStack/CodeBuildProject/Role/Resource.Arn) would create a cyclic reference.
    at Stack.addDependency (/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/stack.ts:272:15)
    at CfnReference.consumeFromStack (/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/private/cfn-reference.ts:125:22)
    at Stack.prepare (/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/stack.ts:489:23)
    at Function.prepare (/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/construct.ts:84:28)
    at Function.synth (/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/construct.ts:41:10)
    at App.synth (/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/app.ts:128:36)
    at process.<anonymous> (/Users/user/cdk/demo/node_modules/@aws-cdk/core/lib/app.ts:111:45)
    at Object.onceWrapper (events.js:284:20)
    at process.emit (events.js:196:13)
    at process.EventEmitter.emit (domain.js:471:20)
skinny85 commented 5 years ago

Hey @corymhall ,

I know this sounds weird, but can you try to ignore this error for now, and actually try to have BuildStack in a different account, like in your desired end state? I have a suspicion it might actually work.

Let me know!

Thanks, Adam

corymhall commented 5 years ago

@skinny85 I can't seem to find any documentation that shows how to specify two sets of credentials, is this currently possible?

corymhall commented 5 years ago

@skinny85 just checking to make sure I wasn't missing anything here. Is it currently possible to deploy stacks to two separate accounts as part of the same app, or is that another issue?

or is it related to https://github.com/aws/aws-cdk/issues/3322?

skinny85 commented 5 years ago

Sorry Cory, I missed your message.

There's a few things you can do to have multiple account calls:

  1. Set the environment variables between each call:
$ export AWS_ACCESS_KEY_ID=...
$ export AWS_SECRET_ACCESS_KEY=...
$ cdk deploy Stack1
$ export AWS_ACCESS_KEY_ID=...
$ export AWS_SECRET_ACCESS_KEY=...
$ cdk deploy Stack2
  1. Use the profile argument:
$ cdk deploy Stack1 --profile account1
$ cdk deploy Stack2 --profile account2
corymhall commented 5 years ago

@skinny85 so the original issue that I raised was that there are dependencies between the two stacks. So in your example when I do the cdk deploy Stack1 I get an error message that I don't have credentials for dependent Stack2.

It seems like there needs to be a way of defining a profile per account, or a concept of providers like Terraform has.

skinny85 commented 5 years ago

Or, you're right, sorry I forgot - use the -e/--exclusively to work around the dependent Stacks (of course, if in your case Stack1 depends on Stack2, deploy Stack2 first).

skinny85 commented 5 years ago

@corymhall did you manage to get this working?

Sorry for the bad experience on the CLI - we're actively thinking on how to make it better (#3401).

tijoer commented 5 years ago

I am having the same problem :(. When I use the same stack the problem is solved, but I want to be able to deploy two separate stacks to different accounts.

skinny85 commented 5 years ago

I am having the same problem :(. When I use the same stack the problem is solved, but I want to be able to deploy two separate stacks to different accounts.

You should be able to do that by using physical names (no cycles should happen).

tijoer commented 5 years ago

Thanks for the answer. However I don't know what part needs to be named. In the above example from corymhall the stacks are explicitly named PipelineStack and BuildStack already.

skinny85 commented 4 years ago

@tijoer What is the error that you're getting?

wywarren commented 4 years ago

Getting the same error where I create the API Gateway in the base stack and pass it to the second stack to attach resources and lambda methods to it and it spits the similar cyclic references

skinny85 commented 4 years ago

@wywarren seeing the full error, and some of your code, would be helpful (the error usually contains the exact paths of the resources that cause the cycle).

skinny85 commented 4 years ago

@wywarren ping

github-actions[bot] commented 4 years ago

This issue has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.

mvn-anhle-hn commented 3 years ago

Is this issue just simply ignored? Or is it fixed? I'm getting the same problem and couldn't find any answer to it

skinny85 commented 3 years ago

@mlh-anhl it is not being actively worked on, but you're right, it shouldn't have been closed. Reopening.

What's the setup that's causing you problems?

sarbajitdutta commented 3 years ago

@skinny85 I have same issues with cdk 1.117.0 I have same issue where I have two separate stack for CodeBuild and CodePipeline and they have cyclic dependency. Even though clearly CodePipeline is dependent on CodeBuild.

ReCiterCdkCodeBuildStack reCiterCdkCodeBuildStack = new ReCiterCdkCodeBuildStack(this, "reCiterCdkCodeBuildStack", NestedStackProps.builder()
        .removalPolicy(RemovalPolicy.DESTROY)
        .build(), 
        reCiterCDKECSStack.getCluster(), reCiterCDKECRStack.getReCiterEcrRepo(), reCiterCDKECRStack.getReCiterPubmedEcrRepo(), reCiterCDKECRStack.getReCiterScopusEcrRepo(), reCiterCDKECRStack.getReCiterPubManagerEcrRepo());
        reCiterCdkCodeBuildStack.addDependency(reCiterCDKECRStack);
        reCiterCdkCodeBuildStack.addDependency(reCiterCDKECSStack);

       ReCiterCdkCodepipelineStack reCiterCdkCodepipelineStack = new ReCiterCdkCodepipelineStack(this, "reCiterCdkCodepipelineStack", NestedStackProps.builder()
        .removalPolicy(RemovalPolicy.DESTROY)
        .build(),
        reCiterCdkCodeBuildStack.getPubmedCodeBuildProject(), reCiterCdkCodeBuildStack.getScopusCodeBuildProject(), reCiterCdkCodeBuildStack.getReCiterCodeBuildProject(), reCiterCdkCodeBuildStack.getReCiterPubManagerCodeBuildProject(),
        reCiterCDKECSStack.getReCiterTopic(), 
        reCiterCDKECSStack.getReCiterPubmedService(), reCiterCDKECSStack.getReCiterScopusService(), reCiterCDKECSStack.getReCiterService(), reCiterCDKECSStack.getReCiterPubManagerService());
        reCiterCdkCodepipelineStack.addDependency(reCiterCdkCodeBuildStack);

Issue -

ReCiterCdkStack (ReCiterCdkMasterStack) failed: Error [ValidationError]: Circular dependency between resources: [reCiterCdkCodepipelineStackNestedStackreCiterCdkCodepipelineStackNestedStackResource75DD9391, reCiterCdkCodeBuildStackNestedStackreCiterCdkCodeBuildStackNestedStackResource2D9C609D]
    at Request.extractError (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/protocol/query.js:50:29)
    at Request.callListeners (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/request.js:688:14)
    at Request.transition (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/request.js:690:12)
    at Request.callListeners (/usr/local/lib/node_modules/aws-cdk/node_modules/aws-sdk/lib/sequential_executor.js:116:18) {
  code: 'ValidationError',
  time: 2021-08-10T17:37:43.071Z,
  requestId: 'c15599ee-a7e5-4953-8090-7d9eac967e57',
  statusCode: 400,
  retryable: false,
  retryDelay: 914.8160473349886
}
Circular dependency between resources: [reCiterCdkCodepipelineStackNestedStackreCiterCdkCodepipelineStackNestedStackResource75DD9391, reCiterCdkCodeBuildStackNestedStackreCiterCdkCodeBuildStackNestedStackResource2D9C609D]
skinny85 commented 3 years ago

@sarbajitdutta can you show more of your code? In particular, what is ReCiterCdkCodeBuildStack and ReCiterCdkCodepipelineStack.

sarbajitdutta commented 3 years ago

@skinny85 They are separate stacks. One for all the CodeBuild projects and one for the CodePipeline stack which uses the CodeBuild projects. I had separate stacks declared and with dependency of CodeBuild stack for CodePipeline expressed as -

ReCiterCdkCodeBuildStack reCiterCdkCodeBuildStack = new ReCiterCdkCodeBuildStack(this, "reCiterCdkCodeBuildStack", NestedStackProps.builder()
        .removalPolicy(RemovalPolicy.DESTROY)
        .build(), 
        reCiterCDKECSStack.getCluster(), reCiterCDKECRStack.getReCiterEcrRepo(), reCiterCDKECRStack.getReCiterPubmedEcrRepo(), reCiterCDKECRStack.getReCiterScopusEcrRepo(), reCiterCDKECRStack.getReCiterPubManagerEcrRepo());
        reCiterCdkCodeBuildStack.addDependency(reCiterCDKECRStack);
        reCiterCdkCodeBuildStack.addDependency(reCiterCDKECSStack);

       ReCiterCdkCodepipelineStack reCiterCdkCodepipelineStack = new ReCiterCdkCodepipelineStack(this, "reCiterCdkCodepipelineStack", NestedStackProps.builder()
        .removalPolicy(RemovalPolicy.DESTROY)
        .build(),
        reCiterCdkCodeBuildStack.getPubmedCodeBuildProject(), reCiterCdkCodeBuildStack.getScopusCodeBuildProject(), reCiterCdkCodeBuildStack.getReCiterCodeBuildProject(), reCiterCdkCodeBuildStack.getReCiterPubManagerCodeBuildProject(),
        reCiterCDKECSStack.getReCiterTopic(), 
        reCiterCDKECSStack.getReCiterPubmedService(), reCiterCDKECSStack.getReCiterScopusService(), reCiterCDKECSStack.getReCiterService(), reCiterCDKECSStack.getReCiterPubManagerService());
        reCiterCdkCodepipelineStack.addDependency(reCiterCdkCodeBuildStack);

As you can see ReCiterCdkCodepipelineStack takes all the codebuild projects as stack paramaters.

The only option to circumvent the issue was to put both CodeBuild and CodePipeline in same stack like I did here - code

sarbajitdutta commented 3 years ago

@skinny85 I think this is bug in the system and should be addressed.

skinny85 commented 3 years ago

Can you please post a minimal reproduction that allows me to get the Circular dependency between resources: [reCiterCdkCodepipelineStackNestedStackreCiterCdkCodepipelineStackNestedStackResource75DD9391, reCiterCdkCodeBuildStackNestedStackreCiterCdkCodeBuildStackNestedStackResource2D9C609D] error locally?

cn-sws commented 3 years ago

I have a very similar issue. I have created a Role in one stack and then create a Pipeline in a second stack. I get the circular dependency. I guess when adding the Role to the Pipeline it is trying to add some permission to the Role. stack 1: codePipelineRole = new Role(scope, "iamrCodePipeline", RoleProps.builder() .roleName("cpPipeline_DEV") .assumedBy(new ServicePrincipal("codepipeline")) .inlinePolicies(pds) .build()); stack 2: Pipeline.Builder.create(scope, "cpWeb") .pipelineName("cpWeb") .role(getCodePipelineDetails().getCodePipelineRole()) .artifactBucket(getCodePipelineDetails().getCpBucket()) .stages(Arrays.asList(stgSource, stgBuild)) .build(); getCodePipelineDetails gets the resources created in stack 1 stgSource and stgBuild are built in stack 2 along with the Pipeline.

LeoHsu0802 commented 2 years ago

Hi @skinny85, I have a similar issue to @cn-sws, when I create a role in role stack for codebuild project useage and use this role in another codebuild stack then I get a circular dependency error.

AreebSiddiqui commented 2 years ago

CodePipeline: CodeBuildAppStack' depends on 'CodePipelineAppStack' cyclic reference I have two stacks. One for CodeBuild and another for CodePipeline. I have been passing CodeBuild Stack into the CodePipeline Stack.

const codebuildStack = new CodeBuildStack(app, 'CodeBuildAppStack');

new CodePipeline(app, 'CodePipelineAppStack', codebuildStack);

This Gives me an error:

/node_modules/aws-cdk-lib/core/lib/stack.ts:395 throw new Error('${target.node.path}' depends on '${this.node.path}' (${cycle.join(', ')}). Adding this dependency (${reason}) would create a cyclic reference.);

The CodeBuildStacks works fine alone but once I pass this stack in CodePipelineStack the error pops up.

CodePipeline.ts

export class CodePipeline extends Stack {
  public readonly SourceStage: codepipeline.IStage;
  public readonly BuildStage: codepipeline.IStage;
  public readonly DeployStage: codepipeline.IStage;
  public readonly codePipelineRole: iam.Role;

  constructor(scope: Construct, id: string, codeBuild: CodeBuildStack, props?: StackProps) {
    super(scope, id, props)
    this.codePipelineRole = new iam.Role(this, `CodePipelineRole`, {
      roleName: `CodePipelineRole`,
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'AmazonEC2ContainerRegistryPowerUser',
        ),
      ]
    });

    const pipeline = new codepipeline.Pipeline(this, `MyCodePipeline`, {
      pipelineName: 'my-codepipeline',
      role:this.codePipelineRole,
      crossAccountKeys: false,
    });

    const sourceOutput = new codepipeline.Artifact();

    this.SourceStage = pipeline.addStage({
      stageName: 'Source',
      actions: [
        new codepipelineActions.CodeStarConnectionsSourceAction({
          actionName: 'Source',
          owner: `onwername`,
          repo: 'reponame',
          connectionArn:'my-arn',
          triggerOnPush:true,
          output: sourceOutput
        })
      ],
    })

    this.BuildStage = pipeline.addStage({
      stageName: 'Build',
      placement: {
        justAfter: this.SourceStage
      },
      actions: [
        new codepipelineActions.CodeBuildAction({
          actionName:'CodeBuild',
          input:sourceOutput,
          project: codeBuild.projectABC
        })
      ]
    })
  }
}

CDK CLI Version 2.8.0 (build 8a5eb49)

Framework Version No response

Node.js Version v16.6.2

OS Ubuntu 20.04.1 LTS (fossa-beric-tgl X40)

Language Typescript

skinny85 commented 2 years ago

@AreebSiddiqui can you show the entire message that you get? (Pretty sure you cut out the most important part 😛).

AreebSiddiqui commented 2 years ago

@skinny85 Here it is :p

/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/stack.ts:395 throw new Error('${target.node.path}' depends on '${this.node.path}' (${cycle.join(', ')}). Adding this dependency (${reason}) would create a cyclic reference.); ^ Error: 'CodeBuildAppStack' depends on 'CodePipelineAppStack' (CodeBuildAppStack -> CodePipelineAppStack/iacdevs-grocer-api-codepipeline/ArtifactsBucket/Resource.Arn). Adding this dependency (CodePipelineAppStack -> CodeBuildAppStack/grocer-api/Resource.Ref) would create a cyclic reference. at CodePipeline._addAssemblyDependency (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/stack.ts:395:13) at Object.addDependency (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/deps.ts:52:20) at CodePipeline.addDependency (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/stack.ts:266:5) at resolveValue (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/private/refs.ts:100:12) at Object.resolveReferences (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/private/refs.ts:30:24) at Object.prepareApp (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/private/prepare-app.ts:30:3) at Object.synthesize (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/private/synthesis.ts:32:3) at App.synth (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/stage.ts:90:23) at process. (/home/areebsiddiqui/a-cdk/node_modules/aws-cdk-lib/core/lib/app.ts:64:45) at Object.onceWrapper (node:events:514:26)

skinny85 commented 2 years ago

@AreebSiddiqui there are a few ways you can break this dependency. The simplest might be to move the CodePipeline's S3 Bucket to the same Stack as your CodeBuild Project:

export class CodeBuildStack extends Stack {
    public readonly projectABC: codebuild.IProject;
    public readonly artifactBucket: s3.IBucket;

    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);

        this.projectABC = new codebuild.PipelineProject(this, 'PipelineProject');
        this.artifactBucket = new s3.Bucket(this, 'ArtifactBucket');
    }
}

export class CodePipelineStack extends Stack {
    public readonly SourceStage: codepipeline.IStage;
    public readonly BuildStage: codepipeline.IStage;
    public readonly DeployStage: codepipeline.IStage;
    public readonly codePipelineRole: iam.Role;

    constructor(scope: Construct, id: string, codeBuild: CodeBuildStack, props?: StackProps) {
        super(scope, id, props)
        this.codePipelineRole = new iam.Role(this, `CodePipelineRole`, {
            roleName: `CodePipelineRole`,
            assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
            managedPolicies: [
                iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryPowerUser'),
            ],
        });

        const pipeline = new codepipeline.Pipeline(this, `MyCodePipeline`, {
            pipelineName: 'my-codepipeline',
            role: this.codePipelineRole,
            crossAccountKeys: false,
            artifactBucket: codeBuild.artifactBucket,
        });

        const sourceOutput = new codepipeline.Artifact();
        this.SourceStage = pipeline.addStage({
            stageName: 'Source',
            actions: [
                new codepipelineActions.CodeStarConnectionsSourceAction({
                    actionName: 'Source',
                    owner: `onwername`,
                    repo: 'reponame',
                    connectionArn:'my-arn',
                    triggerOnPush:true,
                    output: sourceOutput,
                }),
            ],
        });
        this.BuildStage = pipeline.addStage({
            stageName: 'Build',
            actions: [
                new codepipelineActions.CodeBuildAction({
                    actionName: 'CodeBuild',
                    input: sourceOutput,
                    project: codeBuild.projectABC,
                }),
            ],
        });
    }
}

const app = new App();
const codebuildStack = new CodeBuildStack(app, 'CodeBuildAppStack');
new CodePipelineStack(app, 'CodePipelineAppStack', codebuildStack);
skinny85 commented 2 years ago

(BTW, your pipeline Role is wrong - it trusts the CodeBuild service principal, instead of the CodePipeline one. I would suggest letting CDK manage it for you - just remove the role property when creating the Pipeline construct)

AreebSiddiqui commented 2 years ago

@skinny85 This was a lifesaver, thank you so much. It's because of people like you that the aws community is ever going, keep up the good work really appreciate it. Thanks again!