Open pgarbe opened 3 years ago
@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.
Copy @rix0rrr
@eladb Thanks, that actually works :) I'm not just sure what could be a good source hash. Will look into that.
That's definitely the tricky part. You could use the current git commit in production but for dev iterations you'd need something else
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.
Only the synth part works. Within a pipeline the upload action is missing. How can I enforce that?
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?
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
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.
Multipel +1 on this.
I have quite a few stacks i want to deploy to every account in our org.
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.
@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 )...
Making the stack available as an asset use cases is also needed for AWS Config conformance packs.
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,
...
});
}
}
@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
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.
@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
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
?
@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.
Ok. I've edited the title of the issue, let's re-open this one.
BTW, contributions are always welcome 😉.
Same topic.... ( request for conformance packs.. ). https://github.com/aws/aws-cdk/issues/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()
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?
@peterb154 when this code is executed there isn't a synthesized template yet :-)
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,
});
}
}
}
Started to work on a StackSet L2 construct which supports StackSetStack
s with file-based assets: https://github.com/pgarbe/cdk-stackset
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:
Not sure how the implementation could look like.
Other
This is a :rocket: Feature Request