Infrastructure templates, hands-on lab manifests, and automation assets for building Internal Developer Platforms (IDPs) on Amazon EKS.
Amazon EKS Auto Mode · EKS Capabilities (Argo CD, kro, ACK) · Crossplane · Backstage · CNOE
This is sample code, for non-production usage. You should work with your security and legal teams to meet your organizational security, regulatory and compliance requirements before deployment. The environment ships demo-grade defaults that are deliberate teaching choices, not recommendations: read Security and Hardening before you reuse any of it.
This repository is the code. The guided, step-by-step content lives in a separate workshop:
Platform Engineering Foundations on AWS
If you landed here first, start there: it walks the labs in order and explains the reasoning behind each step. This repository is what the workshop clones and applies.
- Overview
- The Problem
- What You Will Build
- Repository Structure
- Getting Started
- Labs
- Optional Module: Migrating Legacy IaC
- Going Further: Fleet Management
- Cost
- Cleanup
- Troubleshooting
- Known Limitations
- Security and Hardening
- Documentation and References
This repository holds the code used by the Platform Engineering Foundations on AWS workshop. You can consume it two ways:
- Guided - follow the workshop on AWS Workshop Studio, which walks through each lab in order: Platform Engineering Foundations on AWS
- Self-paced - deploy the same two CloudFormation stacks in your own account, from this repository, and follow the same workshop content at your own pace. The commands are in Getting Started below and on the workshop's 0.1 Self-Paced Setup page; the two match.
Everything runs in a single AWS account, in a single Region.
Platform teams are asked to give developers self-service infrastructure without handing them the whole AWS API. Three questions decide whether that works:
- Where do platform controllers run, and what can they reach? Controllers hold broad IAM permissions and cluster-scoped CRDs. Running them alongside application workloads entangles blast radius with permission boundaries.
- How much abstraction is the right amount? Every abstraction you add removes a choice from your users. Each removal has to buy them something real.
- What do you do with what already exists? A greenfield platform is a demo. A real one inherits hundreds of resources created by CloudFormation, Terraform, and the console.
The labs work through all three, in that order.
Figure 1: Three-cluster topology. One VPC per cluster with non-overlapping CIDRs, peered in a full mesh so the platform cluster can provision into a genuinely remote cluster.
| Cluster | Role |
|---|---|
psp-cluster-1-platform |
Platform control plane. EKS Capabilities (Argo CD, kro, ACK), Crossplane, Backstage portal |
psp-cluster-2-cnoe-diy |
The CNOE reference implementation, assembled from open source components |
psp-cluster-3-apps |
Workload cluster. Receives applications and infrastructure provisioned through the platform APIs |
All three run Kubernetes 1.35 with EKS Auto Mode. There are no managed node groups and no
launch templates anywhere: compute comes from the general-purpose and system node pools, and
load balancing and block storage are operated by AWS.
Clusters 1 and 2 are deliberately two answers to the same question. Cluster 1 uses managed EKS Capabilities, where AWS operates the controllers. Cluster 2 uses the CNOE stack, where you operate everything. Running both makes the trade-off concrete instead of theoretical.
Figure 2: Access path and the CNOE stack inside the cluster. Participants reach a pre-authenticated browser IDE through CloudFront; Backstage drives kro, Argo CD drives ACK, and provisioning reaches Route 53, Secrets Manager, and CloudWatch.
See docs/architecture.md for the reasoning behind the topology and docs/resources-provisioned.md for the complete resource inventory.
.
├── infrastructure/cloudformation/ Environment provisioning
│ ├── psp-workshop-eks.yaml 3 clusters, 3 VPCs, peering, Backstage supporting stack
│ └── psp-workshop-code-editor.yaml Browser IDE (code-server) with one-click access
│
├── assets/backstage-genai/ Backstage image build (Dockerfile, configs, pinned lock/),
│ deploy manifest, and the custom resource Lambda sources
│
├── labs/ Hands-on lab trees, seeded into ~/environment in the IDE
│ ├── ack/ AWS Controllers for Kubernetes
│ ├── kro/ Kubernetes Resource Orchestrator
│ └── crossplane/ Compositions and Backstage catalog
│
├── automation/ Optional module 4: legacy IaC to ACK and kro
│ ├── legacy-estate/ The IaC estate you transform (seeded as ~/environment/legacy-iac)
│ ├── iac-to-ack-kiro-skill/ Front 1, conversational
│ └── iac-to-ack-atx-custom/ Front 2, batch via AWS Transform
│
├── scripts/ Publish everything above to your assets bucket
│ ├── upload-backstage-genai-assets.sh Backstage build files + Lambda archives
│ ├── upload-lab-assets.sh labs/{ack,kro,crossplane}
│ └── upload-iac-migration-assets.sh automation/legacy-estate + the ATX definition
│
└── docs/
├── architecture.md Why the topology looks like this
├── resources-provisioned.md Complete resource inventory
├── cleanup.md Teardown runbook
├── diagrams/ draw.io sources + the diagram convention
└── images/ Exported figures
The step-by-step walkthrough, with screenshots and the reasoning behind each step, is the 0.1 Self-Paced Setup page of the workshop. The commands below are the same sequence, for reference.
| Requirement | Notes |
|---|---|
| AWS account | A dedicated, non-production account with no other workloads or data, see Security and Hardening. AdministratorAccess or equivalent: the templates create named IAM roles |
| Region | us-east-1 is the tested region, see Known Limitations |
| AWS CLI v2 | Configured with credentials for the target account |
| Git | To clone this repository |
zip and python3 |
Used by upload-backstage-genai-assets.sh to validate and package the Lambda handlers |
| Amazon Bedrock model access | For the Backstage GenAI plugin. The stack validates access to BedrockModelId at deploy time |
kubectl |
Optional. Already installed in the browser IDE; only needed for cluster access from your laptop |
git clone https://github.com/aws-samples/sample-platform-engineering-foundations.git
cd sample-platform-engineering-foundations
export AWS_REGION=us-east-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)Neither stack carries its own code. They read the Lambda archives, the Backstage build files, the lab trees and the module 4 artefacts from an S3 bucket you own, which is also where the CLI stages the templates.
export ASSETS_BUCKET="psp-assets-${ACCOUNT_ID}"
aws s3api create-bucket --bucket $ASSETS_BUCKET --region $AWS_REGION
# Backstage build files + the Lambda archives the main stack references
./scripts/upload-backstage-genai-assets.sh $ASSETS_BUCKET $AWS_REGION
# The hands-on lab trees. The IDE seeds ~/environment/{ack,kro,crossplane} from these
./scripts/upload-lab-assets.sh $ASSETS_BUCKET $AWS_REGION
# Module 4: the legacy estate and the transformation definition
./scripts/upload-iac-migration-assets.sh $ASSETS_BUCKET $AWS_REGION
create-bucketwith--region us-east-1needs no location constraint. In any other Region add--create-bucket-configuration LocationConstraint=$AWS_REGION.
Each script checks its inputs before uploading and asserts what landed, so a missing piece fails here rather than fifteen minutes into a deploy. Confirm the top-level layout:
aws s3 ls s3://$ASSETS_BUCKET/ --recursive | awk '{print $4}' | cut -d/ -f1 | sort -uack
backstage-genai
crossplane
iac-migration
kro
lambda
All three scripts are required. The IDE stack fails if
ack,kroorcrossplanecomes back empty, so a forgotten lab upload stops you at deploy time. The module 4 seeding never fails the stack, because the module is optional, so a forgotteniac-migrationupload only shows up at section 4.2 as an empty~/environment/legacy-iac.
aws cloudformation deploy \
--stack-name psp-workshop-eks \
--template-file infrastructure/cloudformation/psp-workshop-eks.yaml \
--s3-bucket $ASSETS_BUCKET \
--parameter-overrides \
WorkshopName=psp \
WorkshopAssetsBucket=$ASSETS_BUCKET \
--capabilities CAPABILITY_NAMED_IAM \
--region $AWS_REGIONRoughly 20 minutes, 115 resources. During the create, a custom resource builds the Backstage image in CodeBuild from the files you published, so the portal image is in ECR before you need it.
--s3-bucketis required, not optional. Both templates exceed the 51,200-byte limit CloudFormation accepts inline, so the CLI has to stage them in S3 first. Without the flag the command fails immediately withTemplates with a size greater than 51,200 bytes must be deployed via an S3 Bucket.
Keep the stack name
psp-workshop-eks. The workshop content resolves stack outputs and parameters by that name.
Leave
Environmentat its default,dev.prodadds six EKS Access Entries forWSParticipantRoleandWSOpsRole, roles that exist only in an AWS-hosted workshop account. In your own account they fail withThe specified principalArn is invalidand roll the whole stack back. Ondevthe identity that creates the clusters keeps implicit admin, and everything else, including the Bedrock model validation, is created either way.
IAM Identity Center for section 2.1. The managed Argo CD capability signs in through an Identity Center instance. In your own account section 2.1 creates it from the terminal, so you pass nothing here.
CreateIdentityCenterInstance=truemakes the stack create it instead, and fails the stack in an AWS Organization member account whose management account disables account-level instances.
aws cloudformation deploy \
--stack-name psp-workshop-code-editor \
--template-file infrastructure/cloudformation/psp-workshop-code-editor.yaml \
--s3-bucket $ASSETS_BUCKET \
--parameter-overrides \
WorkshopAssetsBucket=$ASSETS_BUCKET \
AssetsBucketPrefix= \
--capabilities CAPABILITY_NAMED_IAM \
--region $AWS_REGIONRoughly 7 minutes, 19 resources. It must run second: the main stack publishes seven SSM parameters (VPC and subnet IDs, cluster names) that this stack reads as parameter defaults.
Pass the same bucket again, with an empty prefix. The IDE seeds
~/environmentfrom it during provisioning.AssetsBucketPrefixis deliberately empty because the scripts uploaded to the root of your bucket; an AWS-hosted event fills it with its own prefix. LeaveWorkshopAssetsBucketout and seeding is skipped: the stack still completes, and the IDE opens with an empty~/environment.
Collect the IDE URL and open it:
aws cloudformation describe-stacks --stack-name psp-workshop-code-editor --region $AWS_REGION \
--query "Stacks[0].Outputs[?OutputKey=='IdeUrl'].OutputValue" --output textIt is a pre-authenticated CloudFront URL that lands you in code-server with kubectl, helm, the
AWS CLI and atx. The stack completes before code-server finishes installing, because setup runs
through SSM State Manager: if the page does not load, wait two to three minutes and refresh.
From a terminal inside the IDE:
ls ~/environment
# ack ack-adoption-transformation crossplane kro legacy-iac
git -C ~/environment/legacy-iac log --oneline -1 # the estate is a git repository, atx needs it
aws eks update-kubeconfig --name psp-cluster-1-platform --region $AWS_REGION --alias cluster-1
aws eks update-kubeconfig --name psp-cluster-2-cnoe-diy --region $AWS_REGION --alias cluster-2
aws eks update-kubeconfig --name psp-cluster-3-apps --region $AWS_REGION --alias cluster-3
for c in cluster-1 cluster-2 cluster-3; do echo "== $c"; kubectl --context $c get nodepools; doneAn empty node list is expected: EKS Auto Mode provisions nodes when the first workload needs one.
~/environment/cnoe must not exist yet. You clone it from the in-cluster Gitea in module 3, and a
pre-existing directory makes that git clone fail. The other directories the labs use
(custom-templates, catalog) are created as you go.
From here the workshop content is identical to an AWS-guided event. Only provisioning and cleanup differ.
Each lab provisions real AWS resources from Kubernetes at a different level of abstraction. Working through them in order shows the progression from raw controllers to a curated platform API.
ACK 11 manifests, one per AWS resource, ordering is yours to manage
│
▼
kro 1 ResourceGroup authored once, 1 instance manifest consumed by developers
│
▼
Crossplane a versioned catalogue of compositions, surfaced as Backstage templates
Eleven numbered manifests that build a complete serverless application, applied in sequence. The lowest level of abstraction: one Kubernetes resource per AWS resource, no composition.
01-rds-subnet-group → 05-lambda-security-group → 09-apigatewayv2-route
02-rds-security-group → 06-lambda-function → 10-apigatewayv2-stage
03-rds-instance → 07-apigatewayv2-api → 11-s3-frontend-bucket
04-lambda-role → 08-apigatewayv2-integration
The takeaway: ACK gives you full fidelity to the AWS API, and the ordering and wiring are your responsibility.
The same application as a single ResourceGroup that a developer consumes with one small instance
manifest. Eleven manifests collapse into one, and kro resolves the dependency graph, so the ordering
problem from the ACK lab disappears.
| Path | Content |
|---|---|
setup/, scripts/ |
Provider installation and helper scripts |
basic/ |
A first CompositeResourceDefinition and matching composition |
advanced/crossplane-backstage/configs/ |
Provider and function configurations |
advanced/workshop-setup/ |
The platform catalog |
The catalog lives under advanced/workshop-setup/platform/backstage/templates/custom-catalog/ and
covers database/postgresql, database/dynamodb, cluster/eks-basic,
cluster/eks-addon/loadbalancer-controller, compute/lambda, compute/container,
network/basic, bucket/webhost, bucket/logging, frontend/spa, app/fullstack, and
app/serverless-multi-tier. Every composition is versioned (v0.0.1/) so the catalog can evolve
without breaking existing claims, and each domain ships the template.yaml and skeleton/ that
expose it as a Backstage software template.
The labs build platform APIs on a clean environment. This module addresses what you actually walk into: resources that already exist, created by other tooling, which need to come under Kubernetes and GitOps control without being recreated.
Figure 3: The migration, offered as two fronts. Both emit the same artefact shape and both enforce
adopt-or-create with deletion-policy: retain.
| Front | Delivery |
|---|---|
automation/iac-to-ack-kiro-skill |
Conversational, one resource at a time. Reads live AWS state |
automation/iac-to-ack-atx-custom |
Batch across a whole repository via the AWS Transform CLI, emitting kro ResourceGraphDefinitions for modules and nested stacks |
The batch front runs against automation/legacy-estate, a small payments
platform in Terraform, CloudFormation and Crossplane that is read and transformed, never applied.
The IDE seeds it as ~/environment/legacy-iac, and the transformation definition as
~/environment/ack-adoption-transformation, from what upload-iac-migration-assets.sh published.
Do one resource by hand with the Kiro Skill first: adoption has a failure mode that is easy to hit and hard to diagnose from a batch report. See automation/README.md for prerequisites and the two rules that separate a migration from an outage.
The labs stop at managing resources from a cluster. The layer above is managing clusters the same way: the EKS cluster itself becomes a resource with an API, a management cluster provisions and bootstraps workload clusters, Argo CD registers them automatically, and Kargo promotes an application across the fleet.
That material is a separate AWS sample, and it is not duplicated here. Clone it on its own:
git clone https://github.com/aws-samples/fleet-management-on-amazon-eks-workshop.git
cd fleet-management-on-amazon-eks-workshop/patterns/kro-eks-cluster-mgmt
cat README.mdIt is a separate environment, with its own IDE stack, Terraform, and GitLab, and it costs materially more than this workshop: hub plus four spokes across two regions means five EKS control planes and five VPCs. Deploy it in a fresh account, not on top of the three clusters from these labs, and read its README first: the prerequisites and the deploy order are its own.
Its
patterns/kro-eks-cluster-mgmt/directory is the entry point; the rest of the repository covers other patterns you do not need for this.
Cost is driven by resources that bill per hour whether or not you are using them.
| Component | Quantity | Billing model |
|---|---|---|
| EKS cluster (control plane) | 3 | Per cluster-hour |
| NAT Gateway | 3 | Per hour, plus data processing per GB |
| Application Load Balancer | 3 | Per hour, plus LCU |
| EKS Auto Mode compute | Varies with the labs | EC2 On-Demand plus an Auto Mode management fee |
| IDE instance | 1 t3.medium |
On-Demand, plus 60 GB gp3 |
| CloudFront, S3, ECR, SSM, Secrets Manager | Low volume | Usage based |
Estimate a session with the AWS Pricing Calculator before you start, especially for a group: three control planes, three NAT Gateways and three load balancers accrue by the hour independently of activity, and the labs add RDS instances and Auto Mode compute on top.
The single most effective cost control is deleting the stacks when you finish.
Order matters. Resources created by the clusters during the labs must be removed before the stacks that own the clusters, otherwise stack deletion stalls on dependencies.
1. Crossplane claims, ACK resources, kro instances, Kubernetes ingresses and LoadBalancer Services
2. psp-workshop-code-editor
3. psp-workshop-eks
4. Verify nothing survived
5. What CloudFormation does not own: log groups, the imported self-signed certificate, the assets bucket
aws cloudformation delete-stack --stack-name psp-workshop-code-editor --region $AWS_REGION
aws cloudformation wait stack-delete-complete --stack-name psp-workshop-code-editor --region $AWS_REGION
aws cloudformation delete-stack --stack-name psp-workshop-eks --region $AWS_REGION
aws cloudformation wait stack-delete-complete --stack-name psp-workshop-eks --region $AWS_REGIONFull procedure, including step 5 and the cases that commonly get stuck, in docs/cleanup.md.
| Symptom | Cause | Fix |
|---|---|---|
Templates with a size greater than 51,200 bytes must be deployed via an S3 Bucket |
Both templates exceed the inline limit, so the CLI must stage them in S3 | Add --s3-bucket $ASSETS_BUCKET to the deploy command. Reuse the assets bucket |
Main stack fails immediately on WorkshopAssetsBucket |
The parameter has no default and the bucket must already contain the Lambda archives | Create the bucket, run the three upload scripts, pass the name with --parameter-overrides |
Main stack fails on BackstageBuildTrigger with assets ... are missing or stale (marker ... not found) |
The build files in the bucket are absent or older than the template expects, so the image would have been built without the current patches | Re-run ./scripts/upload-backstage-genai-assets.sh $ASSETS_BUCKET $AWS_REGION, delete the failed stack, deploy again |
Main stack fails on BackstageBuildTrigger with Build ... finished as FAILED |
The image build failed in CodeBuild | Read /aws/codebuild/psp-backstage-build. The build installs a pinned dependency snapshot (assets/backstage-genai/lock/), so a registry release cannot change it |
IDE stack fails with FATAL: no files seeded for: ... |
The assets bucket had no lab tree under that name when the IDE provisioned | Run ./scripts/upload-lab-assets.sh $ASSETS_BUCKET $AWS_REGION, delete the IDE stack, deploy it again |
~/environment is empty in the IDE |
The IDE stack was deployed without WorkshopAssetsBucket, so seeding was skipped |
Delete the IDE stack and redeploy it with WorkshopAssetsBucket=$ASSETS_BUCKET AssetsBucketPrefix= |
~/environment/legacy-iac is missing or empty (module 4) |
upload-iac-migration-assets.sh did not run before the IDE stack. Module 4 seeding is never fatal, so the stack completed anyway |
Run the script, then delete and redeploy the IDE stack. Or copy by hand: aws s3 cp s3://$ASSETS_BUCKET/iac-migration/legacy-estate ~/environment/legacy-iac --recursive and git init it |
atx says each package being transformed must be managed by git |
~/environment/legacy-iac is not a git repository |
cd ~/environment/legacy-iac && git init -b main && git add -A && git commit -m 'estate' |
Changeset creation fails with AWS::EarlyValidation::ResourceExistenceCheck after a previous attempt rolled back |
Four log groups outlive the rollback, because the Lambda and CodeBuild recreate them as soon as they run. The next deploy is rejected before it starts, on a name that already exists | Delete them and deploy again: for g in /aws/codebuild/psp-backstage-build /aws/lambda/psp-backstage-build-trigger /aws/lambda/psp-bedrock-prewarm /aws/lambda/psp-supported-list-validator; do aws logs delete-log-group --log-group-name $g; done. Also delete the REVIEW_IN_PROGRESS stack left behind by the failed changeset |
| IDE stack fails resolving its VPC or subnet parameters | It reads SSM parameters published by the main stack. Either the main stack has not finished, or the two stacks were deployed with different WorkshopName values |
Confirm the main stack is CREATE_COMPLETE, then check aws ssm get-parameter --name /workshop/psp/vpc-1-id |
IdeUrl opens but the editor is not there yet |
Setup runs through SSM State Manager after the stack completes, so the instance answers before code-server is installed | Wait two to three minutes. If it persists, check the State Manager association and the SSM command output bucket |
kubectl cannot reach a cluster from the IDE |
Access is granted by EKS::AccessEntry, one per cluster |
aws eks update-kubeconfig --name <cluster> --region us-east-1, then kubectl auth can-i get pods |
| No nodes appear when a pod is pending | Auto Mode provisions nodes on demand and a first node takes a minute | kubectl get nodepools and kubectl describe pod to confirm the scheduling constraint matches a node pool |
An ACK resource sits with ACK.Terminal: True |
An invalid or incompatible field in the spec, often a field that changed without an API version bump | kubectl describe <kind> <name> and read the condition message. Fix the spec and re-apply |
An ACK resource sits with ACK.Recoverable: True |
Transient: throttling, or the controller cannot assume its role | Check the controller logs and the Pod Identity association |
| Crossplane claim stays not ready | Usually the provider cannot authenticate, or a referenced resource has not been created yet | kubectl get managed to see what the composition actually created, then check the provider pod logs |
| Backstage portal returns 503 | The pod is not yet registered behind the target group created by CloudFormation | Confirm the TargetGroupBinding exists and the pod is Running |
| Stack deletion stalls on a VPC | Something the cluster created is still attached, commonly a load balancer from a Kubernetes ingress | Follow docs/cleanup.md, which covers ENIs, security group references, and finalizers |
| Limitation | Detail |
|---|---|
| Workshop assets must be published before the first deploy | Both stacks read from the assets bucket, so the three upload scripts have to run before cloudformation deploy, and again (followed by a redeploy of the IDE stack) after you change anything under labs/ or automation/ |
us-east-1 is the only tested region |
CloudFront and ACM handling assumes it, and Bedrock model availability for the Backstage GenAI plugin varies by region |
Environment=prod requires an AWS-hosted workshop account |
It creates six EKS Access Entries for WSParticipantRole and WSOpsRole, roles the hosting platform provides. In your own account the default dev is correct |
| One NAT Gateway per VPC | A cost choice, not a resilience recommendation. Production designs place one per Availability Zone |
AWS Transform custom is not in sa-east-1 |
The batch migration front reads code rather than clusters, so set AWS_REGION=us-east-1 for the transformation even when your workloads run in São Paulo |
| Fleet management lives in another repository | Not duplicated here. See Going Further |
| Document | Content |
|---|---|
| docs/architecture.md | Why three clusters, how peering and Auto Mode are configured, stack dependencies |
| docs/resources-provisioned.md | Every resource each template creates, with parameters and outputs |
| docs/cleanup.md | Teardown in order, and the cases that get stuck |
| docs/diagrams/README.md | The diagram convention: versioned draw.io sources, the CLI export, and the constraints that keep a figure legible |
| docs/reference-architectures/README.md | Reference architectures: GitOps multicluster with Argo CD, platform APIs with kro and ACK, Crossplane compositions, and the CNOE developer portal |
| automation/README.md | The optional migration module and its two fronts |
| scripts/upload-backstage-genai-assets.sh | Packages and uploads the Lambda archives and Backstage build files the main stack consumes |
| scripts/upload-lab-assets.sh | Publishes labs/{ack,kro,crossplane}, which the IDE seeds into ~/environment |
| scripts/upload-iac-migration-assets.sh | Publishes the module 4 estate and transformation definition |
External: Amazon EKS · EKS Auto Mode · kro · ACK · Crossplane · Argo CD · Backstage · CNOE · AWS Transform custom
See CONTRIBUTING.md. Please open an issue before submitting substantial changes.
This environment is built to teach, in a disposable account, for the length of a workshop. Some of its defaults would be defects in production. They are listed here rather than silently fixed, because each one exists to make a lesson visible.
Deploy it into a dedicated, non-production account. In an AWS-hosted workshop the account is destroyed when the event ends. When you run it self-paced in your own account, nothing is destroyed automatically: every endpoint below stays reachable until you run Cleanup.
While the stacks exist, these endpoints are reachable from the internet:
| Endpoint | Protocol | Access control |
|---|---|---|
IDE CloudFront distribution (IdeUrl) |
HTTPS | Token in the URL, exchanged for a cookie at the edge |
| Backstage CloudFront distribution | HTTPS | None. Guest sign-in |
psp-alb, the workshop ingress (Argo CD UI) |
HTTP 80 | Argo CD login |
psp-cnoe-alb, the CNOE ingress (Argo CD, Backstage, Keycloak) |
HTTP 80, HTTPS 443 with a self-signed certificate | The login of each application |
| Default | Why it is like that | Before you reuse it |
|---|---|---|
The IdeUrl is pre-authenticated: possession of the URL is the entire access control |
Participants have to reach a working IDE in one click, with no account setup. The origin is not reachable around CloudFront: the instance accepts only the CloudFront prefix list, nginx rejects requests without the distribution-specific header, and code-server listens on loopback only | Put a real identity provider in front of it. Treat the URL as a short-lived secret and delete the stack when you finish |
| The Backstage portal uses guest sign-in and has no viewer authentication on its CloudFront distribution: anyone with the URL is signed in. Its proxy calls Argo CD with a server-side token, and its assistant invokes Amazon Bedrock | Participants reach the portal in one click, without an identity provider to configure | Configure a real Backstage auth provider, or put the IDE's CloudFront Function token pattern in front of the portal. Treat the URL as a secret and delete the stack when you finish, because every visitor can drive Argo CD and spend Bedrock tokens |
The IDE instance role has AdministratorAccess |
The labs create and adopt resources across many services from the IDE terminal | Anyone holding the IdeUrl holds admin in the account. Scope the role to what your exercises need |
| The CNOE ingress uses a self-signed certificate, and the in-cluster Gitea a fixed admin password set by the IDE bootstrap | No domain and no ACM-issued certificate exist in a workshop account, and the Gitea mirror must be ready before the lab | Use a real domain and certificate, and generate the Gitea credentials into Secrets Manager |
| Platform controllers (ACK, Crossplane) hold broad IAM via Pod Identity | The labs provision across many services, so a narrow policy would break them mid-exercise | Scope one role per controller to the resources it actually manages. This is the first thing to change |
| The three VPCs are peered in a full mesh | So the platform cluster provisions into a genuinely remote cluster, instead of faking it locally | Production segments by blast radius. A mesh means any cluster reaches any other |
The ACK lab commits Kubernetes Secret manifests with placeholder values |
A lab needs credentials to exist before the application starts, and a placeholder makes the shape visible | Move them to Secrets Manager or External Secrets. A Secret in git is base64, not encryption |
| The ACK lab creates a public static-site S3 bucket | The lab needs a reachable frontend to show the provisioning worked end to end | Confirm account-level Block Public Access, and front the bucket with CloudFront and OAC |
| One NAT Gateway per VPC, not per AZ | A cost choice for a short session | One per Availability Zone, or you have coupled every private subnet to a single AZ |
None of this is a substitute for your own review. Run your own scanners and threat model against anything you carry from here into an account that matters.
If you discover a potential security issue, follow the process in CONTRIBUTING.md rather than opening a public issue.
Licensed under the MIT-0 licence (MIT No Attribution). See LICENSE and NOTICE.
Everything in this repository is original to it. No third-party code is bundled.


