Dynamic Bindings for CloudFormation Stacks using Fn::ImportValue
Tags: aws, cloudformation
Published: 2020-07-01 13:22:32

# Overview

CloudFormation offers the ability to export a value from one stack and import it into another stack. When a stack value is imported, it locks the original and prevents it from changing. I like to think of it as _binding_ one stack to another. While the dependency is in use, the original value cannot be modified.

This capability is a double edged sword and, when misused, downright a liability. In this article, I show how to make bindings dynamic, to retain their beneficial properties, but also to give us more flexibility with deployed stacks.

# Basic Approach

To export a value from a stack, use the [Outputs section](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html) in the CloudFormation template and add an `Export` statement to make the value available with a export name. The export name must be unique to the region of the account.

```yaml
Outputs:
  MyOutputValue:
    Value: A value to export
    Export:
      Name: MyExportedValue
```

To import the value from another stack, use the export name with the [`Fn::ImportValue` intrinsic function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html).

```yaml
!ImportValue MyExportedValue
```

The second stack is now bound to the exported value from the first stack and the first stack can no longer modify it. Attempting to do so will cause the CloudFormation update operation to fail and rollback.

Dependencies between stacks can be listed with the [AWS CLI](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/list-imports.html) or, more conveniently, viewed in the [AWS console](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-imports.html).

# Problem 1 - Export Name Collision

The first issue with the basic approach is export name collisions since they have to be unique for a region. If a stack attempts to export a value with the same export name, it will fail. Fortunately, the `Export` statement allows us to use intrinsic functions to make the name dynamic.

# Solution 1 - Dynamic Export Name

My preferred convention is to prefix the output name with the stack name. This approach has two benefits. First, it ensures export names are unique, since the stack name must be unique to begin with. Second, it is easy to correlate the export name to the output in the template.

```yaml
Outputs:
  MyOutputValue:
    Value: A value to export
    Export:
      Name: !Sub "${AWS::StackName}::MyOutputValue"
```

# Problem 2 - Fixed Binding

The second issue is that once a stack is deployed with `Fn::ImportValue`, we are locked in unless we modify the importing template and deploy it again.

As long as the dependency is active, we cannot modify the exported value. This means, we have to either modify all importing stacks or delete them. That is a poor set of choices.

# Solution 2a - Dynamic Binding (v1)

The solution is to make our use of `Fn::ImportValue` dynamic by using a CloudFormation parameter to define the binding reference. By giving the parameter the export name as default value, we can deploy the stack the same way as before, but now we have the option of remapping our binding to another stack when needed.

```yaml
Parameters: 
  MyOutputValueBinding: 
    Type: String
    Default: MySourceStackName::MyOutputValue
```

The updated `!ImportValue` expression now must reference the new parameter.

```yaml
Fn::ImportValue:
  !Ref MyOutputValueBinding
```

# Solution 2b - Dynamic Binding (v2)

Our first solution, however, has a rather annoying limitation: we can only remap our imports to another stack. Consequently, to modify an exported value, we must first deploy a new stack with equivalent outputs as we want to modify, then remap the bindings of all importing stacks, update the original exported value, then revert the bindings to the original stack, and finally delete the intermediate stack.

We can make our lives much easier if we allow fixed values in addition to dynamic references for `Fn::ImportValue`. To do so, we must be able to tell the difference if the parameter value indicates a fixed value or a binding reference. My preferred discriminator is to use `$` as a prefix. When present, the value is a binding reference, otherwise, it's a fixed value. 

First, we need to amend our default value for the parameter to indicate the provided value is a binding reference by using the `$` prefix.

```yaml
Parameters: 
  MyOutputValueBinding: 
    Type: String
    Default: $MySourceStackName::MyOutputValue
```

Second, we need a condition to determine if the parameter has the `$` prefix. To check for the prefix, we use an implementation of the missing `Fn::StartsWith` function from a [previous post](https://dev.to/lambdasharp/emulating-fn-startswith-in-cloudformation-756).

```yaml
Conditions:
  MyOutputValueBindingIsImported:
    !And [ 
      !Not [ !Equals [ !Ref MyOutputValueBinding, "" ]],
      !Equals [ !Select [ 0, !Split [ "$", !Ref MyOutputValueBinding ], "" ]]
    ]
```

Finally, we need to update our usage of `Fn::ImportValue` to make it conditional. When the parameter indicates a binding reference, we use it with `Fn::ImportValue`. Otherwise, we use `!Ref` to use the parameter value as is.

```yaml
!If 
  - MyOutputValueBindingIsImported
  - Fn::ImportValue:
      !Ref MyOutputValueBinding
  - !Ref MyOutputValueBinding
```

# Conclusion

With a bit of work, we can now have our cake and eat it too! For normal operations, we use binding references to import values from other stacks and lock them down as traceable dependencies. However, when the time comes to refactor those dependencies, we can remap the bindings to a new stack, or if need be, to fixed values. All of this can be done just by changing stack parameters and without requiring any updates to the CloudFormation template. This gives the operations team the flexibility to transition between stack deployment topologies through configuration changes rather than code changes.

I hope you found this helpful and Happy Hacking!