32

Is there any way to define shortcuts for often-used values derived from CloudFormation template parameters?

For example - I've got a script that creates a Multi-AZ Project stack with ELB name project and two instances behind the ELB called project-1 and project-2. I only pass ELBHostName parameter to the template and later on use it to construct :

"Fn::Join": [
    ".", [
        { "Fn::Join": [ "", [ { "Ref": "ELBHostName" }, "-1" ] ] },
        { "Ref": "EnvironmentVersioned" },
        { "Ref": "HostedZone" }
    ]
]

This construction or very similar is repeated many times throughout the template - to create the EC2 host name, Route53 records, etc.

Instead of repeating that over and over again I would like to assign the output of that Fn::Join to a variable of some sort and only refer to that, just like I can with "Ref": statement.

Ideally something like:

Var::HostNameFull = "Fn::Join": [ ... ]
...
{ "Name": { "Ref": "Var::HostNameFull" } }

or something similarly simple.

Is that possible with Amazon CloudFormation?

MLu
  • 23,798
  • 5
  • 54
  • 81
  • Is ELBHostName full a parameter you're explicitly passing to Cloudformation? If so, why use a Ref? Might could use Mustache to include variables in your template and transform that to JSON before shipping it off to Cloudformation. Depends what you're provisioning process looks like. – Canuteson Sep 10 '16 at 06:29

5 Answers5

18

I don't have an answer, but did want to point out that you can save yourself a lot of pain by using Fn::Sub in place of Fn::Join

{ "Fn::Sub": "${ELBHostName"}-1.${EnvironmentVersioned}.${HostedZone}"}

Replaces

"Fn::Join": [
    ".", [
        { "Fn::Join": [ "", [ { "Ref": "ELBHostName" }, "-1" ] ] },
        { "Ref": "EnvironmentVersioned" },
        { "Ref": "HostedZone" }
    ]
]
Kevin Audleman
  • 281
  • 2
  • 2
7

No. I tried it, but came up empty. The way that made sense to me was to create a Mappings entry called "CustomVariables" and to have that house all my variables. It works for simple Strings, but you can't use Intrinsics (Refs, Fn::Joins, etc.) inside Mappings.

Works:

"Mappings" : {
  "CustomVariables" : {
    "Variable1" : { "Value" : "foo" },
    "Variable2" : { "Value" : "bar" }
  }
}

Won't work:

  "Variable3" : { "Value" : { "Ref" : "AWS::Region" } }

That's just an example. You wouldn't put a standalone Ref in a Variable.

Rob
  • 81
  • 5
  • 2
    Documentation says mapping values must be literal strings. – Ivan Anishchuk Nov 03 '16 at 09:03
  • Checked the document, it says "You cannot include parameters, pseudo parameters, or intrinsic functions in the Mappings section.". https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html – wei Jan 08 '21 at 06:44
7

I was looking of the same functionality. Using a nested stack as SpoonMeiser suggested came to mind, but then I realised that what I actually needed was custom functions. Luckily CloudFormation allows the use of AWS::CloudFormation::CustomResource that, with a bit of work, allows one to do just that. This feels like overkill for just variables (something I would argue that should have been in CloudFormation in the first place), but it gets the job done, and, in addition, allows for all the flexibility of (take your pick of python/node/java). It should be noted that lambda functions cost money, but we're talking pennies here unless you create/delete your stacks multiple times per hour.

