-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Add local module support to the cloudformation package command #9124
base: develop
Are you sure you want to change the base?
Conversation
Includes a basic test case for unit testing.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #9124 +/- ##
==========================================
- Coverage 0.08% 0.07% -0.01%
==========================================
Files 210 215 +5
Lines 16955 17662 +707
==========================================
Hits 14 14
- Misses 16941 17648 +707 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solid start. I have some corrections and some questions.
overrides = resource_overrides[attr_name] | ||
resource[attr_name] = merge_props(original, overrides) | ||
|
||
def resolve(self, k, v, d, n): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like there are better flows here. You pass in k,v but also the dict and parent k that holds k, v? From what I can tell the big reason for that is to switch an object using an intrinsic function back into a value. You are taking advantage of the fact that dict/list are sent by reference. It feels like an approach that does a return instead of relying on a reference would read better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean something like d[n] = self.resolve(k, v)
? Maybe that is better. But I am hesitant to change it, this was the trickiest piece of code to get right.
I was thinking about how to refer to values from a template and noticed that that is a place where the leaky abstraction might be a downside (most of it is true for the overrides too, but it feels more expected there): If you want to reference a resource in a module you would use e.g.
Having an "outputs" or "outputs" equivalent could solve this. e.g Parent template Modules:
Content:
Source: ./basic-module.yaml
Properties:
Name: foo
Outputs:
ExampleOutput:
Value: !GetAtt Content.BucketArn basic-module.yaml Parameters:
Name:
Type: String
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref Name
Outputs:
BucketArn: !GetAtt Bucket.Arn Output from the package command Resources:
ContentBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: foo
Outputs:
ExampleOutput:
Value: !GetAtt ContentBucket.Arn |
Agreed with Ben on this one. You could still allow for an override capability that allows for a missing output that may be needed but having to understand the module resource names can get frustrating and prevents flexible in changing them. There are going to be limitations in this approach that are similar to modules. Adding mappings/conditions into a module will limit its usage because it cannot rely on another resource from the parent template or other module. |
I don't see any reason not to add Outputs. It's definitely a more predictable way to reference resources in the module. I still want the Overrides section and the ability to reference things in the module directly if you know the name, but that can be considered a more advanced use case. If you're consuming a 3rd party module, the risk is higher than if you're referencing your own local modules. |
Implemented here: 160be7c |
if len(eq) == 2: | ||
val0 = eq[0] | ||
val1 = eq[1] | ||
if IF in val0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Today CloudFormation doesn't support this. The only concern I have with this is it could cause infinite loops.
Conditions:
One:
Fn::Equals:
- !If [Two, "A", "B"]
- "A"
Two:
Fn::Equals:
- !If [One, "A", "B"]
- "B"
Later on we call out that Fn::If
isn't supported so not sure why it is here.
val0 = find_ref(val0[REF]) | ||
if REF in val1: | ||
val1 = find_ref(val1[REF]) | ||
retval = val0 == val1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading this correct its possible this becomes val0 = "foo", val1 = {"Ref": "SomeResource"}. This would evaluate to false but could be true if this resource returns the value foo
I'm for this kind of functionality, but why is the module interface template-like (parameters and output, CloudFormation typing), and not resource-like? To my mind, the parameters-and-outputs part should look like a resource definition schema, and the use of a module should look like a resource. The other thing I'd like to see is something like, the CLI can accept as input not (just) the template file with implicit references to other files, but a zip file containing a template (with a well-known name) along with the associated modules as a single input. Maybe aligning with CDK's cloud assembly format. Because the direction should be, that zip file can be passed directly into CreateStack. |
Thanks for the feedback!
I worry that the added complexity might affect adoption. Can you give an example of why defining the module with a schema would be beneficial? Much of the schema can be implied from the current format, can't it?
Yes, we want to support zip files as input to |
My personal preference is |
This PR adds the ability to write local CloudFormation modules, which are YAML files that contain Parameters and Resources. These modules can be included in a parent template in a new
Modules
section. Modules are configured by a combination of formalParameters
(which coincide with theProperties
defined in the parent) and anOverrides
attribute, which allows users full control over the rendered output. This is a port from the functionality that exists in the Rain CLI. See the snippets below for a basic example.Parent template
basic-module.yaml
Output from the package command
Design Decisions
Modules can be imported into a template (or into a module itself) from a new
Modules
section. In Rain and in registry modules, this section does not exist. Instead, the imported content is configured in a resource and the source of the module is embedded in theType
attribute. This PR implements both ways of configuring an imported module, giving customers the option of whichever feels more intuitive. The benefit of using aModules
section is that it will be more intuitive if we decide to allow injecting other sections other thanResources
, likeOutputs
.Logical IDs are a concatenation of the module name and the logical id defined in the module. Unlike registry modules, users do not have the option of referring to the resources with a dot between the names.
The
Overrides
section is not considered an escape hatch of last resort. It is an encouraged way to configure modules without requiring the module author to anticipate every possible need. IaC abstractions are leaky by nature and we expect users to understand the internals of any module they import. Deciding how many actualParameters
to include in a module is highly opinionated and left up to authors. One caveat here is thatOverrides
are fragile, since they will break if the module author changes any of the logical IDs in the module. See item 5,Outputs
, as a way to mitigate this.This functionality has been added to the
aws cloudformation package
command. We could have created a new dedicated command for module packaging, likesynth
. This is an easily reversible decision. The code is ported from the Rainpkg
command, which was originally written to mimic the behavior ofaws cloudformation package
, so it seems like the right place to put it. We removed the--s3-bucket
requirement from the command, waiting to check for that parameter when we see that you need to upload something to S3.Modules can define
Outputs
, which are key-value pairs that can be referenced by the parent. This gives module authors a formal way to pass values to the consumer without them needing to rely on knowing the internal structure of the module, which will be fragile if the module author changes any of the logical IDs.Instead of using
Fn::ForEach
as a way of implementing loops, we added aMap
attribute to a local module resource. The module output is repeated for each element in the map, substituting$MapValue
and$MapIndex
in module Properties. This can only be used with lists that can be resolved at build time.The PR also adds a new
Constants
section to templates and modules. This feature is not absolutely necessary for an implementation of modules, but it serves a similar purpose of significantly reducing copy-paste when authoring CloudFormation.Additional features to add on this PR or in the future
[ ] - Download modules from S3
[ ] - ForEach support within a module and in the parent template
[ ] - Module package support like in Rain, with an alias to a zip file
How to test this PR
Check out my branch and install the aws cli into a local Python environment.
Examples
See the test templates in this PR at
tests/unit/customizations/cloudformation/modules/
and also a draft PR with a full serverless webapp here: aws-cloudformation/aws-cloudformation-templates#457