All posts by Dan Blanco

Import entire applications into AWS CloudFormation

Post Syndicated from Dan Blanco original https://aws.amazon.com/blogs/devops/import-entire-applications-into-aws-cloudformation/

AWS Infrastructure as Code (IaC) enables customers to manage, model, and provision infrastructure at scale. You can declare your infrastructure as code in YAML or JSON by using AWS CloudFormation, in a general purpose programming language using the AWS Cloud Development Kit (CDK), or visually using Application Composer. IaC configurations can then be audited and version controlled in a version control system of your choice. Finally, deploying AWS IaC enables deployment previews using change sets, automated rollbacks, proactive enforcement of resource compliance using hooks, and more. Millions of customers enjoy the safety and reliability of AWS IaC products.

Not every resource starts in IaC, however. Customers create non-IaC resources for various reasons: they didn’t know about IaC, or they prefer to work in the CLI or management console. In 2019, we introduced the ability to import existing resources into CloudFormation. While this feature proved integral for bringing resources into IaC on an individual basis, the process of manually creating templates to match those resources wasn’t ideal. Customers were required to look up documentation on resources and painstakingly copy values manually. Customers also told us they traditionally engaged with applications (that is, groupings of related resources), so dealing with individual resources didn’t match that experience. We set out to create a more holistic flow for managing resources and their relations.

Recently, we announced the IaC generator and CDK Migrate, an end-to-end experience that enables customers to create an IaC configuration based off a resource as well as its relationships. This works by scanning an AWS account and using the CloudFormation resource type schema to find relationships between resources. Once this configuration is created, you can use it to either import those resources into an existing stack, or create a brand new stack from scratch. It’s now possible to bring entire applications into a managed CloudFormation stack without having to recreate any resources!

In this post, I’ll explore a common use case we’ve seen and expect the IaC generator to solve: an existing network architecture, created outside of any IaC tool, needs to be managed by CloudFormation.

IaC generator in Action

Consider the following scenario:

As a new hire to an organization that’s just starting its cloud adoption journey, you’ve been tasked with continuing the development of the team’s shared Amazon Virtual Private Cloud (VPC) resources. These are actively in use by the development teams. As you dig around, you find out that these resources were created without any form of IaC. There’s no documentation, and the person who set it up is no longer with the team. Confounding the problem, you have multiple VPCs and their related resources, such subnets, route tables, and internet gateways.

You understand the benefits of IaC – repeatability, reliability, auditability, and safety. Bringing these resources under CloudFormation management will extend these benefits to your existing resources. You’ve imported resources into CloudFormation before, so you set about the task of finding all related resources manually to create a template. You quickly discover, however, that this won’t be a simple task. VPCs don’t store relations to items; instead, relations are reversed – items know which VPC they belong to, but VPCs don’t know which items belong to them. In order to find all the resources that are related to a VPC, you’ll have to manually go through all the VPC-related resources and scan to see which vpc-id they belong to. You’ll have to be diligent, as it’s very easy to miss a resource because you weren’t aware that it existed or it may even be different class of resource altogether! For example, some resources may use an elastic network interface (ENI) to attach to the VPC, like an Amazon Relational Database Service instance.

You, however, recently learned about the IaC generator. The generator works by running a scan of your account and creating an up-to-date inventory of resources. CloudFormation will then leverage the resource type schema to find relationships between resources. For example, it can determine that a subnet has a relationship to a VPC via a vpc-id property. Once these relationships have been determined, you can then select the top-level resources you want to generate a template for. Finally, you’ll be able to leverage the wizard to create a stack from this existing template.

You can navigate to the IaC generator page in the Amazon Management Console and start a scan on your account. Scans last for 30 days, and you can run three scans per day in an account.

Scan account button and status

Once the scan completes, you create a template by selecting the Create Template button. After selecting Start from a new template, you fill out the relevant details about the stack, including the Template name and any stack policies. In this case, you leave it as Retain.

Create template section with "Start from a new template" selected

On the next page, you’ll see all the scanned resources. You can add filters to the resource such as tags to view a subset of scanned resources. This example will only use a Resource type prefix filter. More information on filters can be found here. Once you find the VPC, you can select it from the list.

A VPC selected in the scanned resources list]

On the next page, you’ll see the list of resources that CloudFormation has determined to have a link to this VPC. You see this includes a myriad of networking related resource. You keep these all selected to create a template from them.

