| name | add-blueprint |
| description | Add a new blueprint stack (or any new CloudFormation template) to this repo. Use whenever creating a template under blueprints/*/infra/, registering a stack in pipeline/stacks.yml, adding a BlueprintDeploy action, or when a newly added stack deploys nothing. Covers the three-file mirror, the four cornell:* tags, stack naming, and the blueprint.yaml manifest. |
Adding a blueprint stack
Adding a stack is three files edited in one PR. Steps 2 and 3 are mirrored by hand on
purpose — generating stages from the registry is the framework the workshop spec says not to
build.
Skipping step 3 is the expensive mistake: the PR goes green, all pipeline stages report
Succeeded, and no stack appears. validate_stacks.py now catches it at review time, but
only if you run tools/check.
Step 1 — the template
Location: blueprints/<name>/infra/<name>.yml.
Model it on blueprints/hello-world/infra/hello-world.yml. Required parameter block:
Parameters:
Application:
Default: 'aidlc'
AllowedPattern: '[a-z0-9-]{1,10}'
Description: 'Name used for billing and other tracking/identification purposes'
Type: 'String'
Environment:
Default: 'main'
AllowedPattern: '[a-z0-9]{1,4}'
Description: 'Name of the deployment branch'
Type: 'String'
Owner:
Default: 'ai-sei'
AllowedPattern: '.{1,128}'
Description: 'Who owns this deployment (cornell:owner)'
Type: 'String'
BlueprintVersion:
Default: '0.1.0'
AllowedPattern: '[0-9]+\.[0-9]+\.[0-9]+'
Description: 'Version of the <name> blueprint (cornell:blueprint-version)'
Type: 'String'
Environment is capped at four characters, no hyphens. It is a component of every stack
name and of the stack/${Application}-${Environment}* prefix BuildPipelineRole scopes to.
Do not widen the pattern in one template — it has to change in pipeline.yml and every
blueprint template together.
The four tags, on every resource
cornell:owner, cornell:blueprint, cornell:blueprint-version, cornell:deployment-id.
An untagged resource is invisible to inventory and the cost dashboard.
Owner and deployment id arrive as parameters. Blueprint name and version belong to the
template — hardcode the name, bump the BlueprintVersion default in the PR that changes
the blueprint.
Normal (list) form:
Tags:
- Key: 'Application'
Value: !Ref 'Application'
- Key: 'Environment'
Value: !Ref 'Environment'
- Key: 'Resource'
Value: 's3-bucket-<name>'
- Key: 'cornell:owner'
Value: !Ref 'Owner'
- Key: 'cornell:blueprint'
Value: '<name>'
- Key: 'cornell:blueprint-version'
Value: !Ref 'BlueprintVersion'
- Key: 'cornell:deployment-id'
Value: !Sub '${Application}-${Environment}-<name>'
AWS::SSM::Parameter is the exception — it takes Tags as a map:
Tags:
Application: !Ref 'Application'
Environment: !Ref 'Environment'
Resource: 'ssm-parameter-<name>'
'cornell:owner': !Ref 'Owner'
'cornell:blueprint': '<name>'
'cornell:blueprint-version': !Ref 'BlueprintVersion'
'cornell:deployment-id': !Sub '${Application}-${Environment}-<name>'
Every other resource in this repo uses the list form. Getting this backwards is a cfn-lint
failure that reads like a schema problem.
Don't hardcode a singleton by accident
hello-world hardcodes its bucket name and deployment id to <app>-<env>-hello-world, so
only one deployment can exist per app/environment — it is marked singleton: true for that
reason. A real blueprint should take a DeploymentName parameter and interpolate it, so the
same blueprint can be deployed more than once.
Step 2 — register it in pipeline/stacks.yml
Registering is what makes PR checks lint it.
- name: '<name>'
template: 'blueprints/<name>/infra/<name>.yml'
deployed_by: 'pipeline'
description: 'One line on what it deploys and anything unusual about how.'
deployed_by is pipeline or manual. Use manual only for chicken-and-egg bootstrap, and
say why in the description.
Step 3 — add the deploy action in pipeline/pipeline.yml
Add to the BlueprintDeploy stage, modelled on HelloWorldCloudFormation:
- Name: '<Name>CloudFormation'
Namespace: '<Name>CloudFormation'
RunOrder: 1
InputArtifacts:
- Name: 'GitRepositoryArtifact'
ActionTypeId:
Category: 'Deploy'
Owner: 'AWS'
Provider: 'CloudFormation'
Version: '1'
Configuration:
ActionMode: 'CREATE_UPDATE'
Capabilities: 'CAPABILITY_NAMED_IAM'
RoleArn: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cloudformation-deploy-role'
StackName: !Sub '${Application}-${Environment}-<name>'
TemplatePath: 'GitRepositoryArtifact::blueprints/<name>/infra/<name>.yml'
ParameterOverrides: !Sub >-
{
"Application": "${Application}",
"Environment": "${Environment}",
"Owner": "${Owner}",
"SourceCommitId": "#{GitRepository.CommitId}"
}
Two things that are load-bearing here:
StackName must be <application>-<environment>-<name>. BuildPipelineRole scopes
CloudFormation permissions to arn:...:stack/${Application}-${Environment}*. A stack named
outside the convention produces an opaque authorization failure, not a naming complaint.
- Pass every parameter explicitly. Template defaults exist so a stack can be deployed by
hand for debugging; they are not the real values. A blueprint should deploy identically by
hand and by pipeline.
If the blueprint needs a container image, see the add-container-build skill — the image
digest is passed in here as #{<Namespace>.CONTAINER_DIGEST}.
Step 4 (blueprints only) — the blueprint.yaml manifest
blueprints/<name>/blueprint.yaml is the contract the Builder MCP reads. It is not a
CloudFormation template — it declares no AWSTemplateFormatVersion, which is what keeps
validate_stacks.py and cfn-lint away from it. Do not add that key, even in a comment: the
check is a text scan.
Model it on blueprints/hello-world/blueprint.yaml.
This step is enforced, not advisory. tools/check fails a blueprint directory with no
manifest, because skipping it is invisible otherwise: the blueprint deploys on every merge while
blueprint_search silently skips the directory, and a builder asking for it gets the
next-closest blueprint instead of nothing. It also fails a manifest whose metadata.version has
drifted from the BlueprintVersion default in its template, whose metadata.name disagrees with
the directory name, or whose template: is unregistered or missing.
If a blueprint genuinely should not be in the catalog — a scaffold that deploys nothing, or a
Terraform-only blueprint with no CloudFormation template to advertise — add it to
MANIFEST_EXEMPT in pipeline/validate_stacks.py with the reason, rather than leaving the
manifest out. course-chatbot and entra-probe are the current entries.
Verify
tools/check
The registry section prints <- deployed by a pipeline action next to each wired template.
Confirm your new stack shows that marker — its absence is exactly the silent step-3 miss.
The blueprints section below it prints <- in the builder catalog per directory, which is the
same confirmation for step 4.
Never run bare cfn-lint or python pipeline/validate_stacks.py; they fail on a clean
machine. tools/check is what CI runs.
Authoring note
validate_stacks.py rglobs the entire repo for *.yml/*.yaml containing
AWSTemplateFormatVersion, and its SKIP_DIRS is only
.git, .github, node_modules, .venv, __pycache__, outputs-preview. So a scratch or
example template
saved anywhere else — including .claude/ or docs/ — fails tools/check as an
unregistered template. Keep examples inside markdown fences.