I Spent the Day Saving Five Minutes

I Spent the Day Saving Five Minutes

I did not start the day intending to spend a whole day trying to simplify my life, only to make a small quality of life improvement. As a single-person company, I realize my time is valuable, and I have been trying to organize my work tasks for the week (Another topic for another day). One of this week’s tasks was to reduce some pain by making my AWS CloudFormation deployments a bit easier.

As a bit of backstory, I have been a Terraform guy for almost 8 years, and it was a journey to get up to speed. Since I am coming from Terraform, I like the plan logic and how it shows you the changes quickly and in an easily readable format. Then I can run a Terraform apply, and off to the races it will go, applying all of that wonderful Terraform code that totally will not break anything.

Now that I am re-learning the AWS ecosystem, CloudFormation is where I want to be for now, which means learning a whole lot of new stuff. In particular, actually getting CloudFormation to deploy something for me, and preferably from my command line.

The Goals

Before starting the journey for the day, I had several AWS CLI commands that I had to run with a lot of copy-pasting:

#### Push Changes
aws cloudformation create-change-set

#### Review Changes
aws cloudformation describe-change-set

#### Apply the Changes
aws cloudformation execute-change-set

### Check Stack Status
aws cloudformation describe-stacks

Now, there are probably other simple commands that would have made this maybe better, but I have a few stacks to manage, and this wasn’t scaling well, and when I started, I had a few requirements

  • Find all my stacks automatically
  • Plan a release and see the change, along with an easy change-set naming convention, I didn’t have to keep messing with
  • Apply those changes and automatically find the change-set name
  • The deploy option, which would do the plan and apply it with a simple command
  • Look up why a plan or application failed

Building the Flux Capacitor

As Marty McFly says in Back to the Future:
If you put your mind to it, you can accomplish anything

With the goals in mind, I went off to figure out the solution. At first, being old school, I went into creating bash scripts because that is what I am familiar with. But with a bit more digging, I learning about creating task lists and using a tool called task . If anyone has used Ansible , the flow and logic feel very similar. However, instead of an overly complicated bash script, I can describe my tasks and their list of actions.

Once I was running a task, I managed to get my plan running, which was a nice win, and I was still using bash to kick off the task, so I didn’t have to remember all of the flags that the task needed, and I had something like this:

tasks:
  plan:
    desc: Create and review the changes.
    cmds:
      - echo "Running plan for {{.STACK_NAME}}"
      - aws cloudformation create-change-set --stack-name {{.STACK_NAME}} --template-body file://{{.TEMPLATE}} --change-set-name {{.CS_NAME}} --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM --profile {{.PROFILE}} --region {{.REGION}} > /dev/null
      - aws cloudformation wait change-set-create-complete --stack-name {{.STACK_NAME}} --change-set-name {{.CS_NAME}} --profile {{.PROFILE}} --region {{.REGION}}
      - aws cloudformation get-template --stack-name {{.STACK_NAME}} --query 'TemplateBody' --output text --profile {{.PROFILE}} --region {{.REGION}} > current_deployed.yaml
      - dyff between current_deployed.yaml {{.TEMPLATE}}
    silent: false

Now, if you are looking closely at the code snippet, you’ll see I have another app called dyff . Getting the AWS CLI to actually show the changes in a coherent way seemed problematic. I did try a tool called Rain and AWS CDK to show the changes more clearly, but I personally found dyff to give me just the right amount of information and made the changes easier to see. Why dyff won for me was that it created a clear view of exactly what would change, in an almost Terraform-like way. It cut out all of the fluff I didn’t need. For example, to test the script, I was just playing around with a test tag in my CFN template, and both Rain and AWS CDK were giving me so much extra fluff I didn’t need. I essentially ended up with the poor man’s Terraform plan.

Adapting to the New World

After several more hours, I managed to get my required tasks and included ones I didn’t realize I needed till I was writing this beast:

  • plan
  • apply
  • deploy (the combo plan/apply along with a 15-second wait before apply)
  • clean (This was added to remove all of those failed or multiple successful plans I was running)
  • list (Needed to always get an updated list of stacks in place)
  • errors (For those plan/apply failure, quick debugging)
  • default (this was added so that I could quickly see what options I had in my task list)

Now, I kept developing the bash script to handle some other scenarios, like if I forget to capitalize my stack names, etc., essentially nice-to-haves because, well, I can be lazy and forgetful. However, it was at this point that I tried to get cute and add all the case changes to the task list and dump bash altogether.

Most likely, there is a solution, but I found the task’s logic difficult to work with. At times, I needed some CLI defaults in place so I could get lazy when I wanted to run something, but in the end, I compromised. I created an alias function, so now all I had to run was something like:

cfn-tasks deploy <stack_name>

This was the big realization moment for me when comparing bash and task together. Bash was very good for a quick text change, kicking off a command or two, but where task shinned was doing the important actions I needed. Once that was settled, I also ensured my cfn-task command could be run from anywhere. Anyone curious, this was the function I created

cfn-task() {
  # 1. Help Menu Trigger
  if [[ "$1" == "-h" || "$1" == "--help" ]]; then
    echo "Usage: cfn-task [action] [stack_name] [region]"
    echo "Examples:"
    echo "  cfn-task list us-east-1           # List stacks in N. Virginia"
    return 0
  fi

  local action=${1:-default}
  local stack_or_region=$2
  local region_input=$3
  
  local ARGS=()

  # Logic: If action is 'list', the second arg is likely the region
  if [[ "$action" == "list" ]]; then
    local TARGET_REGION=${stack_or_region:-us-east-2}
    ARGS+=(REGION="$TARGET_REGION")
  else
    # For plan/apply, the second arg is the stack, third is region
    local STACK_UPPER=$(echo "${stack_or_region:-<aws-stack-name>}" | tr '[:lower:]' '[:upper:]')
    local TARGET_REGION=${region_input:-us-east-2}
    ARGS+=(STACK_NAME="$STACK_UPPER" REGION="$TARGET_REGION")
  fi

  task -t /<task_list_root>/<task_list>.yaml -d /<task_list_root> "$action" "${ARGS[@]}"
}

One other fun little thing about my apply task. I added some functionality to the tasklist to ensure it would grab the latest change set for deploying. So if on Friday I didn’t apply the change, or if I had multiple successful plans, I would never go back to an older change set version. Also, I didn’t want to figure out the exact name of the change set. Did I mention I am lazy at times?

  apply:
    desc: Deploy the changes
    vars:
      LATEST_CS:
        sh: |
          aws cloudformation list-change-sets --stack-name {{.STACK_NAME}} --profile {{.PROFILE}} --region {{.REGION}} \
          --query 'Summaries[?Status==`CREATE_COMPLETE`] | sort_by(@, &CreationTime) | [-1].ChangeSetName' --output text
    cmds:
      - echo "Running apply for {{.STACK_NAME}}..."
      - aws cloudformation execute-change-set --stack-name {{.STACK_NAME}} --change-set-name {{.LATEST_CS}} --profile {{.PROFILE}} --region {{.REGION}}
      - sleep 8 # This is to allow AWS CFN to settle before trying to wait for a stack to disappear. This number is in seconds
      - aws cloudformation wait stack-update-complete --stack-name {{.STACK_NAME}} --profile {{.PROFILE}} --region {{.REGION}}
      - aws cloudformation describe-stacks --stack-name {{.STACK_NAME}} --query 'Stacks[0].StackStatus' --output text --profile {{.PROFILE}} --region {{.REGION}}
      - rm ./current_deployed.yaml
    silent: false

Final Thoughts

For a Friday adventure, it was enjoyable, and this should save me some time later as I dive more into CloudFormation to get some other key parts of my infrastructure up and running. So, yes, it took me most of the day to get through creating this; in the long run however, it should save me time and provide some useful functionality.

Eventually, I’ll get the full script posted, but that process is another and another project down the road for me. Now, did I use AI for this? Of course, but it was mostly for troubleshooting certain errors or finding that one line command I needed. But hey, maybe it was nice to have that virtual assistant with me? Also, I timeboxed myself to get something working by the end of the day. It was much easier to ask for the CLI command to get the errors and their nuances vs hours of googling and rolling through StackOverflow posts.

Now, instead of having those four manual commands for a single stack, I have a simple command that only needs a couple of parameters, and I can abstract trying to remember the million options an AWS CLI command can have.

For those keeping track, here is what I have, and it feels very Terraform-like:

➜  ~ cfn-task
task: Available tasks for this project:
* apply:         Deploy the changes
* clean:         Delete pending change sets for my stacks.
* create:        Create new CFN stack and apply
* default:       List all of the task options
* deploy:        Plan and Deploy the changes.
* errors:        Find out why plan or apply failed
* list:          List all CFN Stacks
* plan:          Create and review the changes.

The irony: this website for this blog post, at the time of writing, isn’t even live yet. I need to set up DNS and certs, and that requires my CloudFormation Stack to be updated. The thought of copying those AWS CLI commands again was too much for me.

Why this matters for Granite Dog Systems

Automation isn’t just about saving five minutes; it’s about creating a repeatable, auditable, and safe environment. Whether it’s a personal blog or a client’s production infrastructure, the goal is always the same: Total Visibility and Zero Surprises.

Tool References