A list of related resources, all selected

At this point, you select Create template and CloudFormation will generate a template from the existing resources. Since you don’t have an existing stack to import these resource into, you must create a new stack. You now select this template and then select the Import to stack button.

The template detail page with an import to stack button

After entering the Stack name, you can then enter any Parameters your template needs.

The specify stack details page, with a stack name of "networking" entered

CloudFormation will create a change set for your new stack. Change sets allow you to see the changes CloudFormation will apply to a stack. In this example, all of the resources will have the Import status. You see the resources CloudFormation found, and once you’re satisfied, you create the stack.

A change set indicating the previously found resources will be created

At this point, the create stack operation will proceed as normal, going through each resource and importing it into the stack. You can report back to your team that you have successfully imported your entire networking stack! As next steps, you should source this template in a version control system. We recently announced a new feature to keep CloudFormation templates synced with popular version control systems. Finally, make sure to make any changes through CloudFormation to avoid a configuration drift between the stated configuration and the existing configuration.

This example was primarily CloudFormation-based, but CDK customers can use CDK Migrate to import this configuration into a CDK application.

Available Now

The IaC generator is now available in all regions where CloudFormation is supported. You can access the IaC generator using the console, CLI, and SDK.

Conclusion

In this post, we explored the new IaC generator feature of CloudFormation. We walked through a scenario of needing to manage previously existing resources and using the IaC generator’s provided wizard flow to generate a CloudFormation template. We then used that template and created a stack to manage these resources. These resources will now enjoy the safety and repeatability that IaC provides. Though this is just one example, we foresee other use cases for this feature, such as enabling a console-first development experience. We’re really excited to hear your thoughts about the feature. Please let us know how you feel!

About the author

Dan Blanco

Dan is a senior AWS Developer Advocate based in Atlanta for the AWS IaC team. When he’s not advocating for IaC tools, you can either find him in the kitchen whipping up something delicious or flying in the Georgia sky. Find him on twitter (@TheDanBlanco) or in the AWS CloudFormation Discord.

Automate safe AWS CloudFormation deployments from GitHub

Post Syndicated from Dan Blanco original https://aws.amazon.com/blogs/devops/automate-safe-aws-cloudformation-deployments-from-github/

AWS CloudFormation, an Infrastructure as Code (IaC) service that lets you model, provision, and manage AWS and third-party resources, now supports using Git sync to automatically trigger a deployment whenever a tracked Git repository is updated. This enables developers to significantly speed up the development cycle for CloudFormation by integrating into their Git workflow and reducing time lost to context switching. The new integration works directly with GitHub, GitHub Enterprise, GitLab, and Bitbucket.

In this post, you’ll explore what a modern development experience looks like using both GitHub’s native tooling as well as the native CloudFormation Git sync integration. You’ll be creating a cloud development environment using GitHub CodeSpaces, integrating direct feedback into Pull Requests using GitHub Actions and the CloudFormation Linter, and automating safe deployments.

Requirements

Creating an empty repository

For this, you’ll start with a new GitHub repository. GitHub allows you to create new Git repositories for free. For this example, you’ll create a repository called git-sync.

Creating a repository called Git sync

Setting up a Codespace

Once you create the repository, you’ll have the option to create a Codespace. Codespaces are remote development environments managed by GitHub that allows you to develop from anywhere on standardized virtual machines.

Creating a new code space on the Git sync repository

Codespaces uses Visual Studio Code as its code editor of choice. Visual Studio Code is an open-source code editor that has excellent flexibility and extensibility due to the ability to install extensions.

Codespace using Visual Studio Code as code editor

Once it finishes creating, you can set up the environment much like you would your local development environment. You’re going to be adding the CloudFormation Linter extension to provide fast in-editor feedback when developing your template. This lets you avoid having to send CloudFormation your templates for validation and instead have good confidence that your templates are valid before you submit them for provisioning. You’ll install it both using the command line and an extension to Visual Studio Code itself. In the terminal, run:

pip3 install cfn-lint

Once that installs, you can install the linter in the extensions panel:

Installing the CloudFormation Linter Visual Studio Code Extension

Next, you’ll create your template in the base directory called vpc.yaml. As you start typing, the linter extension will offer recommendations and auto-complete for you.

Linter recommending autocompletion for AWS::EC2::VPC

Copy the following template into our newly created vpc.yaml file:

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

This template creates a VPC with a CIDR block of 10.0.0.0/16.

You can verify the template gives no errors by running cfn-lint in the terminal and verifying it returns no errors.

cfn-lint -t vpc.yaml

Adding the deployment file

In order to support many different types of deployments, Git sync supports a deployment file to provide flexibility for managing CloudFormation stacks from within a Git repository. This config file manages the location of the template file, and any parameters or tags you may be interested in using. I strongly encourage you to use a config file for managing your parameters and tags, as it enables easy auditability and deterministic deployments.

You’ll be creating a new file called deployment-file.yaml in your repository. Since this stack doesn’t have parameters or tags, it’ll be relatively simple:

template-file-path: ./vpc.yaml

You also have the ability to add this file in the console later.

Adding Pull Request actions

Now that you’ve configured your development environment just the way you want it, you want to ensure that anyone who submits a pull-request will receive the same high-quality feedback that you’re getting locally. You can do this using GitHub Actions. Actions are a customizable workflow tool that you can leverage to enable pull-request feedback and CI builds.

To do that, you’ll have to create the following folder structure: .github/workflows/pull-request.yaml. The contents of this file are as follows:

name: Pull Request workflow

on:
  - pull_request

jobs:
  cloudformation-linter:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@3
      - name: Linter install
        uses: scottbrenner/cfn-lint-action@v2
        with:
          command: cfn-lint -t ./vpc.yaml

With this configured, you’ll now get feedback on a pull request with the linter’s findings. Push your work to the remote branch.

git add -A
git commit -m "add pull request linting workflow, add base vpc template"
git push origin main

Now you’ll add a subnet to your VPC and intentionally make a mistake by adding an invalid property called VpcName, instead of VpcId.

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcName: !Ref VPC
      CidrBlock: 10.0.0.1/16

The linter will immediately inform you this is invalid:

CloudFormation Linter GitHub Action indicating errors and line numbers

You can ignore these for now. To create your pull request, you have to create a new branch and commit your local changes. You can do that using:

git switch -c add-subnet
git add -A
git commit -m "add subnet"
git push origin add-subnet

Once you push these commits, GitHub will allow you to create a pull request against your main branch. However, once you create it, you’ll notice that your checks fail when your GitHub Actions finish running.

Stack Deployments File section with "I am providing my own file in my repository" selected

You can see what went wrong by checking the “Files changed” tab. Your linter action will provide feedback directly on the pull request and block your merge action if you’ve set up your branch protection. This repository requires at least one reviewer and all checks to pass, so you’ll have to resolve both these failures.

CloudFormation Linter GitHub Action indicating errors and line numbers

Now that you have the high-quality feedback as well as the offending line numbers, you can go back to your template and make the necessary fix of changing VpcName to VpcId.

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.1/16

The local linter is happy, and after you commit again you’ll see that your remote linter is equally happy. After getting another approver, you can merge your commit into your main branch.

Approval from reviewer and passing checks enabling a merge

Enabling Git sync

You now have a high-quality cloud development environment and your pull request process ensures your templates are linted before merging. You can be sure that a CloudFormation template that makes it to the main branch is ready to be deployed. Next, you’ll be leveraging the newly released Git sync feature of CloudFormation to automatically sync your deployed stack with this new template.

First, create the role that will deploy our CloudFormation template. Be sure to note the name you select for this as you’ll be using it to manage your stack later. This example uses vpc-example-cloudformation-deployment-role.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateVpc",
        "ec2:CreateSubnet",
        "ec2:DescribeVpcs",
        "ec2:DeleteVpc",
        "ec2:DeleteSubnet",
        "ec2:ModifySubnetAttribute",
        "ec2:ModifyVpcAttribute"
      ],
      "Resource": "*",
      "Condition": {
        "ForAnyValue:StringEquals": {
          "aws:CalledVia": ["cloudformation.amazonaws.com"]
        }
      }
    }
  ]
}

Once the role has been created, you’ll have to create a new stack:

Template source section with sync from Git option selected

Here, you can see the new option to select Sync from Git template source, which you can configure on the next screen. Since you already created your stack deployment file, you can select I am providing my own file in my repository.

Stack Deployments File section with "I am providing my own file in my repository" selected

