Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

aws cloudformation best practices

authoring rules the pre-deploy validators do not catch, distilled from the field

iac scope

a running collection of cfn authoring rules you only learn by shipping. cfn-lint passes, aws cloudformation validate-template passes, bash -n passes — the failure surfaces the first time cfn submits the create call to the underlying service api, which means the whole stack rolls back and you have to delete-stack + wait for DELETE_COMPLETE before you can redeploy.

every rule below was caught during an actual deploy. each one includes the reason the validator misses it, the exact error you see, and the fix.

rule 1 — ec2 security group descriptions must be pure ascii

any non-ascii character in AWS::EC2::SecurityGroup GroupDescription or rule Description fields causes the stack to fail with:

Character sets beyond ASCII are not supported

that means no em-dash , no curly quotes "/', no non-breaking space, no accented letters, no emoji. the ec2 api hard-rejects these on the create call. cfn does not validate them, cfn-lint does not validate them, and aws cloudformation validate-template does not validate them. the first signal is a ROLLBACK_COMPLETE after the round trip.

this rule applies to ec2 security groups specifically. most other aws resources accept utf-8 freely in description fields.

fix

use plain ascii hyphens - everywhere in security group descriptions. grep every template before committing:

# flag any non-ascii byte in any resource's Description field
grep -Pn '[^\x00-\x7f]' cfn/**/*.yaml

example

wrong:

BastionSecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: wireguard bastion — udp/51820 inbound only

right:

BastionSecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: wireguard bastion - udp/51820 inbound only

rule 2 — sso permission set inline policies must parameterize the workload region

iam identity center (sso) permission sets deploy into the region that hosts the sso instance. that is almost never the region where the workloads you are granting access to actually live.

if the inline policy uses ${AWS::Region} as the region in any resource arn or in a kms:ViaService condition, it resolves to the stack's own region — the sso-instance region — not the region of the resources you intended to grant access to. users sign in, try to read or write the target resource, and get AccessDeniedException. the permission set looks correct in the console; the arns just point at the wrong region.

fix

add an explicit TargetRegion parameter (or a more specific name like TargetSsmRegion) and substitute it in the policy body. never let ${AWS::Region} appear inside the inline policy of an AWS::SSO::PermissionSet.

example

wrong:

Parameters:
  InstanceArn:
    Type: String

Resources:
  CiDebugPermissionSet:
    Type: AWS::SSO::PermissionSet
    Properties:
      Name: ci-debug
      InstanceArn: !Ref InstanceArn
      InlinePolicy: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [{
            "Effect": "Allow",
            "Action": ["ssm:PutParameter", "ssm:GetParameter"],
            "Resource": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/ci/*"
          }]
        }

right:

Parameters:
  InstanceArn:
    Type: String
  TargetSsmRegion:
    Type: String
    Default: us-west-2
    Description: region where the /ci/* parameters live (not the sso instance region)

Resources:
  CiDebugPermissionSet:
    Type: AWS::SSO::PermissionSet
    Properties:
      Name: ci-debug
      InstanceArn: !Ref InstanceArn
      InlinePolicy: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [{
            "Effect": "Allow",
            "Action": ["ssm:PutParameter", "ssm:GetParameter"],
            "Resource": "arn:${AWS::Partition}:ssm:${TargetSsmRegion}:${AWS::AccountId}:parameter/ci/*"
          }]
        }

generalization

any stack that deploys in region A but grants access to resources in region B must use a parameter for region B, never ${AWS::Region}. this rule bites hardest with iam identity center (sso instances are region-pinned) but it also applies any time a permission set, cross-region role, or kms grant crosses a region boundary.

rule 3 — aws ssm put-parameter --overwrite and --tags are mutually exclusive

this one is not a cloudformation authoring rule — it hits in the post-deploy script that writes secret values into the parameter store that cfn defined. included here because the failure is silent and the debugging cost is high.

aws ssm put-parameter rejects calls that combine --overwrite with --tags. the cli returns exit 254 with the error on stderr:

An error occurred (ValidationException) when calling the PutParameter operation: Tags cannot be specified when overwriting parameters. Tags can be added or modified only through the AddTagsToResource API.

a shell wrapper that pipes the call through 2>&1 | head -1 && echo OK under set -euo pipefail will mask this entirely — pipefail catches the failure, the short-circuited && produces no output, and the migration loop exits silently with no hint of which parameter broke.

fix

new parameters do not need --overwriteput-parameter creates them on first call. drop --overwrite from the initial-write path and keep --tags for audit attribution. for an existing tagged parameter that needs updating, split into two calls:

aws ssm put-parameter --name /ci/example --value "$val" --type SecureString --overwrite
aws ssm add-tags-to-resource --resource-type Parameter --resource-id /ci/example \
  --tags Key=project,Value=ci Key=env,Value=prod

never wrap put-parameter with 2>&1 | head -1 && ... under pipefail. redirect stderr to a file, or drop pipefail on that one line.

pre-deploy checklist

the single most useful habit: run these before every deploy.

# rule 1 — flag non-ascii in any cfn template
grep -Pn '[^\x00-\x7f]' cfn/**/*.yaml

# rule 2 — flag any ${AWS::Region} inside an inline policy body of a permission set
grep -Pzon '(?s)AWS::SSO::PermissionSet.*?InlinePolicy.*?\$\{AWS::Region\}' cfn/**/*.yaml

# rule 3 — flag any put-parameter script that passes both --overwrite and --tags
grep -Pn 'ssm put-parameter.*(--overwrite.*--tags|--tags.*--overwrite)' scripts/**/*.sh

cfn-lint is still the right first pass — it catches schema errors, missing !Ref targets, and a lot else. these rules are the things it does not catch, not a replacement for running it.

why this repo exists

these rules came out of a real sprint in 2026-04 that shipped seven cloudformation stacks to a solo-operator aws account. every rule cost a deploy round trip and a delete-stack wait — not the end of the world on a small stack, very painful on anything nested or iam-heavy.

pre-deploy validation catches what the cfn schema knows about. the service apis validate a much larger surface area than cfn's own schema, and they do it only at create time. the gap between those two surfaces is where these rules live.

About

authoring rules the cfn pre-deploy validators do not catch, distilled from the field

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors