Skip to content
GitHub LinkedIn

Creating resources using Fn::ForEach with CloudFormation

Introduction

It’s been a while since I’ve published a blog post as I’ve been busy experimenting with some new forms of content creation (i.e. Twitch) and settling into my new job. I’m back now with a look at an exciting new feature to CloudFormation, the Fn::ForEach intrinsic function.

CloudFormation

For a long time, if you wanted to create multiple resources in CloudFormation you’d need to define them all individually. For example, take a look at the snippet below.

Resources:
  BucketOne:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-bucket-one
  BucketTwo:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-bucket-two

Whilst there are of course advantages to this, the main one being that it’s incredibly explicit what resources are being created, it does introduce repetitiveness and create inertia in the developer experience when lots of similar resources need to be created.

Back in October 2021, the idea of an Fn::Map intrinsic function was proposed on GitHub here. In May 2022, an official request for comment (RFC) was published by AWS attracting lots of thoughts and opinions from various members of the community. It’s great to see AWS taking input from end users into account when developing new features. The time taken from the feature being marked as approved and it being released for general availability was around a month. It’s always interesting to get a little insight to how AWS propose, plan and deliver features.

The new Fn::ForEach intrinsic function was released on 26 July 2023 here. The way that it’s described is “With Fn::ForEach, you can replicate parts of your templates with minimal lines of code”. Let’s dive in and take a look at how easy (or not) this really is.

First of all, it’s important to note that this new function requires the AWS::LanguageExtensions transform to be specified in the CloudFormation template.

In it’s simplest form, defining a mappable resource follows the pattern in the snippet below. Each loop needs to have a unique name. This is represented by the {UniqueLoopName} part of the example. Within the loop (represented as a list) there are three items.

AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::LanguageExtensions'
Resources:
  Fn::ForEach::{UniqueLoopName}:
    - { ValueIdentifier }
    - [...]
    - Resource${ValueIdentifier}:
        Type: AWS::Service::Resource
        Properties:
          Property: !Ref { ValueIdentifier }

Let’s move on to building a couple of examples in CloudFormation, followed by a look at Terraform as an alternative.

Example one

In this example, we’re creating a couple of S3 buckets. This will create buckets named bucketone and buckettwo, both at the logical level (within CloudFormation) and the physical level (the bucket name).

This is the most simplistic example I could think of, but to me it presents a problem. Typically CloudFormation logical names are CamelCase and S3 buckets can only have lowercase characters in their physical names. As a result, we’ve had to compromise on the logical name to meet the hard requirement imposed by the physical name. Annoying, huh? Keep reading to see an alternative way.

AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::LanguageExtensions'
Resources:
  Fn::ForEach::Buckets:
    - BucketName
    - - bucketone
      - buckettwo
    - ${BucketName}:
        Type: AWS::S3::Bucket
        Properties:
          BucketName: !Ref BucketName

Example two

In this example we’re still building two S3 buckets, however this time we’re making use of CloudFormation’s ‘Mappings’ functionality in order to retrieve different values. Using the pattern below we can define values like One and Two to identify our buckets, then link them to physical resource names of my-bucket-one and my-bucket-two using the Fn::FindInMap intrinsic function. Pretty nifty.

AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::LanguageExtensions'
Mappings:
  BucketMap:
    One:
      BucketName: my-bucket-one
	Two:
      BucketName: my-bucket-two
Resources:
  Fn::ForEach::Buckets:
    - BucketNumber
    - - One
      - Two
    - Bucket${BucketNumber}:
        Type: AWS::S3::Bucket
        Properties:
          BucketName:
			Fn::FindInMap: ["BucketMap", !Ref BucketNumber, "BucketName"]

Terraform

I’m not going to spend too much time on Terraform mainly because the focus of this article is the new functionality in CloudFormation, we’ll take a look at how it compares however.

At a high level, Terraform offers two features that are contenders to our example usage Fn::ForEach intrinsic function. These are the for_each and count meta arguments.

for_each

The for_each meta argument is the closest match to CloudFormation’s Fn::ForEach. It takes a map or strings as input and then creates a resource for each item.

In the example below, we define two bucket names as a set of strings and two buckets as a map. From looking at the two resources, aws_s3_bucket.buckets_set and aws_s3_bucket.buckets_map we can see that all that’s required is specifying for_each and each.value to access the iterated over values. In the case of the map, it’s possible to then access nested properties such as each.value.name.

locals {
  bucket_names = set(["my-bucket-one", "my-bucket-two"])
  buckets = {
    three = { name = "my-bucket-three" },
    four = { name = "my-bucket-four" }
  }
}

resource "aws_s3_bucket" "buckets_set" {
  for_each = local.bucket_names

  bucket_name = each.value
}

resource "aws_s3_bucket" "buckets_map" {
  for_each = local.buckets

  bucket_name = each.value.name
}

When you define resources in this way, you can access properties via syntax like aws_s3_bucket.buckets_map["three"].

count

If requirements are as simple as just creating x number of resources, then Terraform’s count meta argument is the one for you. This allows you to say “I want to create 10 S3 buckets”, and make use of the count index (zero-indexed) to build up unique names. For an example, see the snippet below which’ll create S3 buckets names my-bucket-1 through to my-bucket-10.

locals {
  number_of_buckets = 10
}

resource "aws_s3_bucket" "buckets" {
  count = local.number_of_buckets

  bucket_name = "my-bucket-${count.index + 1}"
}

When you define resources in this way, you can access properties via the syntax aws_s3_bucket.buckets_map[0].

Summary

So, what’s my overall view on this?

It’s great that CloudFormation now supports the dynamic creation of resources from a list of items. I’m sure that I’m not the only person who’s been waiting for this for a long time. But unfortunately I think that’s about as far as my excitement goes. Whilst functionally it does solve the problem, it just feels a bit messy. I don’t think that’s down to any fault of the AWS service team, nor suggestions made by the community, but rather a limitation of having to implement it into something YAML and JSON compatible.

To me the Terraform implementation is cleaner and easier to read. For this reason, I’d still recommend that over CloudFormation if you’re going to be defining lots of resources dynamically.

That’s all for now. I promise not to take so long until I next publish something, I’ve got a few ideas for services I’d like to explore!

You can find some useful related links below: