CloudFormation Best Practices
Tags: aws, devops
Published: 2021-01-05 13:15:46

There are plenty of tools to provision AWS resources, I prefer to use CloudFormation for my daily work. Here are my recommendations that can help you to use CloudFormation more effectively and securely throughout its entire workflow.

### Do not hardcode names in your templates

When using Continuous Integration and Continuous Deployment patterns for your service, you might want to spin up your CloudFormation Stack more than once in a region or account. Sooner or later you might end up with resources using the same name (eg. IAM Roles, etc.) since these resources are global. 

❗️ Try to avoid using names in general and use tags or add a prefix to all your resources.

```yaml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Prefix:
    Type: String
Resources:
  Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${Prefix}-role
```

### Validate Templates Before Using Them
Validate your templates against the [AWS CloudFormation Resource Specification](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) and check your templates for insecure infrastructure - you can use the following tools to do that automatically. 

#### cfn-lint 
[cfn-lint](https://github.com/aws-cloudformation/cfn-python-lint) is an open-source command-line tool that validates CloudFormation YAML/JSON templates against the AWS CloudFormation Resource Specification and additional checks. It includes checking valid values for resource properties and best practices.

#### cfn_nag
[cfn_nag](https://github.com/stelligent/cfn_nag) is an open source command-line tool that performs static analysis of CloudFormation templates. It will search for insecure infrastructure like: 
 - IAM rules that are too permissive (wildcards)
 - Security group rules that are too permissive (wildcards)
 - Access logs that aren't enabled
 - Encryption that isn't enabled
 - Password literals

⚠️ If you want to suspend certain findings you can do that by adjusting the metadata of the resource of the template eg: 

```yaml
AWSTemplateFormatVersion: 2010-09-09
Resources:
  MyRule:
    Type: 'AWS::IAM::Role'
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F3
            reason: "Complies with own Coding Guidelines"
          - id: W11
            reason: "Complies with own Coding Guidelines"
          - id: W28
            reason: "Should never be replaced"
```

</br>

### Automate CloudFormation testing with taskcat
[taskcat](https://aws-quickstart.github.io/taskcat/) is a tool that tests AWS CloudFormation templates. It deploys your CloudFormation template in multiple AWS Regions and generates a report with a pass/fail grade for each region.

### Use AWS-Specific Parameter Types or use Allowed Patterns / Values
To avoid false statements and the following errors in deployments use AWS-Specific Parameter Types for existing resources (such as existing Virtual Private Cloud IDs or an EC2 key-pair name), for not existing resources define allowed patterns or allowed values for the parameter.

```yaml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  BackupFrequency:
    Description: Frequency of the Backup
    ConstraintDescription: Must be a valid selection
    Type: String
    Default: Daily
    AllowedValues:
      - Daily
      - Weekly
      - None
  AccountId:
    Type: String
    Description: >-
      The account number.
    ConstraintDescription: Must be a valid Account number
    Default: '123456789101'
    MinLength: 12
    MaxLength: 12
    AllowedPattern: '^\d{12}$'
```

</br>
</br>

### Split up Stacks
Group resources by their technical context, not the AWS service name that will help you keep the overview of your template(s) and it will help you to make changes to a particular set of resources by using their own process and schedule without affecting other resources.

#### Nested Stacks
Use one Master stack and trigger all other stacks as nested stacks. That way you can mix and match different templates but use nested stacks to create a single, unified stack.

</br>
</br>

### Short-form Syntax
Try to use the short-form syntax for all intrinsic functions - there are just a few cases where it's not possible to use short-form syntax. It is way much easier to read the short-form syntax instead of reading the syntax for the full function name. 

```yaml
🚨 Wrong:
 Arn: 
   Fn::GetAtt: 
     - CostReporterLambda 
     - Arn 
✅ Right: 
Arn: !GetAtt CostReporterLambda.Arn
```

</br>

💡 If you use the short form and immediately include another function in the valueToEncode parameter, use the full function name for at least one of the functions eg:

```yaml
    UserData:
        Fn::Base64: !Sub |
        #!/bin/bash
```

</br>

If you have more than two intrinsic functions successively you can do something like that (😁 yes I know that is not the normal case but someday it might be your case) eg:

```yaml
      UserData: !Base64
        Fn::If:
        - UseWindows
        - Fn::Join:
          - ''
          - - |
               <powershell>
```

</br>

For readability of code, it is better to use **!GetAtt** in short notation within **!Sub**.

```yaml
    🚨 Wrong: !Join ['', ['arn:aws:s3:::', !GetAtt [Example, ParameterValue], /*]]
    ✅ Right: !Sub arn:aws:s3:::${Example.ParameterValue}/*
```
</br>

### Always pay attention to the tab stops
If you need to deal with several interconnected `Ifs` pay attention to the tab stops, otherwise your template will not be invoked.
In the following example we have four interconnected if conditions:

```yaml
    SubnetIds:
    !If
    - UseAZ1
    - - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetAID
    - !If
        - UseAZ2
        - - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetAID
          - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetBID
        - !If
        - UseAZ3
        - - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetAID
          - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetBID
          - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetCID
        - !If
            - UseAZ4
            - - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetAID
              - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetBID
              - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetCID
              - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnetDID
            - !Ref AWS::NoValue
```


### Parameters vs. Parameter Store
If you need to use settings/parameters which you need more than once, I for example, always store them in the AWS Parameter Store and query them in my other templates via the `AWS::SSM::Parameter::Value<String>`
Parametertype in other templates. For Parameters which I just need to use once, I just handover them via input.