Currently, CloudFormation doesn’t have support for the Parameter Store Secure Strings, which is unfortunate. This is just a matter of time though, as AWS will probably announce support at some point in the future.

Fortunately there is a “nice” workaround, called Custom Resources. This works by creating a Lambda function, which creates whatever you want to create.
This opens a whole can of worms though, you end up writing a lambda function, uploading it to s3, calling it from within CloudFormation, etc.
And by doing so, now you have to maintain the file in S3, take care of the packaging, versioning and deploy process, while you JUST WANT THE D*MN THING TO WORK.

There is a somewhat nice solution for this though. Using a special notation, you can actually embed the source code for a Lambda function within your template.

Using the ZipFile type for the Code parameter, you can write (messy) inline functions. There is a limit (4000 bytes), but that’s more than enough to run something decent. Here’s an example:

And because boto3 and requests are available by default in the Python runtime, you don’t actually have to do any packaging, yay!

Let’s take this a step further.

You can actually write a lambda function which calls a boto3 function using the parameters from the custom resources and output the boto3 response as JSON. Which you can then turn into attributes for the resource, which can then be used using the !GetAtt function for other resources.

Wait wut?

A Custom Resource is just like any other resource, with parameters and all. Whenever you create a Custom Resource, during the Create (and Update) state of the Custom Resource you are obligated to respond to CloudFormation with a specific response. In the call that you’re sending to CloudFormation, you give it a JSON response with the actual status of the Custom Resource (Failed, Created, Deleted, Updated).

This JSON response can also have a list of arbitrary key/values, which will be exposed as attributes for the Custom Resource inside your CloudFormation template. These attributes can be used as any other attribute in CloudFormation, using the !GetAtt function.

This is the python function which does all this magic for you:

importboto3importjsonimportastfrombotocore.vendoredimportrequestsdefflatten(d,parent_key=''):items=[]fork,vind.items():try:items.extend(flatten(v,'%s%s.'%(parent_key,k)).items())exceptAttributeError:items.append(('%s%s'%(parent_key,k),v))returndict(items)defhandler(event,context):props=event['ResourceProperties']data={}client=boto3.client(props['Service'])event_type=event['RequestType']# we default to success if no event_type is definedstatus='SUCCESS'reason=''data={}# Check if the current event type is configured, if so, run itifevent_typein['Create','Delete','Update']andevent_typeinprops:method=getattr(client,props[event_type]['Method'])kwargs={}# We get text values from CloudFormation properties, this won't# work for booleans and numbers (boto is pretty anal about this).# so we convert them using the AST library to proper typesforkey,valueinprops[event_type]['KwArgs'].items():try:kwargs[key]=ast.literal_eval(value)exceptExceptionase:print(str(e))kwargs[key]=value# Try to call the actual method in bototry:data=method(**kwargs)status='SUCCESS'reason=''exceptExceptionase:status='FAILED'reason=str(e)data={}response_data={'Status':status,'Reason':reason,'PhysicalResourceId':event['LogicalResourceId'],'StackId':event['StackId'],'RequestId':event['RequestId'],'LogicalResourceId':event['LogicalResourceId'],# We need to flatten the response data object, so we can also access# nested objects and values, because in CF only the first level# of properties will become available as attributes.# So {"foo": {"bar": "baz"}} becomes {"foo.bar": "baz"}, and can be# used as !GetAtt myresource.foo.bar, as expected'Data':flatten(data)}# print the callback for debuggingprint(response_data)resp=requests.put(event['ResponseURL'],data=json.dumps(response_data))ifresp.status_code!=200:print(resp.text)raiseException(resp.text)return

MyCustomResource:Type:Custom::FoobarProperties:# service name for client, check the boto documentation for the right name# to use: http://boto3.readthedocs.io/en/latest/reference/services/index.htmlService:S3# There are 3 different "main" properties:# - Create (when creating the resource)# - Update (when updating the resource)# - Delete (when deleting the resource)Create:# the method use on the boto clientMethod:put_object# the arguments which need to be passed to the method# see the boto documentation, eg:# http://boto3.readthedocs.io/en/latest/reference/services/s3.html#S3.Client.put_objectKwArgs:Body:"foobar"Bucket:"my-bucket"Key:'my-file.txt'# This is the Delete command, if needed, optional# Delete:# Method: delete_object# KwArgs:# Bucket: !Ref MyS3Bucket# Key: 'myPasswordFile.txt'# This is the Update command, if needed, optional# Update:# Method: put_object# KwArgs:# Body: "foobar"# Bucket: "my-bucket"# Key: 'my-file.txt'ServiceToken:"Fn::GetAtt":-BotoCustomResource-Arn

Full example

So how this does work? Well, let’s do this: as an example, let’s create a CloudFormation template, which:

Creates an s3 Bucket

Fetches a SECRET parameter value from the parameter store

Writes the secret to a file on the newly created S3 bucket

Of course, using the Secret in this way is discouraged, but this shows you that you can create the custom resource function once, and re-use it multiple times in the same template.

important
Be sure to give the custom resource role the correct permissions to call the actual boto function, or things will fail in CloudFormation. In the template below, notice the lines within the LambdaExecutionRole resource:

AWSTemplateFormatVersion:'2010-09-09'Description:FoobarResources:# create an S3 bucketMyS3Bucket:Type:"AWS::S3::Bucket"# create a file in S3, which contains the secret from MySecretPasswordMyS3File:Type:Custom::S3FileProperties:# tell boto to create an s3 clientService:s3Create:# write the object when the resource is createdMethod:put_objectKwArgs:# as body, use the attribute of the secret parameterBody:!GetAttMySecretPassword.ValueBucket:!RefMyS3BucketKey:'myPasswordFile.txt'# when deleting the resource, also cleanup the file from the bucketDelete:Method:delete_objectKwArgs:Bucket:!RefMyS3BucketKey:'myPasswordFile.txt'ServiceToken:"Fn::GetAtt":-BotoCustomResource-Arn# Get a secret parameter from the parameter storeMySecretPassword:Type:Custom::SecretPasswordProperties:Service:ssmCreate:Method:get_parameterKwArgs:Name:'/my/secret/password'WithDecryption:'True'ServiceToken:"Fn::GetAtt":-BotoCustomResource-Arn# Create the custom resource function which contains the calls to botoBotoCustomResource:Type:AWS::Lambda::FunctionProperties:Handler:"index.handler"Runtime:python3.6Timeout:30Role:!GetAttLambdaExecutionRole.ArnCode:ZipFile:"Fn::Join":-"\n"--"importboto3"-"importjson"-"importast"-"frombotocore.vendoredimportrequests"-""-"defflatten(d,parent_key=''):"-"items=[]"-"fork,vind.items():"-"try:"-"items.extend(flatten(v,'%s%s.'%(parent_key,k)).items())"-"exceptAttributeError:"-"items.append(('%s%s'%(parent_key,k),v))"-"returndict(items)"-""-"defhandler(event,context):"-"props=event['ResourceProperties']"-"data={}"-"client=boto3.client(props['Service'])"-"event_type=event['RequestType']"-"status='SUCCESS'"-"reason=''"-"data={}"-""-"ifevent_typein['Create','Delete','Update']andevent_typeinprops:"-"method=getattr(client,props[event_type]['Method'])"-"kwargs={}"-"forkey,valueinprops[event_type]['KwArgs'].items():"-"try:"-"kwargs[key]=ast.literal_eval(value)"-"exceptExceptionase:"-"print(str(e))"-"kwargs[key]=value"-"try:"-"data=method(**kwargs)"-"status='SUCCESS'"-"reason=''"-"exceptExceptionase:"-"status='FAILED'"-"reason=str(e)"-"data={}"-""-"response_data={"-"'Status':status,"-"'Reason':reason,"-"'PhysicalResourceId':event['LogicalResourceId'],"-"'StackId':event['StackId'],"-"'RequestId':event['RequestId'],"-"'LogicalResourceId':event['LogicalResourceId'],"-"'Data':flatten(data)"-"}"-"print(response_data)"-"resp=requests.put(event['ResponseURL'],data=json.dumps(response_data))"-"ifresp.status_code!=200:"-"print(resp.text)"-"raiseException(resp.text)"-"return"# execution role for the custom resource function, needs S3 and SSM accessLambdaExecutionRole:Type:AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Version:'2012-10-17'Statement:-Effect:AllowPrincipal:Service:-lambda.amazonaws.comAction:-sts:AssumeRolePath:"/"Policies:-PolicyName:rootPolicyDocument:Version:'2012-10-17'Statement:-Effect:AllowAction:-logs:CreateLogGroup-logs:CreateLogStream-logs:PutLogEventsResource:arn:aws:logs:*:*:*-Effect:AllowAction:-s3:PutObject-s3:DeleteObject-ssm:GetParameterResource:"*"

Using the Custom Resource attributes

You can get the values(/attributes) of the response, eg: !GetAtt MyS3File.ETag will give you the ETag for the created file. Which you could then use as a value for whatever you want. For example in outputs:

Outputs:S3FileETag:Description:MyFileEtaValue:!GetAttMyS3File.ETag

Conclusion

Using custom resources is always a hassle, but at least with the Custom Boto Resource, you can make things a lot easier to maintain, and make it more generic for re-usability. Also, because we can put the function inline the cloudformation script, we only have one place to maintain.