First step is to make a lambda function on this page that does nothing but take the input value and copy it to the output. We could have the lambda function do all sorts of crazy stuff, but once we have the identity function, anything else is easy. Alternatively we could have the lambda function being created in the stack itself. Since I use many stacks in 1 account, I would have a whole bunch of leftover lambda functions and roles (and all stacks need to be created with --capabilities=CAPABILITY_IAM, since it also needs a role.

Create lambda function

  • Go to lambda home page, and select your favourite region
  • Select "Blank Function" as template
  • Click "Next" (don't configure any triggers)
  • Fill in:
    • Name: CloudFormationIdentity
    • Description: Returns what it gets, variable support in Cloud Formation
    • Runtime: python2.7
    • Code Entry Type: Edit Code Inline
    • Code: see below
    • Handler: index.handler
    • Role: Create a Custom Role. At this point a popup opens that allows you to create a new role. Accept everything on this page and click "Allow". It will create a role with permissions to post to cloudwatch logs.
    • Memory: 128 (this is the minimum)
    • Timeout: 3 seconds (should be plenty)
    • VPC: No VPC

Then copy-paste the code below in the code field. The top of the function is the code from the cfn-response python module, that does only get auto-installed if the lambda-function is created through CloudFormation, for some strange reason. The handler function is pretty self-explanatory.

from __future__ import print_function
import json

try:
    from urllib2 import HTTPError, build_opener, HTTPHandler, Request
except ImportError:
    from urllib.error import HTTPError
    from urllib.request import build_opener, HTTPHandler, Request


SUCCESS = "SUCCESS"
FAILED = "FAILED"


def send(event, context, response_status, reason=None, response_data=None, physical_resource_id=None):
    response_data = response_data or {}
    response_body = json.dumps(
        {
            'Status': response_status,
            'Reason': reason or "See the details in CloudWatch Log Stream: " + context.log_stream_name,
            'PhysicalResourceId': physical_resource_id or context.log_stream_name,
            'StackId': event['StackId'],
            'RequestId': event['RequestId'],
            'LogicalResourceId': event['LogicalResourceId'],
            'Data': response_data
        }
    )
    if event["ResponseURL"] == "http://pre-signed-S3-url-for-response":
        print("Would send back the following values to Cloud Formation:")
        print(response_data)
        return

    opener = build_opener(HTTPHandler)
    request = Request(event['ResponseURL'], data=response_body)
    request.add_header('Content-Type', '')
    request.add_header('Content-Length', len(response_body))
    request.get_method = lambda: 'PUT'
    try:
        response = opener.open(request)
        print("Status code: {}".format(response.getcode()))
        print("Status message: {}".format(response.msg))
        return True
    except HTTPError as exc:
        print("Failed executing HTTP request: {}".format(exc.code))
        return False

def handler(event, context):
    responseData = event['ResourceProperties']
    send(event, context, SUCCESS, None, responseData, "CustomResourcePhysicalID")
  • Click "Next"
  • Click "Create Function"

You can now test the lambda function by selecting the "Test" button, and select "CloudFormation Create Request" as sample template. You should see in your log that the variables fed to it, are returned.

Use variable in your CloudFormation template

Now that we have this lambda function, we can use it in CloudFormation templates. First make note of the lambda function Arn (go to the lambda home page, click the just created function, the Arn should be in the top right, something like arn:aws:lambda:region:12345:function:CloudFormationIdentity).

Now in your template, in the resource section, specify your variables like:

Identity:
  Type: "Custom::Variable"
  Properties:
    ServiceToken: "arn:aws:lambda:region:12345:function:CloudFormationIdentity"
    Arn: "arn:aws:lambda:region:12345:function:CloudFormationIdentity"

ClientBucketVar:
  Type: "Custom::Variable"
  Properties:
    ServiceToken: !GetAtt [Identity, Arn]
    Name: !Join ["-", [my-client-bucket, !Ref ClientName]]
    Arn: !Join [":", [arn, aws, s3, "", "", !Join ["-", [my-client-bucket, !Ref ClientName]]]]

ClientBackupBucketVar:
  Type: "Custom::Variable"
  Properties:
    ServiceToken: !GetAtt [Identity, Arn]
    Name: !Join ["-", [my-client-bucket, !Ref ClientName, backup]]
    Arn: !Join [":", [arn, aws, s3, "", "", !Join ["-", [my-client-bucket, !Ref ClientName, backup]]]]

First I specify an Identity variable that contains the Arn for the lambda function. Putting this in a variable here, means I only have to specify it once. I make all my variables of type Custom::Variable. CloudFormation allows you to use any type-name starting with Custom:: for custom resources.

Note that the Identity variable contains the Arn for the lambda function twice. Once to specify the lambda function to use. The second time as the value of the variable.

Now that I have the Identity variable, I can define new variables using ServiceToken: !GetAtt [Identity, Arn] (I think JSON code should be something like "ServiceToken": {"Fn::GetAtt": ["Identity", "Arn"]}). I create 2 new variables, each with 2 fields: Name and Arn. In the rest of my template I can use !GetAtt [ClientBucketVar, Name] or !GetAtt [ClientBucketVar, Arn] whenever I need it.

Word of caution

When working with custom resources, if the lambda function crashes, you're stuck for between 1 and 2 hours, because CloudFormation waits for a reply from the (crashed) function for an hour before giving up. Therefore it might be good to specify a short timeout for the stack while developing your lambda function.

Claude
  • 196
  • 1
  • 3
  • Awesome answer! I read it and ran it in my stacks, though for me, I don't worry about the proliferation of lambda functions in my account and I like templates that are standalone (I modularize using the `cloudformation-tool` gem), so I pack the lambda creation into the template and then can use it directly instead of creating the `Identity` custom resource. See here for my code: https://gist.github.com/guss77/2471e8789a644cac96992c4102936fb3 – Guss Nov 09 '17 at 14:35
  • When you are "...you're stuck for between 1 and 2 hours..." because a lambda crashed and did not reply back with a cfn-response, you can get the template moving again by manually using curl/wget on the signed URL. Just make sure to always print out the event/URL at the beginning of the lambda so that you can go to CloudWatch and get the URL if it does hang. – Taylor Mar 08 '19 at 15:21
3

You could use a nested stack which resolves all your variables in it's outputs, and then use Fn::GetAtt to read the outputs from that stack

Use another stack as nested stack & pass in computed Parameter sample:


  YourResourceName:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./YourStackYouUseAsNested.yaml
      Parameters:
        ParamOfNestedStack: !Join [ "-", [ "you-compute-value", !Ref AWS::Region ] ]

Lukas Liesis
  • 123
  • 7
SpoonMeiser
  • 183
  • 2
  • 7
  • Wouldn't the nested stack need to define some resources as that section is required? – wheelerswebservices Sep 06 '20 at 22:50
  • Nested Stack will define everything like any normal stack, when including stack as nested stack you pass in Parameters needed for that stack you trying to include. So if you include stack which uses some variable in several places, you pass in computed value to nested stack. added sample to answer for more clarity – Lukas Liesis Aug 19 '21 at 10:00
2

You might use nested templates in which you "resolve" all your variables in the outer template and pass them to another template.

JoseOlcese
  • 166
  • 2