DEV Community

Steve Bjorg for LambdaSharp

Posted on • Edited on

Dynamic Bindings for CloudFormation Stacks using Fn::ImportValue

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 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.

Outputs:
  MyOutputValue:
    Value: A value to export
    Export:
      Name: MyExportedValue
Enter fullscreen mode Exit fullscreen mode

To import the value from another stack, use the export name with the Fn::ImportValue intrinsic function.

!ImportValue MyExportedValue
Enter fullscreen mode Exit fullscreen mode

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 or, more conveniently, viewed in the AWS console.

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.

Outputs:
  MyOutputValue:
    Value: A value to export
    Export:
      Name: !Sub "${AWS::StackName}::MyOutputValue"
Enter fullscreen mode Exit fullscreen mode

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.

Parameters: 
  MyOutputValueBinding: 
    Type: String
    Default: MySourceStackName::MyOutputValue
Enter fullscreen mode Exit fullscreen mode

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

Fn::ImportValue:
  !Ref MyOutputValueBinding
Enter fullscreen mode Exit fullscreen mode

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.

Parameters: 
  MyOutputValueBinding: 
    Type: String
    Default: $MySourceStackName::MyOutputValue
Enter fullscreen mode Exit fullscreen mode

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.

Conditions:
  MyOutputValueBindingIsImported:
    !And [ 
      !Not [ !Equals [ !Ref MyOutputValueBinding, "" ]],
      !Equals [ !Select [ 0, !Split [ "$", !Ref MyOutputValueBinding ], "" ]]
    ]
Enter fullscreen mode Exit fullscreen mode

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.

!If 
  - MyOutputValueBindingIsImported
  - Fn::ImportValue:
      !Ref MyOutputValueBinding
  - !Ref MyOutputValueBinding
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)