Next, you can configure your Git integration to choose your repository. Since it’s your first time, you’ll need to use the CodeStar Connection you created beforehand and select your repository.

Git sync configuration with CodeStar connection selected, repository set to "Git sync" and branch of "main" selected

Select GitHub, your connection, the repository, and branch, the deployment file location.

Finally, you will select New IAM Role to create a service managed role. This role will enable Git sync to connect to your repository. You’ll only need to do this once; in the future you’ll be able to use the existing role you’ll create here.

IAM Role selection

On the next page, you’ll select the IAM Role you created to manage this stack. This role controls the resources that CloudFormation will deploy. Stacks managed by Git sync must have a role already created.

Finally, you can see the status of your sync in the new “Git sync” tab, including the configuration you provided earlier as well as the status of your sync, your previous deployments, and the option to retry or disconnect the sync if needed.

Git sync configuration data indicting repository, provider, branch, deployment file path, and Git sync status

Conclusion

At this point, you’ve configured a remote development environment to get high-quality feedback when creating and updating your CloudFormation templates. You also have the same high-quality feedback when creating a pull request. Finally, when a template does get merged to the main branch, it will be automatically deployed to your stack. This represents a robust and extensible CI/CD system to manage your infrastructure as code. I’m excited to hear your feedback about this feature!

Dan Blanco

Dan is a senior AWS Developer Advocate based in Atlanta for the AWS IaC team. When he’s not advocating for IaC tools, you can either find him in the kitchen whipping up something delicious or flying in the Georgia sky. Find him on twitter (@TheDanBlanco) or in the AWS CloudFormation Discord.

Exploring Fn::ForEach and Fn::FindInMap enhancements in AWS CloudFormation

Post Syndicated from Dan Blanco original https://aws.amazon.com/blogs/devops/exploring-fnforeach-and-fnfindinmap-enhancements-in-aws-cloudformation/

AWS CloudFormation, an Infrastructure as Code (IaC) service that lets you model, provision, and manage AWS and third-party resources, recently released a new language transform that enhances the core CloudFormation language. Today, we’ll be covering two more enhancements we’ve added since our initial release: Fn::FindInMap enhancements and a new looping function – Fn::ForEach.

These new language extensions are the result of open discussions with the larger CloudFormation community via our Request For Comments (RFC) proposals for new language features at our Language Discussion GitHub repository. We want to collaborate with the community to better align features and incorporate early feedback into the development cycle to meet the community’s needs. We invite you to participate in new RFCs to help shape the future of the CloudFormation language.

In this post, I’ll dive deeper into the new enhancements for Fn::FindInMap as well as explore the new Fn::ForEach looping mechanism and provide some examples.

Prerequisites

To use these new language features, you must add AWS::LanguageExtensions to the transform section of your template.

---
AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::LanguageExtensions'

If you have a list of transforms, then we recommend having AWS managed transforms at the end, and AWS::LanguageExtensions must be listed before AWS::Serverless.

---
AWSTemplateFormatVersion: 2010-09-09
Transform: 
 - 'AWS::LanguageExtensions'
 - 'AWS::Serverless-2016-10-31'

This transform will cover all of the existing and future language extensions.

FindInMap enhancements

We have updated the language extension transform for CloudFormation to support Fn::FindInMap enhancements, that extend the existing functionality of the Fn::FindInMap intrinsic function so that now you can:

  • use an optional, default value in Fn::FindInMap parameters, if a given key in a Mappings section is not found, and
  • use a number of additional intrinsic functions in the parameters of Fn::FindInMap; for more information, see Supported functions.

Let’s see an example use case where Fn::FindInMap enhancements can help you simplify the business logic of your template, and make it more readable and easier to maintain. Let’s suppose you create a CloudFormation template that describes an Amazon Elastic Compute Cloud (Amazon EC2) instance, and you need to use smaller EC2 instance types for pre-production environments, and a larger EC2 instance type for production for cost savings. In this example, you choose a t2.micro instance type for the dev environment, t2.medium for the qa environment, and t2.large for the prod environment, that you start to describe as follows:

---
AWSTemplateFormatVersion: "2010-09-09"

Description: 'Sample template that describes usage for `Fn::FindInMap` enhancements'

Parameters:
 Environment:
 Description: Lifecycle environment.
 Type: String
 AllowedValues:
 - sandbox
 - dev
 - qa
 - prod
 Default: dev

 LatestAmiId:
 Description: Region-specific image to use.
 Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>  Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

