Mastering AWS CloudFormation
上QQ阅读APP看书,第一时间看更新

Referring to existing stacks

In large environments with shared responsibility models, where different teams manage their own stack resources, we sometimes have a situation that resources or resource attributes (RDS instance endpoints, for example) have to be shared between different stacks.

Remember that we have a network stack in our application. Resources from this stack, such as subnets, are used in the application templates.

Whenever you deploy a virtual machine, AutoScaling group, load balancer, or a database cluster, you need to specify the subnet ID. We can specify that subnet ID as a parameter and then use Fn::Ref to map it in the resource attribute, but that will require a lot of unnecessary actions that can be avoided, by using exports and Fn::ImportValue.

In Chapter 1, CloudFormation Refresher, we used outputs to obtain the ARN of the IAM role, which we used in the AWS command line.

Outputs are handy for quickly accessing the attributes of created resources, but they can also be transformed into exports.

Exports are the aliases to the physical resource IDs. Each Export name must be unique within a region, but doesn't have to be within a single account.

This means that when we want to refer to the resource attribute created in another stack, we need to create an output record with the Export name. Once this is done and the stack is created, we can then refer to it using an intrinsic function called Fn::ImportValue.

The Fn::ImportValue function will look up the necessary attribute in the CloudFormation exported values across the whole account within a region and write a necessary entry to the resource attribute.

If this sounds a bit complicated, let's look at how this works in an example. Here, we create subnets in our core stack and we need to use their IDs in our WebTier (and other) stacks. This is what our core template will look like:

core.yaml

Parameters:

# ...

Resources:

  Vpc:

    Type: AWS::EC2::VPC

    Properties:

      # ...

  PublicSubnet1:

    Type: AWS::EC2::Subnet

    Properties:

      # ...

  # The rest of our resources...

The Outputs section is as follows:

Outputs:

  VpcId:

    Value: !Ref Vpc

    Export:

      Name: Vpc

  PublicSubnet1Id:

    Value: !Ref PublicSubnet1

    Export:

      Name: PublicSubnet1Id

  PublicSubnet2Id:

    Value: !Ref PublicSubnet2

    Export:

      Name: PublicSubnet2Id

  PublicSubnet3Id

    Value: !Ref PublicSubnet3

    Export:

      Name: PublicSubnet3Id

  # And so on

Here is how we refer to exported values from the core stack:

webtier.yaml

Resources:

  WebTierAsg:

    Type: AWS::AutoScaling::AutoScalingGroup

    Properties:

      # Some properties...

      VpcZoneIdentifier:

        - !ImportValue PublicSubnet1Id

        - !ImportValue PublicSubnet2Id

        - !ImportValue PublicSubnet3Id

      # And so on...

You've noticed that I'm doing extra work here by creating additional exports and importing those values one by one.

Sometimes, you need to import single attributes, such as a shared secret or an IAM role for ECS task execution, but for listed items, such as subnets or Availability Zones, it is wise to combine them into lists and refer to them as lists.

Let's refactor the preceding example into a simpler structure:

core.yaml

Outputs:

  PublicSubnetIds:

    Value: !Split [",", !Join [",", [!Ref PublicSubnet1, !Ref PublicSubnet2, !Ref PublicSubnet3]  ]  ]

    Export:

      Name: PublicSubnetIds

What we do here is that we make a comma-separated string with our subnet IDs and then use Fn::Split to turn this long string into a list. Since there is no intrinsic function to generate a list of elements, we have to use this workaround.

Now, we can import this value in a shorter form:

webtier.yaml

Resoures:

  WebTierAsg:

    Type: AWS::AutoScaling::AutoScalingGroup

    Properties:

      # Some properties

      VpcZoneIdentifier: !ImportValuePublicSubnetIds

Since we already pass a list of values, CloudFormation will parse it properly and create the instance's AutoScaling group the way we need it.

Outputs/exports also support conditional functions. In the previous section (Deletion policies), we had to declare the same DB instance multiple times. If we create exports for each of them, the stack creation will fail because not every single one of them is being created. It is wise to also use conditional functions there.

Important note

Note that outputs do not have the condition attribute, like resources. We have to use conditional functions in order to use the correct value for exports.

database.yaml

Outputs:

  DbEndpoint:

    Value: !If [ ProdEnv, !GetAttProdDatabase.Endpoint.Address, !If [ TestEnv, !GetAttTestDatabase.Endpoint.Address,!GetAtt.DevDatabase.Endpoint.Address ]]

    Export:

      Name: DbEndpoint

This is one of the cases when a short syntax form makes our template almost unreadable, so let's write it in a standard syntax form:

Outputs:

  DbEndpoint:

    Value:

      Fn::If:

        - ProdEnv

        - !GetAttProdDatabase.Endpoint.Address

        - Fn::If:

            - TestEnv

            - !GetAttTestDatabase.Endpoint.Address

            - !GetAttDevDatabase.Endpoint.Address

    Export:

      Name: DbEndpoint

Looks much better, right? However, it is up to you to decide which syntax form (long or short) to choose when you declare complex conditional checks. Just don't forget that you write the code not only for the machine (CloudFormation, in our case), but also for your fellow colleagues.

Now, let's move to the next important topic, which is about AWS pseudo parameters.