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

RFC 0009: Add looping functionality in CFN Templates #75

Merged
merged 9 commits into from
Jun 19, 2023

Conversation

MalikAtalla-AWS
Copy link
Contributor

@MalikAtalla-AWS MalikAtalla-AWS commented May 13, 2022

Language Enhancement Request For Comment

This is a request for comments about a new intrinsic function called Fn::ForEach to support a looping functionality in CloudFormation templates. See the tracking issue (#9) for
additional details.


Licensing

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@benkehoe
Copy link

benkehoe commented May 13, 2022

I have a number of different pieces of feedback.

Naming

In the issue I originally opened (#41), I proposed Fn::Map; I believe it's important for users to understand this as a functional concept, rather than an imperative one. They aren't writing a for loop, they are mapping a JSON template over a set of values. I'd settle for Fn::Foreach, though. I believe there's also value in a Fn::Reduce (for example, to create a list of ARNs from a collection of resources created with this function), which supports Fn::Map as a name.

Syntax

Index/value parameter names

I don't like the need to define the index and value parameter names. They're probably always going to be the same, it's just one more thing to type out and one more thing to go wrong. Could they be made optional, defaulting to some useful names? This brings me to my next point...

Function parameter ordering

There are so many parameters to this function. I understand that all other intrinsic functions use positional parameters, but maybe this is a place to use named parameters. For example:

!Foreach
  Input: !Ref AmiIds
  Value:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: "m1.small"
      ImageId: !Ref x
  LogicalId: !Sub Instance${i}
  ParameterNames: ["i", "x"]

This would allow for multiple independently-optional parameters, like LogicalId and ParameterNames. Speaking of LogicalId...

Use cases

I think there's actually four use cases here.

  1. Inserting a list as a value
  2. Inserting an object as a value
  3. Merging an object into a parent
  4. Creating a set of CloudFormation resources

Use cases 3 and 4 are both accomplished by the LogicalId parameter. Use case 2 is not supported.

Here's I'd propose:

  • Instead of LogicalId and its current semantics, allow a Key parameter that, if set, means the function produces an object rather than a list. By default, this object is the value where the function is defined.
  • Allow a boolean Merge parameter that requires the Key parameter also be set, that when true, merges the object defined by the function with its parent.
  • Define a separate syntax for resources. Here's why...

Reifying resource mapping

If this intrinsic function simply allows users to create multiple resources with an intrinsic function that shuffles YAML around, we're missing a giant opportunity.
We should be enabling CloudFormation to represent a collection of resources defined by mapping over some values as a first-class concept.
Say that I want subnets in every AZ. That's an architectural intention that should be fully represented in the cloud. It should get a name, it should be able to inspected, etc.

This proposal doesn't serve that purpose. I think it can be a separate RFC, but imagine something like

Resources:
  Subnets:
    Type: CloudFormation::Map
    Input: [...]
    LogicalId: ... # actual logical id for the resource
    ResourceValue: ...
    # etc, similar syntax to the function

If we agree that such a thing should exist, it will help focus the syntax of this intrinsic function on use cases 1-3 (where use case 3 serves to allow users to create a non-reified set of resources).

@michaelbrewer
Copy link

Fn::Map or at worst Fn::Foreach make a-lot more sense then just Fn::For.

@cloudyparts
Copy link

cloudyparts commented May 14, 2022

Naming


Issue #9 describes Fn::For as;

Fn::For traverses a single given List of Strings from first to last, and over each iteration, it references each item under a given template snippet to be replicated.

This definition describes an iterator which implies functions like Map, Reduce, Filter, and Zip.

The term For may confuse developers since this term has a long history as A control-flow-statement.

I agree with @benkehow Fn::Map communicates the intent better than Fn::For.

@jlhood
Copy link
Contributor

jlhood commented May 17, 2022

Really appreciate all the feedback and engagement on this RFC! After some internal discussions, we've determined it needs significant rework. We're going to iterate a bit internally and then post a new revision for more feedback. Stay tuned!

@MalikAtalla-AWS
Copy link
Contributor Author

Hey all, I have posted a new revision addressing some of the feedback. This is a summary of the changes:

  • We are changing the name of the function to Fn::Map
  • We are making the parameters named, instead of positional. We figured that is warranted given the number of parameters, some of them being optional and how that impacts readability.
  • The parameters for the index and value variables are now optional. We think customizing them is an important use case, but in most cases, users will be fine with the defaults.
  • The LogicalId parameter is now simply called "key"
  • We're introducing a new intrinsic function called Fn::Merge, which can be used to merge the result of Fn::Map into its parent object.

@MalikAtalla-AWS MalikAtalla-AWS changed the title RFC 0009: Add looping functionality in CFN Templates (Fn::For) RFC 0009: Add looping functionality in CFN Templates Jun 21, 2022
RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
{
"Fn::Map": {
"index": "Index",
"value": "Value",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Things I'm wondering:

  • Would it be clearer to use IndexParameter and ValueParameter for the keys (to make it clear that you're configuring something you will !Ref later)
  • Would it be clearer to use Item instead of Value? This might be related to the languages I've been exposed to, but I'd use Key-Value or Index-Item, not using Value prevents confusion with the Key parameter

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "key" and "value" if they are both used should be outputs, currently "fragment" is used for the output value. We've got four parameters here:

  • input value from the input collection (currently "value")
  • index of the input value (currently "index")
  • output value (currently "fragment")
  • optional output key (currently "key")
    Maybe making them more explicit makes sense? InputIndex, InputValue, OutputValue, OutputKey? It would leave open the possibility that in the future the collection could be an object, which would would then involve an InputKey parameter as well.

RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
* Caveat: Make sure Logical Ids are alphanumeric characters

#### SSM/AWS List Type Parameters
* SSM/AWS List type parameters can be referenced in `Fn::Map`. Only List type parameter is allowed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an example of using !Split to convert a string to a list?

RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
Properties:
SecurityGroups:
- Fn::Map:
collection: [!Ref SecurityGroup]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!GetAtt x.DnsEntries of a AWS::EC2::VPCEndpoint is also an interesting example

RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::For.md Outdated Show resolved Hide resolved
@MalikAtalla-AWS
Copy link
Contributor Author

Really appreciate your feedback @benbridts and @benkehoe

@benkehoe
Copy link

re-posting this as a general comment so it doesn't get lost on an old revision:

from @benbridts

Things I'm wondering:

  • Would it be clearer to use IndexParameter and ValueParameter for the keys (to make it clear that you're configuring something you will !Ref later)
  • Would it be clearer to use Item instead of Value? This might be related to the languages I've been exposed to, but I'd use Key-Value or Index-Item, not using Value prevents confusion with the Key parameter

I think "key" and "value" if they are both used should be outputs, currently "fragment" is used for the output value. We've got four parameters here:

  • input value from the input collection (currently "value")
  • index of the input value (currently "index")
  • output value (currently "fragment")
  • optional output key (currently "key")

Maybe making them more explicit makes sense? InputIndex, InputValue, OutputValue, OutputKey? It would leave open the possibility that in the future the collection could be an object, which would would then involve an InputKey parameter as well.

@benkehoe
Copy link

Re-posting this as a general comment so it doesn't get lost on an old revision:

Both @benbridts and I commented that we think the parameters should be CamelCase to be consistent with CloudFormation conventions

@benkehoe
Copy link

@MalikAtalla-AWS FYI amending the commit and pushing means comments on previous commits are no longer visible in the diff, I'd recommend creating new commits and only squashing when it's ready for merging

RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
RFCs/0009-Fn::Map.md Outdated Show resolved Hide resolved
@benbridts
Copy link

I think "key" and "value" if they are both used should be outputs, currently "fragment" is used for the output value. We've got four parameters here:

  • input value from the input collection (currently "value")
  • index of the input value (currently "index")
  • output value (currently "fragment")
  • optional output key (currently "key")

(and collection, but that one seems to be clear)

Maybe making them more explicit makes sense? InputIndex, InputValue, OutputValue, OutputKey? It would leave open the possibility that in the future the collection could be an object, which would would then involve an InputKey parameter as well.

the current value and index do not feel like inputs to me. They are more meta-inputs (used within the fragment) than inputs (used by the map function).

Since the current default for Value is X, I would propose something like element, item or variable.

I'm not the biggest fan of using "Output" in the names for fragment and key, but if you translates what happens to python, it makes sense to use Key and Value in the names. And even though output is a fitting name, they are also templates. (template already has a specific meaning in CloudFormation, but I don't think this has to be a problem)

def cfn_map(collection: List, key: str, fragment: str):
    return {key.format(i=i, x=x): fragment.format(i=i, x=x) for i, x in enumerate(collection)}
    
 # or with different names
 
 def cfn_map(collection: List, key: str, value: str):
    return {key.format(i=i, x=x): value.format(i=i, x=x) for i, x in enumerate(collection)}

This leads me to thinking something like

  • IndexName (how you will refer to the index)
  • VariableName, ItemName or ElementName (how you will refer to the variable)
  • Collection (the actual collection you are mapping over)
  • KeyTemplate or OutputKey (the key you will output, that can also reference variables)
  • ValueTemplate or OutputValue (the value/fragment you will output, that can also reference variables)

@arthurboghossian
Copy link
Contributor

Update: The AWS CloudFormation team really appreciates the interest and feedback we've received on this RFC! In the past months, our team did a deep dive to better understand specific customer pain points and brainstormed ideas with customers and community representatives. Based on this deep dive, we are updating this RFC with a proposal to add a single Fn::ForEach intrinsic function. Let us know your feedback and thoughts!

RFCs/0009-Fn::ForEach.md Show resolved Hide resolved
RFCs/0009-Fn::ForEach.md Show resolved Hide resolved
Co-authored-by: Malik <60721392+MalikAtalla-AWS@users.noreply.github.com>
"Identifier",
["Value1", "Value2"], ## Collection
{
"OutputKey": "OutputValue" ## Ex: {"OutputKeyName${Identifier}": "OutputValue"}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using only a single item in the object could be read as only permitting one item; it might be useful to include two items, or a comment indicating more items are possible

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! I’ve updated the syntax to include a comment to indicate there can be multiple key-value pairs within the 3rd positional parameter

"Resources": {
"Fn::ForEach::Buckets": [
"Identifier",
["A", "B", "C"],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this example might be better with the collection from a Mapping, so that it's not duplicated between the two?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that would be better, so I’ve updated the example accordingly

"Outputs": {
"SecondInstanceId": {
"Description": "Instance Id for InstanceB",
"Value": {"Ref": "InstanceB"}
Copy link

@benkehoe benkehoe Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels gross, having to reference a generated identifier, relative to something that is more like !Select [1, !Ref Instances] (the ForEach name has to be a unique identifier in the template namespace, right?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the ForEach name has to be a unique identifier in the template namespace (see Constraint 3 in RFC), where based on the number of asks for the feature, !Ref on the ForEach name, which would return a list of logicalId’s generated by ForEach, can be implemented in the future (see GitHub issue here with details on Additional Enhancements to ForEach)


## Potential Follow-up Features

Features like supporting iterating over a key-valur pair or allowing Ref/Fn::GetAtt on the UniqueLoopName are out of scope for this RFC.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo on "key-valur pair"

@benkehoe
Copy link

Needed in the FAQ, because people will try this immediately:

  • Can I use an object for the collection instead of a list?
  • Can I output a list instead of merge into an object?


|FunctionName |Pros |Cons |
|--- |--- |--- |
|Fn::ForEach |Addresses valid feedback in GitHub pull requestIntrinsic function name indicates it’s a functional concept and not an imperative oneForEach Illustrates that we’re operating on a template fragment over a set of valuesGives customers familiar with coding concepts the idea that some form of “looping” (or replication) is done |Intrinsic function name is potentially not unclear to customers who aren’t familiar with coding concepts; however, the syntax of Fn::ForEach::<ResourceLogicalId> would give users an idea that within the function, we define the template fragment "for each" resource |

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting on these table entries is a mess

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! Looks like the resolved markdown looks ok, but I've updated the raw markdown to be clean as well.

@@ -0,0 +1,1461 @@
# Looping functionality for CFN Templates

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with Fn::ForEach as a name, but I'd prefer to stay away from "loop" as much as possible, if there's a way to do that. At least use it as an opportunity to help users understand that they're using it in a functional programming way, rather than an imperative one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense! I've updated the title to not mention "looping", to ensure it isn’t misunderstood as being an imperative solution.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably impossible to avoid completely, but as a general direction I think it's wise

@rhbecker
Copy link

Are there outstanding concerns preventing the assigned reviewers from approving this?

I'm looking forward to taking advantage of this capability, and am hoping it can soon move towards implementation.

@arthurboghossian
Copy link
Contributor

Thanks for the callout @rhbecker!

Believe there aren't any outstanding concerns, so I'll re-request reviews and will merge the pull request if I receive the necessary approvals.

@arthurboghossian arthurboghossian self-assigned this Jun 19, 2023
@arthurboghossian arthurboghossian merged commit 3e50091 into aws-cloudformation:main Jun 19, 2023
@rhbecker
Copy link

Sorry to be a pest, but if I'm reading the tracking issue #9 correctly, in order to keep things moving, I think someone needs to add an approved label to this PR, and a status/final-comments-period label to the tracking issue?

@MalikAtalla-AWS
Copy link
Contributor Author

You're right @rhbecker. Appreciate the reminder. We'll take care of that.

@rhbecker
Copy link

rhbecker commented Jul 7, 2023

🤔 Maybe instead of pestering you about labels (which still don't seem quite right on this or the tracking issue), I should instead ask whether implementation is fully unblocked.

My fear is that this issue is stuck in some queue, due to a "paperwork" blockage, when it could otherwise be ready for some hungry CFN engineer to start working on.

If it's already unblocked, or perhaps even already in progress, I can sit back, relax, and patiently (🤞) await completion.

@MalikAtalla-AWS MalikAtalla-AWS added the approved RFC PR approved by CFN engineers label Jul 10, 2023
@MalikAtalla-AWS
Copy link
Contributor Author

Hey @rhbecker, thanks for following up. This issue is and has been unblocked. We have set the status of the RFC to "implementing" in the main readme of this repo and I (just now) added the approved-label to this PR. (Maybe we should consider removing that line about the approved label from our readme. The info in the main readme and the fact that the PR is merged is probably indication enough about the status.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved RFC PR approved by CFN engineers
Projects
None yet
Development

Successfully merging this pull request may close these issues.