Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StackSet/Conformance Packs support - make stacks available as an asset #11896

Open
1 of 2 tasks
pgarbe opened this issue Dec 6, 2020 · 26 comments
Open
1 of 2 tasks

StackSet/Conformance Packs support - make stacks available as an asset #11896

pgarbe opened this issue Dec 6, 2020 · 26 comments
Labels
@aws-cdk/core Related to core CDK functionality effort/medium Medium work item – several days of effort feature-request A feature should be added or improved. p2

Comments

@pgarbe
Copy link
Contributor

pgarbe commented Dec 6, 2020

A stack should be made available as an asset. At the moment it's not possible to add the generated template file as asset in another stack, as the file does not exist at this point in time.

Use Case

If you want to deploy a StackSet or ServiceCatalog Product (in a pipeline) two stacks are used. One stack contains the StackSet construct and the other stack (let's call it template stack) is the one which should be deployed in target accounts via StackSet. This template stack will never deployed directly but just synthesized. The StackSet stack needs an s3 url of the template stack.

This could be also related to an integration with Proton where it's also the synthesized stack template needs to be available as asset as well.

Proposed Solution

The usage could look like this:

export interface StackSetStackProps extends cdk.StackProps {
  stack: cdk.Stack
  ...
}

export class StackSetStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: StackSetStackProps) {
    super(scope, id, props);

    new cfn.CfnStackSet(this, 'StackSet', {
      ...
      templateUrl: new s3assets.Asset(props.stack),
    });
  }
}

Not sure how the implementation could look like.

Other

  • 👋 I may be able to implement this feature request
  • ⚠️ This feature might incur a breaking change

This is a 🚀 Feature Request

@pgarbe pgarbe added feature-request A feature should be added or improved. needs-triage This issue or PR still needs to be triaged. labels Dec 6, 2020
@github-actions github-actions bot added the @aws-cdk/assets Related to the @aws-cdk/assets package label Dec 6, 2020
@eladb
Copy link
Contributor

eladb commented Dec 8, 2020

@pgarbe I am curious if you can use addFileAsset() to achieve this?

export interface StackSetStackProps extends cdk.StackProps {
  stack: cdk.Stack
  ...
}

export class StackSetStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: StackSetStackProps) {
    super(scope, id, props);

    const template = stack.addFileAsset({
      packaging: cdk.FileAssetPackaging.FILE,
      sourceHash: 'git-commit',
      fileName: props.stack.templateFile,
    });

    new cfn.CfnStackSet(this, 'StackSet', {
      ...
      templateUrl: template.s3ObjectUrl
    });
  }
}

I'd be interested in adding official support for StackSets so let's figure the right model and add it.

@eladb
Copy link
Contributor

eladb commented Dec 8, 2020

Copy @rix0rrr

@eladb eladb changed the title (@aws-cdk/assets): make stacks available as an asset (assets): make stacks available as an asset Dec 8, 2020
@eladb eladb changed the title (assets): make stacks available as an asset (assets): StackSet/ServiceCatalog support - make stacks available as an asset Dec 8, 2020
@eladb eladb added effort/medium Medium work item – several days of effort p1 labels Dec 8, 2020
@pgarbe
Copy link
Contributor Author

pgarbe commented Dec 8, 2020

@eladb Thanks, that actually works :) I'm not just sure what could be a good source hash. Will look into that.

@eladb
Copy link
Contributor

eladb commented Dec 8, 2020

That's definitely the tricky part. You could use the current git commit in production but for dev iterations you'd need something else

@hoegertn
Copy link
Contributor

hoegertn commented Dec 8, 2020

I really like this solution. And it makes so much sense if you see it ;-)

I would love to have this wrapped in a construct so this magic does not need to be copied.

The hash might really be tricky.

@pgarbe
Copy link
Contributor Author

pgarbe commented Dec 8, 2020

Only the synth part works. Within a pipeline the upload action is missing. How can I enforce that?

@eladb
Copy link
Contributor

eladb commented Dec 8, 2020

Only the synth part works. Within a pipeline the upload action is missing. How can I enforce that?

What do you mean only the synth part works?

Can you paste the manifest.json file in your cloud assembly?

@SomayaB SomayaB removed the needs-triage This issue or PR still needs to be triaged. label Dec 8, 2020
@pgarbe
Copy link
Contributor Author

pgarbe commented Dec 8, 2020

Previously, I had issues that synth failed to circular dependencies of the PipelineStack and StackSetStack. With your snippet this has been solved and it synthesized the correct template url.

But the pipeline does not contain a stage to upload the assets. Here's a sample project: https://github.com/pgarbe/cdk-stackset

@pgarbe
Copy link
Contributor Author

pgarbe commented Dec 9, 2020

It might be useful to upload the synthesized stack into a separate bucket as there might be a different lifecycle. For me assets (and the assets bucket) are ephemeral and I can recreate everything needed when I run the pipeline. But in case of a service catalog product it's needed to keep different versions of a template for a longer time.

@mrpackethead
Copy link

mrpackethead commented Jun 28, 2021

Multipel +1 on this.

I have quite a few stacks i want to deploy to every account in our org.

@deyceg
Copy link

deyceg commented Jun 28, 2021

It might be useful to upload the synthesized stack into a separate bucket as there might be a different lifecycle. For me assets (and the assets bucket) are ephemeral and I can recreate everything needed when I run the pipeline. But in case of a service catalog product it's needed to keep different versions of a template for a longer time.

Theres a good chance StackSets are managed by centralized teams so a separate bucket would make a lot sense with different access permissions. The initial bootstrap for CDK is targeted at teams deploying workloads IMO but alot of enterprises will have governance models that would prevent this e.g. multi-tenanted account structure where customers can only deploy in to their accounts, but admins can deploy stacks in to all accounts. A trivial example might be a ChatOps stack.

@mrpackethead
Copy link

mrpackethead commented Jun 29, 2021

@eladb and everyone else.

I've just been standing up some stack sets using the L1 construct.. Just some thoughts about using that ( and pipelines )

  • I've created another role in my bootstrap to be the 'cdk-stackset-execution-role' that is used. Thats got a level of complicaiton around it.. My pipelines run in one account, but i never deploy any stacks in that account.. Now the account where the CF template that holds teh stack sets needs to be trusted by 'target' environments.. Not a show stopper, but not ideal.

  • So far the stacks that are being deployed by my stacksets are trivial in nature.. I've got a stack for pushing out a set of common prefix lists, ( prefix lists shared by RAMS are invisible to the API! thats a different topic though ). I've got a stack that associates R53 Private Hosted Zones ( that were shared via RAMS ) to VPC's..
    These stacks are so simple that i was able to 'craft' Cloudformation.. ( example )..

	cdkqualifier = parameters['env_parameters']['CdkQualifier']    # TODO. Load this from cdk.json
	execution_role_name = f'cdk-${cdkqualifier}-stacksetExecution-${account}-${self.region}'
		

for vpc in vpc_to_use:
				
				routeresolver_rules = {}
				for resolver_rule in resolver_rules:
					routeresolver_rules[resolver_rule['Name']] = {
						'Type': 'AWS::Route53Resolver::ResolverRuleAssociation',
						'Properties': {
							'ResolverRuleId': resolver_rule['Id'],
							'VPCId': vpc['VpcId']
						}
					}
			

				stack_set = cfn.CfnStackSet(self, 'stackset',
					permission_model= 'SELF_MANAGED',
					stack_set_name= 'Route53Assn',
					administration_role_arn= stack_set_administrator.arn,
					execution_role_name=  execution_role_name,
					stack_instances_group = [
						{
							'Regions': self.region,		#list of regions
							'DeploymentTargets': {
								'Accounts': account		#list of accounts
							}
						}
					], 
					template_body= {'Resources': routeresolver_rules}
				)

For non trivial stacks this becomes really limiting.. I'd want to be able to create stack sets more natively with Cdk.. Sure i can create them, and copy them from cdk.out to a bucket by hand, but thats going to be quite a few layers of hackery... ( not impossible though )...

@0xjjoyy
Copy link

0xjjoyy commented Aug 20, 2021

Making the stack available as an asset use cases is also needed for AWS Config conformance packs.

https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-config.CfnOrganizationConformancePack.html

@s0enke
Copy link

s0enke commented Nov 22, 2021

But the pipeline does not contain a stage to upload the assets. Here's a sample project: https://github.com/pgarbe/cdk-stackset

@pgarbe I got it working by utilizing the new Service Catalog ProductStack support. This way, the asset for the stack set is also uploaded during cdk deploy:

class DeployedViaStackSet extends servicecatalog.ProductStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new aws_codecommit.Repository(this, 'CodeRepo', {
      repositoryName: "example" 
    })
  }
}

export class CdkStack extends Stack {

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    
    const deployedViaStackSet = new DeployedViaStackSet(this, 'DeployedViaStackSet');

    new CfnStackSet(this, 'StackSet', {
      templateUrl: servicecatalog.CloudFormationTemplate.fromProductStack(DeployedViaStackSet).bind(this).httpUrl,
...
    });
  }
}