Mappings:
 LifecycleEnvToInstanceType:
 dev:
 InstanceType: t2.micro
 qa:
 InstanceType: t2.medium
 prod:
 InstanceType: t2.large

You described instance types for each of your 3 lifecycle environments in the Mappings section, and you engineered your template to read environment names as input data from Environment in the Parameters section. Looking closer at Environment, you define another allowed value: sandbox, that in this example is an environment for developers to use for prototype testing only: you choose not to include this environment in the mapping you created, with the intent to do the same for any other non-formal environment (for example, a contributor’s personal environment). Next, powered by the new enhancements toFn::FindInMap, you assign a default value for environment names that are different than dev, qa, and prod; this way, the only change you’ll need to make in this context is a new value(s) to AllowedValues in Environment. You describe this business logic in your template, to which you add the sample code shown next:

Transform: AWS::LanguageExtensions

Resources: Ec2Instance: Type: AWS::EC2::Instance Properties: ImageId: !Ref 'LatestAmiId' InstanceType: !FindInMap - LifecycleEnvToInstanceType - !Ref 'Environment' - InstanceType - DefaultValue: t2.micro Tags: - Key: test Value: test

In the snippet above, you have declared the AWS::LanguageExtensions transform, and described your configuration for an EC2 instance in the Resources section. For InstanceType, you chose to use Fn::FindInMap enhancements, and pass DefaultValue as an additional parameter with t2.micro as its value. When the user uses this template to create a stack, and chooses sandbox for Environment, the !Ref 'Environment' reference to the value for Environment will evaluate to sandbox, which is not present in the mapping you created: in this case, t2.micro will be used as a value for InstanceType.

These new enhancements also allow you to use more intrinsic functions inside of Fn::FindInMap. Let’s say you have received requirements to use -env as a suffix to environment names. You choose to make a minimal set of changes to your template, and start with the Parameters section as follows:

Parameters:
  Environment:
    Description: Lifecycle environment.
    Type: String
    AllowedValues:
      - sandbox-env       - dev-env       - qa-env       - prod-env     Default: dev-env

Next, instead of modifying all of your keys the Mappings section, you choose to only change the second parameter to Fn::FindInMap enhancements as follows: first, you use the Fn::Split intrinsic function to split the user-selected environment value string (for example, dev-env) into a list of values using the ‘-‘ character as a delimiter, and next you use the Fn::Select intrinsic function to choose the first element (that is, 0) of that list:

Resources:
  Ec2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref 'LatestAmiId'
      InstanceType: !FindInMap
        - LifecycleEnvToInstanceType         - !Select           - 0           - !Split             - '-'             - !Ref 'Environment'         - InstanceType         - DefaultValue: t2.micro       Tags:
        - Key: test           Value: test

With the updated code above, if the user selects the dev-env value for Environment, Fn::FindInMap enhancements will use dev as the second parameter when looking up values in the Mappings section.

Fn::ForEach intrinsic function

Another enhancement to the language extensions is the addition of native looping inside of CloudFormation with Fn::ForEach. Imagine you have a situation where you need three EC2 instances that look exactly the same. Currently, you would have to copy and paste each instance as a separate resource with CloudFormation:

---
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::LanguageExtensions

Resources:
 FirstInstance:
 Type: AWS::EC2::Instance
 Properties: 
      # ..removed for brevity..

 SecondInstance:
 Type: AWS::EC2::Instance
 Properties: 
      # ..removed for brevity..

 ThirdInstance:
 Type: AWS::EC2::Instance
 Properties: 
      # ..removed for brevity..

If you encounter the need to update one property (the AMI ID, for example), you will have to update all three separately. While this is trivially easy for our example, templates that extend into the hundreds of resources quickly becomes difficult to maintain.

With the Fn::ForEach language extension, we’re able to group all of these items together into an easy-to-manage snippet:

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::LanguageExtensions
 Resources:
 Fn::ForEach::Instances:
    - InstanceLogicalId
    - [FirstInstance, SecondInstance, ThirdInstance]
    - ${InstanceLogicalId}:
 Type: AWS::EC2::Instance
 Properties: 
          # ..removed for brevity..

This results in the following output YAML, which is identical to our previous example:

---
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::LanguageExtensions

