AWS CloudFormation Guard is an open-source general-purpose policy-as-code evaluation tool. The Guard command line interface (CLI) provides developers with a simple-to-use, yet powerful and expressive domain-specific language (DSL) that you can use to express policy as code. In addition, you can use CLI commands to validate structured hierarchical JSON or YAML data against those rules. Guard also provides a built-in unit testing framework to verify that your rules work as intended.
Guard doesn’t validate CloudFormation templates for valid syntax or allowed property values. You can use the cfn-lint tool to perform a thorough inspection of template structure.
For full details on the options of CloudFormation Guard and example use cases outside of tagging, see the documentation AWS CloudFormation Guard
Open the AWS Cloud9/VSCode Server terminal, and run the following commands.
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh
export PATH=~/.guard/bin:$PATH
Check if the install has properly completed
cfn-guard --version
Create our folder and files for the Guard lab.
cd ~/environment/
mkdir cfnguard
cd cfnguard
touch example_bucket.yaml example_guard.guard
ls -al
You have now created the files required to validate your template based on rules. In the following instructions, you will create an S3 bucket and test that the required tags have been applied.
In the navigation pane, choose and expand the cfnguard folder and then open the file example_bucket.yaml and add a bucket resource as per the below. Ensure to save after pasting.
Resources:
S3Bucket:
Type: "AWS::S3::Bucket"
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
Tags:
- Key: "environment"
Value: "dev"
- Key: "service"
Value: "myservice"
Create the Guard rule. Open the file example_guard.guard and add rules as per the below. Ensure to save after pasting.
let my_buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]
rule validate_env_tag when %my_buckets !empty {
%my_buckets.Properties {
some Tags[*].Key == 'environment'
some Tags[*].Value in ['dev', 'test', 'prod']
<<Tag key 'environment' not set correctly. Check both the Key & Value>>
}
}
rule validate_service_tag when %my_buckets !empty {
%my_buckets.Properties {
some Tags[*].Key == 'service'
<<Tag key 'service' not set>>
}
}
Lets take a quick look over the rules. The some keyword tells Guard to ensure that one or more values from the resultant query match the check and you can use a list to check that allowed values are present also. Where you are free to allow teams to set their own values, you can still check directly that a required key exists. In the rule validate_env_tag you are running a statement that requires both the key to be a fixed value, and the value of that key to be fixed to a choice of strings held within the list.
Run the following command in the terminal:
cfn-guard validate -S all -d example_bucket.yaml -r example_guard.guard
You should see the following return:
example_bucket.yaml Status = PASS
PASS rules
example_guard.guard/validate_env_tag PASS
example_guard.guard/validate_service_tag PASS
---
Let’s make some changes to our template to see what happens when there is a failure. In your example_buckeyt.yaml you are going to set the environment tag value to have used the incorrect case for the environment, see below. After making the change, save the file.
Value: "Dev"
Re-run the command in the terminal.
cfn-guard validate -S all -d example_bucket.yaml -r example_guard.guard
You should see the following return:
example_bucket.yaml Status = FAIL
PASS rules
example_guard.guard/validate_service_tag PASS
FAILED rules
example_guard.guard/validate_env_tag FAIL
---
Evaluating data example_bucket.yaml against rules example_guard.guard
Number of non-compliant resources 1
Resource = S3Bucket {
Type = AWS::S3::Bucket
Rule = validate_env_tag {
ALL {
Check = Tags[*].Value IN ["dev","test","prod"] {
ComparisonError {
Message = Tag key 'environment' not set correctly. Check both the Key & Value
Error = Check was not compliant as property [/Resources/S3Bucket/Properties/Tags/0/Value[L:11,C:16]] was not present in [(resolved, Path=[L:0,C:0] Value=["dev","test","prod"])]
}
PropertyPath = /Resources/S3Bucket/Properties/Tags/0/Value[L:11,C:16]
Operator = IN
Value = "Dev"
ComparedWith = [["dev","test","prod"]]
Code:
9. SSEAlgorithm: AES256
10. Tags:
11. - Key: "environment"
12. Value: "Dev"
13. - Key: "service"
14. Value: "myservice"
}
Check = Tags[*].Value IN ["dev","test","prod"] {
ComparisonError {
Message = Tag key 'environment' not set correctly. Check both the Key & Value
Error = Check was not compliant as property [/Resources/S3Bucket/Properties/Tags/1/Value[L:13,C:16]] was not present in [(resolved, Path=[L:0,C:0] Value=["dev","test","prod"])]
}
PropertyPath = /Resources/S3Bucket/Properties/Tags/1/Value[L:13,C:16]
Operator = IN
Value = "myservice"
ComparedWith = [["dev","test","prod"]]
Code:
11. - Key: "environment"
12. Value: "Dev"
13. - Key: "service"
14. Value: "myservice"
}
}
}
}
You can now see that you have a flexible set of rules that can be applied to tags for our resources in a modular fashion.
Guard gives you the ability to write tests for your rules, to validate that your rules work as you expect.
Create a test file for the rules you created earlier by running the following command in the AWS Cloud9/VSCode Server terminal.
cd ~/environment/cfnguard
touch example_bucket_tests.yaml
In the navigation pane, choose and expand the cfnguard folder and then open the file example_bucket_tests.yaml. Append the following content that contains tests for one of the named rules you used earlier. Ensure to save after pasting.
- input:
Resources:
MyExampleBucket:
Type: AWS::S3::Bucket
Properties:
Tags:
- Key: "environment"
Value: "dev"
expectations:
rules:
validate_env_tag: PASS
- input:
Resources:
MyExampleBucket:
Type: AWS::S3::Bucket
Properties:
Tags:
- Key: "environment"
Value: "Prod"
expectations:
rules:
validate_env_tag: FAIL
Run the following command in the terminal to initiate the test for our Guard rule:
cfn-guard test -t example_bucket_tests.yaml -r example_guard.guard
You should see the following return:
Test Case #1
No Test expectation was set for Rule validate_service_tag
PASS Rules:
validate_env_tag: Expected = PASS
Test Case #2
No Test expectation was set for Rule validate_service_tag
PASS Rules:
validate_env_tag: Expected = FAIL
If you set the test to show something other than the expectation, you will see an expected vs evaluated result in the test, see example here:
Test Case #1
No Test expectation was set for Rule validate_service_tag
FAIL Rules:
validate_env_tag: Expected = FAIL, Evaluated = [PASS]
Test Case #2
No Test expectation was set for Rule validate_service_tag
PASS Rules:
validate_env_tag: Expected = FAIL
In the file example_bucket_tests.yaml, add the following at the end of the file. This adds a final test for the validate_service_tag rule. Ensure to save after pasting.
- input:
Resources:
MyExampleBucket:
Type: AWS::S3::Bucket
Properties:
Tags:
- Key: "service"
expectations:
rules:
validate_service_tag: PASS
Run the following command in the terminal to include the test added in the previous step.
cfn-guard test -t example_bucket_tests.yaml -r example_guard.guard
You should see that our validate_service_tag rule also has a test associated with it.
Test Case #1
No Test expectation was set for Rule validate_service_tag
PASS Rules:
validate_env_tag: Expected = PASS
Test Case #2
No Test expectation was set for Rule validate_service_tag
PASS Rules:
validate_env_tag: Expected = FAIL
Test Case #3
No Test expectation was set for Rule validate_env_tag
PASS Rules:
validate_service_tag: Expected = PASS
AWS CloudFormation Guard allows you to write Policy-as-Code to help you validate your cloud environments. Here you have seen how to write guards specificaly for tagging and also how to write tests for those guards.