Using AWS Systems Manager Parameter Store Secure String parameters in AWS CloudFormation templates

When using AWS CloudFormation templates to code your infrastructure, you should consider applying best practices to improve the maintainability of your code. Further, these best practices should be augmented by guidelines like those outlined for twelve-factor apps, which are targeted at optimizing applications for continuous deployment. Of these factors, you should note that you should strive for strict separation of configuration information from your code wherever possible, since configuration information will often vary across deployments, while code typically does not. Also, from a code and configuration perspective, you should strive to keep your development and production code as similar as possible because this will aid quick replication of any production issues or bugs.

With AWS CloudFormation, you have several options to avoid storing configuration as constants in your template code, which amounts in most cases to hardcoding runtime configuration details. Instead, you should take advantage of various options for using parameters. Further, you can elicit those parameters from users in the AWS Management Console, or from a separate runtime variable by using a CLI or API call, or by using a parameter file.

With AWS Systems Manager Parameter Store, you can set up parameter strings that you can store and update centrally, so they can be reused in template code and in many other use cases beyond CloudFormation. Support for Parameter Store integration with CloudFormation was first released in December 2017 for plain text strings, and you can review some additional examples on how to use non-secure strings in that post. Support for secure strings was announced recently. We’ll cover an example on how to use secure strings later in this article.

Example 1: Two ways to use Parameter Store plain text string parameters

To review the various options available for reusing Parameter Store values in your template code, see the following snippet:

AWSTemplateFormatVersion: "2010-09-09"
Description: How to use existing SSM Parameters in CloudFormation
Parameters:
KeyName:
Type: 'AWS::EC2::KeyPair::KeyName'
Default: brinks
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
Resources:
PSBInstance:
Type: "AWS::EC2::Instance"
Properties:
ImageId: !Ref LatestAmiId
KeyName: !Ref KeyName
InstanceType: '{{resolve:ssm:ssbEC2iDev:1}}'

Two parameters are declared in this snippet, and a third parameter is used inline but not explicitly declared. The first parameter, KeyName, is unrelated to Parameter Store. It’s an example of reusing an external parameter using AWS-Specific parameter types in CloudFormation. In this example, by using a default value, you’ll allow the KeyPair to be selected from a drop-down list in the AWS Management Console. You’ll also have a value pre-selected by default. This can cut down on typing errors in the console and avoid missing parameter errors in CLI and API invocations. Note: as you reuse and experiment with this template snippet, you will want to use your own KeyPair name.

The second parameter leverages global parameters that can be used from Parameter Store. Since Amazon Machine Images (AMIs) change over time, it’s a good idea to not hardcode these AMI values in your template code. You can query, for example, the latest Windows Server AMI available by following the examples in this blog post. In building the previous template snippet, I’ve used similar commands to discover the latest AMI for Amazon Linux 2. The AWS CLI command I used is:

aws ssm get-parameters-by-path --path "/aws/service/ami-amazon-linux-latest" --region us-west-1

Notice the Type and Default attributes for the LatestAmiId parameter. The Type attribute describes the format required for signaling to CloudFormation that it should retrieve the parameter string value for the EC2 image ID from Parameter Store. The Default attribute leverages a public, global Parameter Store value that you don’t have to maintain because it is provided for all users. The tree hierarchy of the path begins with /aws, signaling that it is a global AWS namespace.

As mentioned before, a third parameter is also used, but it is not declared in the parameters section:

InstanceType: '{{resolve:ssm:ssbEC2iDev:1}}'

This method of retrieving external parameters from a CloudFormation template was recently added as part of the secure string support, and it is referred to as dynamic references. You can read more about this method in its documentation page, but we’ll summarize some key aspects here:

The single quote and double bracket characters signal to CloudFormation that it needs to parse and resolve the enclosed string before executing the template code.

The keyword resolve identifies the string as a dynamic reference, which allows developers to store and maintain values in a service external to CloudFormation. Generically, the pattern is {{resolve:service-name:reference-key}}.

Based on the pattern mentioned earlier, ssm refers here to Systems Manager Parameter Store. Specifically, it indicates that a non-secure string will be retrieved from there.

For the purposes of this snippet example, I created the ssbEC2iDev parameter in my account. As you experiment with the snippet, you’ll have to create your own parameter. I used the following AWS CLI command:

aws ssm put-parameter --name ssbEC2iDev --type String --value "t2.small"

So, as my template gets parsed and executed, the value inserted in the template as the instance type for my EC2 instance will be 't2.small'.

Finally, notice the digit 1 at the end of my dynamic reference pattern. This indicates that I want to retrieve the first version of my parameter. You use these versions explicitly to allow for your template and stack to be deployed for an extended period of time. This ensures that any future stack operation, like a stack update, can be properly rolled back (if necessary) to the appropriate version of the parameter. You can use a future stack update to specify a different version of the parameter, which is a minimal change.

Example 2: Using a Parameter Store secure string parameter