Resources:
 FirstInstance:
 Type: AWS::EC2::Instance
 Properties: 
      # ..removed for brevity..

 SecondInstance:
 Type: AWS::EC2::Instance
 Properties: 
      # ..removed for brevity..

 ThirdInstance:
 Type: AWS::EC2::Instance
 Properties: 
      # ..removed for brevity..

To break down the syntax, the Fn::ForEach function requires:

  • A Logical ID for the looping function directly following the Fn::ForEach call. In our case, we named it Instances
  • The variable name we’ll be referencing in our snippet below
  • The collection of strings we’ll be iterating over. You can write these inline, or pass them as parameters or mappings.
  • A section of the template we’ll be iterating over using the variable name above. This is standard CloudFormation JSON/YAML.

These must be listed in an array immediately following the Fn::ForEach intrinsic function and in this exact order.

We can use our key to reference values found elsewhere in the template. This adds additional flexibility when combined with the aforementioned FindInMap enhancements. Imagine a similar scenario where each instance needs a specific instance type, dictated by which instance it is and which environment we’re in. As described before, we would add our parameters and mappings to our template:

---
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::LanguageExtensions

Parameters:
 Environment:
 Description: Lifecycle environment.
 Type: String
 AllowedValues:
 - sandbox
 - dev
 - qa
 - prod
 Default: dev

 LatestAmiId:
 Description: Region-specific image to use.
 Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>  Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

Mappings:
 dev:
 FirstInstance:
 InstanceType: t2.micro
 SecondInstance:
 InstanceType: t2.micro
 ThirdInstance:
 InstanceType: t2.micro
 qa:
 FirstInstance:
 InstanceType: t2.medium
 SecondInstance:
 InstanceType: t2.medium
 ThirdInstance:
 InstanceType: t2.large
 prod:
 FirstInstance:
 InstanceType: t2.large
 SecondInstance:
 InstanceType: t2.xlarge
 ThirdInstance:
 InstanceType: t2.2xlarge

Given this configuration, we have different environment values as Parameters and a Mapping section that details our sizing requirements for our instance. With this, we can then use our new Fn::ForEach functionality and FindInMap enhancements:

Resources:
  Fn::ForEach::Instances:
    - InstanceLogicalId     - [FirstInstance, SecondInstance, ThirdInstance]     - ${InstanceLogicalId}:         Type: AWS::EC2::Instance
        Properties: 
          ImageId: !Ref LatestAmiId
          InstanceType: !FindInMap
            - !Ref Environment             - !Ref InstanceLogicalId             - InstanceType             - DefaultValue: t2.micro

This results in the following output:

Resources:
  FirstInstance:
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: !Ref LatestAmiId
      InstanceType: !FindInMap
        - !Ref Environment
        - FirstInstance
        - InstanceType
        - DefaultValue: t2.micro


  SecondInstance:
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: !Ref LatestAmiId
      InstanceType: !FindInMap
        - !Ref Environment
        - SecondInstance
        - InstanceType
        - DefaultValue: t2.micro

  ThirdInstance:
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: !Ref LatestAmiId
      InstanceType: !FindInMap
        - !Ref Environment
        - ThirdInstance
        - InstanceType
        - DefaultValue: t2.micro

This looping feature can be used to create more than just resources – say we want to reference outputs from these EC2 instances as we create them. We can modify our above template to add an Output section and iterate over it in the same way, as well as exporting the instance ID. We can even express more than one Output per iteration. We’ll also move the instances list to a parameter for increased clarity.

Parameters:
 InstancesToManage:
 Type: CommaDelimitedList
 Description: Instances to be managed
 Default: FirstInstance,SecondInstance,ThirdInstance
 Outputs:
 Fn::ForEach::InstanceOutputs:
    - InstanceLogicalId
    - !Ref InstancesToManage
    - "${InstanceLogicalId}Id": 
 Export: 
 Name: !Sub ${AWS::AccountId}-${InstanceLogicalId}Id
 Value: !Ref 
 Ref: InstanceLogicalId

      "${InstanceLogicalId}AvailabilityZone":
 Value:
 Fn::GetAtt: 
            - !Ref InstanceLogicalId
            - AvailabilityZone

This outputs to:

Outputs:
 FirstInstanceId: 
 Export: 
 Name: !Sub ${AWS::AccountId}-FirstInstanceId
 Value: !Ref FirstInstance
 FirstInstanceAvailabilityZone:
 Value:
 Fn::GetAtt:
        - FirstInstance
        - AvailabilityZone
 SecondInstanceId: 
 Export: 
 Name: !Sub ${AWS::AccountId}-SecondInstanceId
 Value: !Ref SecondInstance
 SecondInstanceAvailabilityZone:
 Value:
 Fn::GetAtt:
        - SecondInstance
        - AvailabilityZone
 ThirdInstanceId: 
 Export: 
 Name: !Sub ${AWS::AccountId}-ThirdInstanceId
 Value: !Ref ThirdInstance
 ThirdInstanceAvailabilityZone:
 Value:
 Fn::GetAtt:
        - ThirdInstance
        - AvailabilityZone

In this snippet, we iterated over our collection and created multiple outputs. For each output, we concatenated our key with some other string. In this case, both Id and AvailabilityZone were concatenated with the key to create a unique output name based on the stack name and the logical ID of the resource.

Finally, loops can be nested inside other loops. Combined with the ability to concatenate values and do lookups inside of the Mapping section, we’re able to significantly simplify complex CloudFormation templates. Imagine an example where we are tasked with creating a Virtual Private Cloud (VPC) with three private subnets and three public subnets. This is a common configuration our customers have and we can configure it simply with looping.

---
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::LanguageExtensions

Parameters:
 AvailabilityTypes:
 Type: CommaDelimitedList
 Description: Types of subnets availability - public, private, or both
 AllowedValues:
 - Public
 - Private
 Default: Public,Private

Mappings:
 SubnetOne:
 Public: 
 Cidr:  10.215.0.0/24 
 Private: 
 Cidr:  10.215.1.0/24 
 SubnetTwo:
 Public: 
 Cidr:  10.215.2.0/24
 Private: 
 Cidr:  10.215.3.0/24
 SubnetThree:
 Public: 
 Cidr:  10.215.4.0/24
 Private: 
 Cidr:  10.215.5.0/24

Resources:
 VPC:
 Type: AWS::EC2::VPC
 Properties:
 CidrBlock: 10.215.0.0/16
 EnableDnsSupport: true
 EnableDnsHostnames: true

 Fn::ForEach::Subnets:
 - SubnetIdentifier
 - - SubnetOne
 - SubnetTwo
 - SubnetThree
 - Fn::ForEach::SubnetAvailabilityType:
 - AvailabilityType
 - !Ref AvailabilityTypes
 - "${SubnetIdentifier}${AvailabilityType}":
 Type: AWS::EC2::Subnet
 Properties:
 VpcId: !Ref VPC
 CidrBlock: !FindInMap
 - !Ref SubnetIdentifier
 - !Ref AvailabilityType
 - Cidr

which outputs the following resource section:

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true

  SubnetOnePublic:
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap
        - SubnetOne         - Public         - Cidr 
  SubnetOnePrivate:
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap
        - SubnetOne         - Private         - Cidr 
  SubnetTwoPublic:
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap
        - SubnetTwo         - Public         - Cidr 
  SubnetTwoPrivate:
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap
        - SubnetTwo         - Private         - Cidr 
  SubnetThreePublic:
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap
        - SubnetThree         - Public         - Cidr 
  SubnetThreePrivate:
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap
        - SubnetThree         - Private         - Cidr

Combining everything we’ve learned so far, this created six subnets total, three public, three private, and attached them to the respective VPC with a relevant CIDR block.

We’re excited to share this functionality with our community, and we invite you to share feedback on future enhancements to the looping functionality here. A few enhancements we’re discussing are:

  • Iterating over a key/value pair
  • Iterating over a list of lists
  • Support in other template sections
  • And more!

Please head over and let us know what you think!

Conclusion

In this post, we walked through the new CloudFormation additions to the language extensions transform, how to enable them in your templates, and how to engage in future language extensions via our open language discussion repository. Leave us your feedback at our Language Discussion GitHub repository to help shape the future of the CloudFormation language. We look forward to hearing from you!

 

About the Author:

Dan Blanco

Dan is a senior AWS Developer Advocate based in Atlanta for the AWS IaC team. When he’s not advocating for IaC tools, you can either find him in the kitchen whipping up something delicious or flying in the Georgia sky. Find him on twitter (@TheDanBlanco) or in the AWS CloudFormation Discord server