12

I want to read the URL of my database from parameter store in my CloudFormation template. This is easy enough for a single URL, but I can't figure out how to change the URL with different environments.

I have four environments (development, integration, pre-production and production) and their details are stored on four different paths in Parameter Store:

/database/dev/url
/database/int/url
/database/ppe/url
/database/prod/url

I now want to pick the correct Database URL when deploying via CloudFormation. How can I do this?

Parameters:
  Environment:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - int
      - ppe
      - prod
  DatabaseUrl:
    Type: 'AWS::SSM::Parameter::Value<String>'
    # Obviously the '+' operator here won't work - so what do I do?
    Default: '/database/' + Environment + '/url'
Frederik
  • 1,458
  • 4
  • 15
  • 26
  • If you can't find a better solution, you can always pre-process the template and fill in the environment in all the SSM parameters. – kichik Feb 13 '18 at 23:55
  • Would appreciate if the submitter identified the answer to his question. – Adam Apr 23 '19 at 14:18

5 Answers5

7

This feature isn't as neat as one would wish. You have to actually pass name/path of each parameter that you want to look up from the parameter store.

Template:

AWSTemplateFormatVersion: 2010-09-09

Description: Example

Parameters:

  BucketNameSuffix:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /example/dev/BucketNameSuffix

Resources:

  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub parameter-store-example-${BucketNameSuffix}

If you don't pass any parameters to the template, BucketNameSuffix will be populated with the value stored at /example/dev/BucketNameSuffix. If you want to use, say, a prod value (pointed to by /example/prod/BucketNameSuffix), then you should specify value for parameter BucketNameSuffix, but instead of passing the actual value you should pass the alternative name of the parameter to use, so you'd pass /example/prod/BucketNameSuffix.

aws cloudformation update-stack --stack-name example-dev \
--template-body file://./example-stack.yml

aws cloudformation update-stack --stack-name example-prod \
--template-body file://./example-stack.yml \
--parameters ParameterKey=BucketNameSuffix,ParameterValue=/example/prod/BucketNameSuffix

A not so great AWS blog post on this: https://aws.amazon.com/blogs/mt/integrating-aws-cloudformation-with-aws-systems-manager-parameter-store/

Because passing million meaningless parameters seems stupid, I might actually generate an environment-specific template and set the right Default: in the generated template so for prod environment Default would be /example/prod/BucketNameSuffix and then I can update the prod stack without passing any parameters.

jbasko
  • 7,028
  • 1
  • 38
  • 51
2

You can populate CloudFormation templates with parameters stored in AWS Systems Manager Parameter Store using Dynamic References

In this contrived example we make two lookups using resolve:ssm and replace the environment using !Join and !Sub

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Environment:
    Type: String
    Default: prod
    AllowedValues:
      - prod
      - staging
Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        Image: !Join
          - ''
          -   - 'docker.io/bitnami/redis@'
              - !Sub '{{resolve:ssm:/app/${Environment}/digest-sha/redis}}'
        Environment:
          - Name: DB_HOST
            Value: !Sub '{{resolve:ssm:/app/${Environment}/db-host}}'
Tim
  • 3,091
  • 9
  • 48
  • 64
0

You can make use of Fn::Join here.

Here is some pseudo code.

You will have to take Environment as an parameter, which you are already doing.

Creating the required string in the resources where DatabaseUrl is required.

Resources :
  Instance :
    Type : 'AWS::Some::Resource'
    Properties :
       DatabaseURL : !Join [ "", [ "/database/", !Ref "Environment" , "/url ] ]

Hope this helps.

Note: You cannot assign value to a Parameter dynamically using some computation logic. All the values for defined parameters should be given as Input.

krisnik
  • 1,406
  • 11
  • 18
  • 1
    Unfortunately, this does not solve the problem. You would need to do this on the Default of BucketNameSuffix, but that is not allowed by CloudFormation :( – okaram May 09 '19 at 15:52
0

I like Fn::Sub, which is much cleaner and easy to read.

!Sub "/database/${Environment}/url"
  • 1
    Unfortunately, this does not solve the problem. You would need to do this on the Default of BucketNameSuffix, but that is not allowed by CloudFormation :( – okaram May 09 '19 at 15:52
0

I am stuck into the same problem, below are my findings:

  1. We can compose values and descriptions while writing into the SSM Parameter Store from CloudFormation like below :

    LambdaARN:
      Type: AWS::SSM::Parameter
      Properties:
        Type: String
        Description: !Sub "Lambda ARN from ${AWS::StackName}"
        Name: !Sub "/secure/${InstallationId}/${AWS::StackName}/lambda-function-arn"
        Value: !GetAtt LambdaFunction.Arn
    
  2. We can not compose values/defaults to looking for in SSM Parameter Store. Like below:

    Parameters:
    ...
      LambdaARN:
        Type: Type: AWS::SSM::Parameter::Value<String>
        Value: !Sub "/secure/${InstallationId}/teststack/lambda-function-arn"
    

This is not allowed as per AWS Documentation[1]. Both(Sub/Join) Functions won't work. Following is the error which I was getting:

An error occurred (ValidationError) when calling the CreateChangeSet operation: Template format error: Every Default member must be a string.

  1. Composing and passing values while creating stacks can be done like this:

    Parameters:
    ...
      LambdaARN:
        Type: Type: AWS::SSM::Parameter::Value<String>
    ....
    
    $ aws cloudformation deploy --template-file cfn.yml --stack-name mystack  --parameter-overrides 'LambdaARN=/secure/devtest/teststack/lambda_function-arn'
    
  2. If you add your custom tags while putting the values in the Parameter Store, it will overwrite the default tags added by CFN.

Default Tags:

- aws:cloudformation:stack-id
- aws:cloudformation:stack-name
- aws:cloudformation:logical-id
  1. Every time we update the values in parameter store, it creates a new version, which is beneficial when we are using DynamicResolvers, this can serve as a solution to the problem in the question like

{{ resolve:ssm:/my/value:1}}

The last field is the version. Where different versions can point to different environments.

  1. We are using versions with the parameters, and adding the labels to them[2], this can't be done via CFN[3], only possible way via CLI or AWS Console. This is AWS's way of handling multiple environments.

[1] https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html

[2] https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-labels.html

[3] https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html

samtoddler
  • 8,463
  • 2
  • 26
  • 21