DEV Community

David Krohn for AWS Community Builders

Posted on • Originally published at globaldatanet.com

CloudFormation Best Practices

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.

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Prefix:
    Type: String
Resources:
  Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${Prefix}-role
Enter fullscreen mode Exit fullscreen mode

Validate Templates Before Using Them

Validate your templates against the AWS CloudFormation Resource Specification and check your templates for insecure infrastructure - you can use the following tools to do that automatically.

cfn-lint

cfn-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 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:

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"
Enter fullscreen mode Exit fullscreen mode

Automate CloudFormation testing with taskcat

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.

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}$'
Enter fullscreen mode Exit fullscreen mode

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.

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.

🚨 Wrong:
 Arn: 
   Fn::GetAtt: 
     - CostReporterLambda 
     - Arn 
✅ Right: 
Arn: !GetAtt CostReporterLambda.Arn
Enter fullscreen mode Exit fullscreen mode

💡 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:

    UserData:
        Fn::Base64: !Sub |
        #!/bin/bash
Enter fullscreen mode Exit fullscreen mode

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:

      UserData: !Base64
        Fn::If:
        - UseWindows
        - Fn::Join:
          - ''
          - - |
               <powershell>
Enter fullscreen mode Exit fullscreen mode

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

    🚨 Wrong: !Join ['', ['arn:aws:s3:::', !GetAtt [Example, ParameterValue], /*]]
    ✅ Right: !Sub arn:aws:s3:::${Example.ParameterValue}/*
Enter fullscreen mode Exit fullscreen mode

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:

    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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)