AWS released a CloudFormation Timeline to the AWS Console. Presenting the Stack Events like this helps us comprehend the reasons why a Stack takes time to update, and visualize causes of failure.
GitHub Actions runs my deployments, code that gets merged into main runs in GitHub Actions. That's where I'm looking at when I click merge Pull Request. When there is an error debugging is
Going from GitHub Acions to the AWS Console.
Give me the Stack Name
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("stack", help="Name of CloudFormation Stack")
# tbh i could find this, but i already have this vslue where I use this
parser.add_argument("start", help="Time of First in Event ISO Formatted Time")
args = parser.parse_args()
init_time = datetime.fromisoformat(args.start)
stack_name = args.stack
Get the Stack Events
from boto3 import Session
from datetime import datetime
session = Session()
cloudformation = session.client("cloudformation")
paginator = cloudformation.get_paginator("describe_stack_events")
def get_deployment_events(stack_name: str, start_time: datetime):
for page in paginator.paginate(StackName=stack_name):
for stack_event in page["StackEvents"]:
if stack_event["Timestamp"] < start_time:
return
yield stack_event
Group Events By Resource
from collections import defaultdict
events_by_resource = defaultdict(list)
for event in get_deployment_events(stack_name, init_time):
events_by_resource[event["LogicalResourceId"]].append(event)
Build a Gantt Chart
MERMAID_GANTT = f"""
gantt
title Stack {stack_name} Deployment
dateFormat x
axisFormat %S.%L
"""
for section_name, events in events_by_resource.items():
section = f"\n section {section_name} [{events[0]["ResourceType"]}]\n"
MERMAID_GANTT += section
for index, event in enumerate(events):
status = event["ResourceStatus"]
earlier_events = events[index+1:]
if status.rpartition("_")[-1] not in {"FAILED", "COMPLETE"}:
continue
init_status = "_".join((status.rpartition("_")[0], "IN_PROGRESS"))
end_time = int(event["Timestamp"].timestamp() * 1000)
# try to find init time
init_time = end_time # use end_time as default if can't find start
# there may be more than 1 event with the init_status
# we naively at the moment select the earliest
candidates = [candidate for candidate in earlier_events if candidate["ResourceStatus"] == init_status]
if candidates:
earliest_candidate = candidates[-1]
init_time = int(earlist_candidate["Timestamp"].timestamp() * 1000)
is_failure = status.endswith("FAILED")
label = "crit" if is_failure else "done"
MERMAID_GANTT += f" {status} :{label}, {init_time}, {end_time}\n"
def format_github(mermaid_diagram_data):
return f"```mermaid\n{mermaid_diagram_data}\n```"
print(format_github(MERMAID_GANTT))
And the output
gantt
title Stack Example Deployment
dateFormat x
axisFormat %S.%L
section Example [AWS::CloudFormation::Stack]
CREATE_COMPLETE :done, 1732724310778, 1732724331438
section RoleB [AWS::IAM::Role]
CREATE_COMPLETE :done, 1732724312879, 1732724330031
section RoleA [AWS::IAM::Role]
CREATE_COMPLETE :done, 1732724312847, 1732724329912
section PolicyA [AWS::IAM::ManagedPolicy]
CREATE_COMPLETE :done, 1732724312866, 1732724329101