@skinny85
Copy link
Contributor

@s0enke that's great, that's exactly what this functionality is for 🙂.

The documentation is here: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-servicecatalog-readme.html#creating-a-product-from-a-stack

I will close this issue as "done", if anyone runs into problems and can't use the ServiceCatalog ProductStack mentioned above, please leave a comment (or open a new issue)!

Thanks,
Adam

@github-actions
Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

@fitzoh
Copy link

fitzoh commented Nov 22, 2021

@skinny85 that makes it pretty clear how to manage Service Catalog products , but doesn't provide a clear solution for stacksets, so that covers half the issue.

It also doesn't handle generic usage such as AWS config conformance packs as mentioned here

@skinny85
Copy link
Contributor

Hmm... could the solution be as simple as adding a class to the @aws-cdk/core library that's very similar in capabilities to the ServiceCatalog's ProductStack?

@s0enke
Copy link

s0enke commented Nov 23, 2021

@skinny85 @fitzoh I assume that Conformance Packs can be populated in the same way, but I did not test it (they are no real CFN templates, but a subset IIRC):

class DeployedViaConformancePack extends servicecatalog.ProductStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new aws_config_rule(...)
  }
}

export class CdkStack extends Stack {

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    
    const deployedViaConformancePack = new DeployedViaConformancePack(this, 'DeployedViaConformancePack');

const cfnOrganizationConformancePack = new config.CfnOrganizationConformancePack(this, 'MyCfnOrganizationConformancePack', {
  ...
  templateS3Uri: servicecatalog.CloudFormationTemplate.fromProductStack( DeployedViaConformancePack).bind(this).httpUrl,
});
  }
}

I agree that it looks a bit more like a workaround/hack, since we are utilizing something from the Service Catalog Construct, so a convenience method/construct in the core might make sense. I count four occurrences which work with CFN templates under the hood: Service Catalog, Stack Sets, and Conformance Packs, and Organizational Conformance Packs.

@skinny85 skinny85 added @aws-cdk/core Related to core CDK functionality and removed @aws-cdk/assets Related to the @aws-cdk/assets package labels Nov 23, 2021
@skinny85 skinny85 changed the title (assets): StackSet/ServiceCatalog support - make stacks available as an asset (assets): StackSet/Conformance Packs support - make stacks available as an asset Nov 23, 2021
@skinny85 skinny85 changed the title (assets): StackSet/Conformance Packs support - make stacks available as an asset StackSet/Conformance Packs support - make stacks available as an asset Nov 23, 2021
@github-actions github-actions bot added the @aws-cdk/aws-servicecatalog Related to AWS Service Catalog label Nov 23, 2021
@skinny85
Copy link
Contributor

Ok. I've edited the title of the issue, let's re-open this one.

@skinny85 skinny85 reopened this Nov 23, 2021
@skinny85 skinny85 removed the @aws-cdk/aws-servicecatalog Related to AWS Service Catalog label Nov 23, 2021
@skinny85 skinny85 removed their assignment Nov 23, 2021
@skinny85
Copy link
Contributor

BTW, contributions are always welcome 😉.

@mrpackethead
Copy link

Same topic.... ( request for conformance packs.. ).
#16682