Using secure string parameters is an appropriate way to avoid hard coding a password in your template code. This ensures that sensitive runtime parameters are kept as secure as you keep other secrets, while also keeping them separate from your deployment code.

Review the following snippet:

AWSTemplateFormatVersion: "2010-09-09"
Description: Using existing secure and non-secure SSM Parameters in CloudFormation
#
# This template creates a MariaDB RDS instance using the following:
# A non-secure SSM Parameter for the DB instance class
# A secure SSM Parameter for the master password
#
Resources:
MyRDSDB:
Type: "AWS::RDS::DBInstance"
Properties:
# The following line uses a plain-text Parameter Store dynamic reference
DBInstanceClass: "{{resolve:ssm:ssbRDSiClass:1}}"
AllocatedStorage: '20'
Engine: mariadb
EngineVersion: '10.2'
MasterUsername: appadmin
# The following line uses a secure-string Parameter Store dynamic reference
MasterUserPassword: "{{resolve:ssm-secure:ssbRDSmEcntl:1}}"
Outputs:
DbInstanceId:
Description: InstanceID of My RDS DB
Value: !Ref MyRDSDB

Two parameters are used in this template and, because we use the dynamic references method for both parameters, no parameters section is required in this example.

This template creates an Amazon RDS MariaDB 10.2 database. The first parameter is used for the DBInstanceClass attribute, and it looks a lot like the first example. To create this parameter, I used the following AWS CLI command:

aws ssm put-parameter --name ssbRDSiClass --type String --value "db.t2.medium"

It resolved to the value 'db2.t2.medium'. If necessary, the parameter can be updated in the Systems Manager Parameter Store console in the future, when a more appropriate instance type is identified. Remember that the version number will change when this occurs.

The second parameter uses a different service name in its parameter reference. In our case, rather than using ssm for the non-secure string, we specify ssm-secure to indicate to CloudFormation that the parameter must be decrypted before the resource can be created:

MasterUserPassword: "{{resolve:ssm-secure:ssbRDSmEcntl:1}}"

To create this parameter, I executed this AWS CLI command:

aws ssm put-parameter --name ssbRDSmEcntl --type SecureString --value "ch4ng1ng-s3cr3t"

When I created the parameter, I did not use the word “password” in its name. I didn’t want to draw attention to to the fact that it is a secret, sensitive parameter. Only authorized users should be able to retrieve and decrypt this parameter. Note that I specified its version, just as I did with the instance class parameter.

You should be able to experiment with secure parameters in CloudFormation by using the second snippet as is, after you’ve created your two parameters with the AWS CLI and the parameter store commands listed.

Additional considerations

When using dynamic references to fetch Parameter Store strings, as illustrated in this article, keep in mind some of the following restrictions:

You can’t natively create secure-string values in CloudFormation code, although you can create non-secure string and string list parameters, as illustrated in this documentation page. As it is the case with any resource not natively supported by CloudFormation, you could create an AWS Lambda-backed custom resource. In this case, the Lambda function can generate a random password, potentially create an encryption key with a separate call, and call the native Parameter Store API. In this use case, you could then also call the Lambda function to decrypt the parameter and pass it through to a subsequent resource creation call in cleartext. Remember that when you log your function outputs, don’t reveal the cleartext value, and don’t hard code a cleartext password in your function code.

As mentioned previously, when using the compact dynamic reference pattern to retrieve the parameters, you must specify a version.

For secure strings, CloudFormation currently supports 11 specific resources and attributes. These primarily cover use cases where a password should be specified. The following table lists these supported resources:

Resource | Attribute | Properties
AWS::DirectoryService::MicrosoftAD | | Password
AWS::DirectoryService::SimpleAD | | Password
AWS::ElastiCache::ReplicationGroup | | AuthToken
AWS::IAM::User | LoginProfile | Password
AWS::KinesisFirehose::DeliveryStream | RedshiftDestinationConfiguration | Password
AWS::OpsWorks::App | AppSource | Password
AWS::OpsWorks::Stack | CustomCookbooksSource | Password
AWS::OpsWorks::Stack | RdsDbInstances | DbPassword
AWS::RDS::DBCluster | | MasterUserPassword
AWS::RDS::DBInstance | | MasterUserPassword
AWS::Redshift::Cluster | | MasterUserPassword

Conclusion

By using the secure string support in CloudFormation with dynamic references you can better maintain your infrastructure as code. You’ll be able to avoid hard coding passwords into your templates and you can keep these runtime configuration parameters separated from your code. Moreover, when properly used, secure strings will help keep your development and production code as similar as possible, while continuing to make your infrastructure code suitable for continuous deployment pipelines.

About the Author

| Luis Colon is a Senior Developer Advocate for the AWS CloudFormation team. Luis writes blog posts and represents AWS at conferences and other events. He also advises CloudFormation customers on implementing best practices. You can find him on twitter at @luiscolon1.