Custom CloudFormation Resource for looking up config data
This project, CloudFormation Lookup
enables you to build CloudFormation templates that pull configuration data from DynamoDB as a dictionary. Consider the scenario where you are using CodeBuild to deploy apps into a series of AWS accounts you control. Each of those accounts may have differing configuration data, depending on the intent of the account. For instance, perhaps you deploy an application across segregated tenants for customers? Each of those tenets may have different configurations, like DNS host names.
This project can be found here on GitHub
As of right now with CloudFormation, there is no means to pull that data, on a per account basis. To solve this problem, we have developed a custom CloudFormation resource, that enables you to define resource, as below in your CloudFormation template:
# Looks up properties for automated deployments that we store within DynamoDB as configuration.
# The properties are looked up by key. The return values could be strings, string lists,
# numbers, or maps. Maps enable you to put a bunch of values in the resulting data structure.
PropertiesLookupTest:
Type: Custom::PropertiesLookup
Version: '1.0'
Properties:
# Identified the Lambda function which we will invoke for this custom resource
ServiceToken: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:dsop-tools-lookup"
# This is the lookup value that we will attempt to find from
# storage. We key this value on the name of the application PLUS
# the account identifier. We may want to deploy everything with the same
# configuration—or potentially have different tenets which have account
# specific configurations.
Value: !Sub "/website/${AWS::AccountId}/${AWS::Region}"
# This looks for the default value that maybe stored in the key.
# This is useful for auto deployed StackSet Instances in AWS.
DefaultLookupValue: "/website/default"
# This default value, in JSON format if the value or default
# lookup are not able to be discerned
DefaultValueAsJson: "{ \"domainName\": \"smoke-test.monkton.io\" }"
Once this has been defined in the Resources
block of your CloudFormation template, you can reference the data anywhere in template. For instance, we may want to pass the domainName
into a ACM
certificate:
SampleCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !GetAtt PropertiesLookupTest.domainName
ValidationMethod: DNS
Here, we first use the PropertiesLookupTest
to lookup values based on the Value
key. This is a key within the DynamoDB table that maps to the value you want. There are fallback conditions as well. If the value cannot be found, it will look to the DefaultLookupValue
value. If that isn't found, it will fallback to the DefaultValueAsJson
value.
This design enables you to create new environments that would have default values configured—but ones that could be customized on a per-environment basis.
To do
The last remaining task is to drop this Lambda function into a VPC.
[_] Deploy into VPC
What gets deployed
We have a series of objects that will be created. In the account that this project is deployed into, we create the following:
DynamoDB table to hold the configuration data
KMS Key to protect the DynamoDB data
IAM Role named
master-dynamodb-account-role
that functions in the child accounts can assumeThis has permissions to access the DynamoDB
GetItem
as well as the KMSDecrypt
actionsStackSet that deploys the StackSet Instances into the desired target
In each target account, we deploy:
The function to retrieve the data
IAM Role for the function to use to execute
Why do we have to deploy a function to every account? you intelligently ask. Because a single root Lambda is a total pain to try to work with. Deploying a Lambda into every account that pulls the data enables you to easily invoke it from CloudFormation. Simple as that. This design and assumption of roles enables you to pull data from a single root DynamoDB table with configuration data.
DynamoDB Data
There are limitations as of now. We limit the type of data to be returned to simple strings, string sets, or simple maps. String sets are joined into comma delimited strings (thus can be split in CloudFormation templates). Maps should only be a single level and contain only Strings and StringSets.
The database should have these keys:
settingKey
: this is the hash key in DynamoDB and must be set, this is whatValue
andDefaultLookupValue
provide lookups for.storedValue
: this is the String, StringSet, or Map that has the values to be returned within it.
AWS Organizations Account Makeup
Account Breakdown
This project deployment runs on the idea of account separation using AWS Organizations. For instance, part of our account management and breakout is the following accounts:
DSOP Account
N+1 Test Accounts
N+1 Production Accounts
We then break those projects into the following OUs:
DSOP OU
for our single DSOP accountTest OU
for our one to many test accountsProduction OU
for our one to many production accounts
Under our Production OU
we also separate solutions into their own OUs. For instance, Monkton's Website would be under the Monkton Website OU
, our Orchestrator app backend would be in the Orchestrator OU
.
The structure looks like:
Root
DSOP OU
DSOP Account
Test OU
Test Account 1
Test Account 2
Production OU
Monkton Website OU
Monkton Website App
Orchestrator OU
Shared Tenet App
USAF Tenet App
USSF Tenet App
Thus, when we apply this template to the Production OU
, every account underneath it is now allowed to pull configuration data.
When we use CodePipeline to update the Compute IaC for Orchestrator, we will apply it to the Orchestrator OU
which will either create or update the Compute IaC StackSets for all the accounts in that OU.
Deployment
To deploy this stack, we are going to use your DSOP Account
as the Delegated Admin for deploying the CloudFormation Lookup
StackSet. (You will need to delegate this account as a Delegated Admin via the root account CloudFormation settings). You will also need to do this if you are using your DevSecOps account and CodePipeline.
Next, deploy the dsop-parameter-lookup-stackset.yaml
template, providing the following parameters:
parProjectDeploymentOU
: a path that leads to the production OU for your organization. It will follow this format: org/root/ouparRegionDistribution
: a comma separated list of regions this could be deployed to. For instancesus-east-1,us-west-1
The org OU structure can be determined by navigating to your Root account and your AWS Organizations configuration. Tap on AWS Accounts
and view the Organization structure. The path should look something like o-XXXXXXXXXX/r-XXXX/ou-XXXX-XXXXXXXX
/
Test StackSet
Our test StackSet relies on the same parameters and configuration. This enables you to configure a key in DynamoDB and it sends it to Output
for the deployed StackSet Instances.