I solved this problem by creating two apps, the first one creates templates, synths them and sticks them in cfassests.out. The 'main' stack, picks those templates which become 'assests'.. This

    cfassests = cdk.App(outdir='cfassests.out')
    createtemplates(cfassests, 'createtemplates'
    cfassests.synth()

#Create the main application.
app = cdk.App()
Dosomething(app, 'do something')
app.synth()

@peterb154
Copy link
Contributor

    const template = stack.addFileAsset({
      packaging: cdk.FileAssetPackaging.FILE,
      sourceHash: 'git-commit',
      fileName: props.stack.templateFile,
    });

This is AWESOME.. Maybe I am missing something @eladb ? Wy wouldn't you just use the hash of the synthesized template as the sourceHash?

@eladb
Copy link
Contributor

eladb commented Dec 26, 2021

@peterb154 when this code is executed there isn't a synthesized template yet :-)

@ArielPrevu3D
Copy link

ArielPrevu3D commented Mar 15, 2022

I ended up doing something absolutely horrible to make assets work with StackSet stack instances. Had to change the staging bucket encryption by using a custom bootstrap template. One stackset per region because the bucket is passed in parameters. One stack with all the stacksets to allow parallel deployment. Let me know if you guys find a better way.

class ComplianceStackInstance extends Stack {
  constructor(scope: Construct) {
    super(scope, 'Custom-Config-Rules', {
      env: {
        region: MAIN_REGION,
        account: ACCOUNTS.Root.Management.toString(),
      },
    });
    new AccountDefaults(this);
  }
}

const DEPLOYMENT_ORG_UNITS = [ORGUNITS.Development];

class ComplianceAssetStack extends Stack {
  readonly toolkitStagingBucket: IBucket;

  constructor(
    scope: App,
    env: Environment,
    region: string,
    virtualStackArtifact: CloudFormationStackArtifact,
  ) {
    super(scope, `${env.stage}-ComplianceAssets-${region}`, {
      env: {
        account: ACCOUNTS.Root.Management.toString(),
        region,
      },
    });
    for (const asset of virtualStackArtifact.assets) {
      if (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY)
        this.synthesizer.addFileAsset({
          packaging: FileAssetPackaging.ZIP_DIRECTORY,
          fileName: asset.path,
          sourceHash: asset.sourceHash,
        });
    }
    this.toolkitStagingBucket = Bucket.fromBucketName(
      this,
      'CDKStagingBucket',
      (MANAGEMENT_STAGING_BUCKETS as Record<string, string>)[this.region],
    );

    const pol = new BucketPolicy(this, 'CDKStagingBucketPolicy', {
      bucket: this.toolkitStagingBucket,
    });

    pol.document.addStatements(
      new PolicyStatement({
        actions: ['s3:GetObject'],
        resources: virtualStackArtifact.assets.map((asset) =>
          this.toolkitStagingBucket.arnForObjects(
            `assets/${asset.sourceHash}.zip`,
          ),
        ),
        principals: [new AnyPrincipal()],
        conditions: {
          'ForAnyValue:StringEquals': {
            'aws:PrincipalOrgPaths': DEPLOYMENT_ORG_UNITS.map(
              (ou) => `${ORGID}/${ORGUNITS.Root}/${ou}/`,
            ),
          },
        },
      }),
    );
  }
}

export class ComplianceStack extends Stack {
  constructor(scope: App, env: Environment) {
    super(scope, `${env.stage}-Compliance`, {
      env: {
        region: MAIN_REGION,
        account: ACCOUNTS.Root.Management.toString(),
      },
      terminationProtection: env.isProductionStage,
    });

    const app = new App();
    const virtualComplianceStack = new ComplianceStackInstance(app);

    const templateAsset = this.synthesizer.addFileAsset({
      packaging: FileAssetPackaging.FILE,
      sourceHash: randomBytes(18).toString('base64'),
      fileName: virtualComplianceStack.templateFile,
    });

    const virtualStackArtifact = app
      .synth()
      .getStackArtifact(virtualComplianceStack.artifactId);

    for (const region of GOVERNED_REGIONS) {
      const assetStack = new ComplianceAssetStack(
        scope,
        env,
        region,
        virtualStackArtifact,
      );
      this.addDependency(assetStack);
      new CfnStackSet(this, `StackSet-${region}`, {
        autoDeployment: {
          enabled: true,
          retainStacksOnAccountRemoval: false,
        },
        stackInstancesGroup: [
          {
            regions: [region],
            deploymentTargets: { organizationalUnitIds: DEPLOYMENT_ORG_UNITS },
          },
        ],
        capabilities: ['CAPABILITY_IAM'],
        operationPreferences: {
          maxConcurrentPercentage: 50,
          failureTolerancePercentage: 50,
        },
        parameters: virtualStackArtifact.assets.flatMap((asset) => {
          return 's3BucketParameter' in asset
            ? [
                {
                  parameterKey: asset.s3BucketParameter,
                  parameterValue: assetStack.toolkitStagingBucket.bucketName,
                },
                {
                  parameterKey: asset.s3KeyParameter,
                  parameterValue: `assets/||${asset.sourceHash}.zip`,
                },
                {
                  parameterKey: asset.artifactHashParameter,
                  parameterValue: asset.sourceHash,
                },
              ]
            : [];
        }),
        stackSetName: `Custom-Compliance-${region}`,
        permissionModel: 'SERVICE_MANAGED',
        templateUrl: templateAsset.httpUrl,
      });
    }
  }
}

@pgarbe
Copy link
Contributor Author

pgarbe commented Dec 22, 2022

Started to work on a StackSet L2 construct which supports StackSetStacks with file-based assets: https://github.com/pgarbe/cdk-stackset

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/core Related to core CDK functionality effort/medium Medium work item – several days of effort feature-request A feature should be added or improved. p2
Projects
None yet
Development

No branches or pull requests