Golden Path: From Template to Running Service¶
The golden path is the single, opinionated workflow every developer follows to ship a service. No decisions about CI, container registries, deployment targets, or monitoring — the platform handles those.
The Journey (end-to-end)¶
Local (Kind)¶
1 2 3 4 5 6 7 8 9 10 11 | |
AWS (planned)¶
1 2 3 4 5 6 7 8 9 10 11 12 | |
Step-by-Step for Developers¶
1. Pick a template in Backstage¶
Open the Backstage portal and click Create → select one of:
Service templates
| Template | Language | Default Port |
|---|---|---|
| Node.js Service | Express | 3000 |
| Python FastAPI Service | FastAPI + uvicorn | 8000 |
| Go Service | Go | 8080 |
| Ruby Sinatra Service | Ruby / Sinatra | 4567 |
| JVM (Java/Spring Boot) Service | Java 21 / Spring Boot / Gradle | 8080 |
| LLM App (Python + Langfuse) | FastAPI + Claude, tracing pre-wired | 8000 |
Fill in name, description, owner, and GitHub repo. Click Create.
Backstage will:
- Fetch the skeleton and render it with your values
- Publish the repo to GitHub (tagging it with the idp / idp-app topics)
- Register the component and API in the catalog
How a scaffolded repo stays in the catalog¶
Registration is not a one-off write. catalog.providers.github.idpOrg (in app-config.yaml,
with an AWS counterpart in app-config.aws.yaml) polls the GitHub org every 15 minutes and
ingests the catalog-info.yaml from the main branch of every repo carrying the idp or
idp-app topic. Scaffolded repos therefore appear on their own, and edits to a repo's
catalog-info.yaml flow back into Backstage without touching this repo.
Two consequences worth knowing:
- Remove the topic and the service leaves the catalog — and, because the DORA exporter cross-checks the catalog, it drops off the DORA dashboards too (see DORA & FinOps § Which services appear).
- A repo that isn't discovered can still be registered by hand via a
catalog.locationsURL entry, which is how this repo's own hand-written services are listed.
Per-service infra (Crossplane, AWS)
| Template | Resource | Notes |
|---|---|---|
| S3 Object Bucket (Crossplane) | S3 bucket with encryption + public-access block | Versioning configurable |
| RDS Postgres (Crossplane) | RDS instance + connection Secret | 30-day backup retention default; backupRetentionDays param |
| Kafka Topic (Crossplane) | MSK topic on an existing cluster | Requires MSK cluster ARN; form validates arn:aws:kafka: prefix |
| DynamoDB Table (Crossplane) | DynamoDB table with PITR | Optional rangeKey + rangeKeyType for composite primary keys |
| SQS Queue (Crossplane) | SQS queue (FIFO opt-in) | SSE always enabled |
Each template opens a PR with two files:
- services/<ownerService>/claims/<name>.yaml — the Crossplane Claim
- services/<ownerService>/claims/catalog-info-<name>.yaml — registers the resource in the Backstage catalog
ArgoCD syncs on merge, Crossplane provisions the AWS resource — no terraform apply step.
See crossplane.md for the full flow.
The legacy Terraform-PR templates (
s3-bucket,rds-database,kafka-topic) still exist for callers mid-migration. Pick the Crossplane variant for new resources.
2. Clone your new repo¶
1 2 | |
3. Implement your service¶
The skeleton contains:
1 2 3 4 5 6 7 8 9 10 | |
The skeleton carries only the local values file. The AWS values
(helm-values-aws.yaml) come from the template's platform-values/ directory and
land in the GitOps pull request the scaffolder opens, not in your service repo's
initial commit. There is no bare helm-values.yaml in a scaffolded repo — that
filename only appears in services generated by the idp CLI's local fallback.
Note that helm-values-aws.yaml ships with ingress.enabled: false. Scaffolded
services get no ALB by default; opt in explicitly if the service needs public
ingress.
Optional parameters have real defaults. The scaffolder's "Infrastructure" parameter page (port, cost centre) can be skipped — templates apply
port | default(8080)andcostCenter | default("eng-platform")at the step level. Skipping it used to emit a bareEXPOSEwith no port and break the new repository's first CI run; that is fixed, so you only need that page when the service genuinely differs from the defaults.
Conventions (enforced by the platform):
| Convention | Value |
|---|---|
| Liveness path | GET /healthz → 200 {"status":"ok"} |
| Readiness path | GET /ready → 200 {"status":"ready"} |
| Metrics path | GET /metrics → Prometheus text format |
| Logs | Structured JSON to stdout |
| Namespace | services |
4. Push to trigger CI¶
1 2 3 | |
GitHub Actions (test job on ubuntu-latest) will:
1. Install dependencies (npm install / pip install)
2. Run tests (passes if no tests exist yet)
3. Build the Docker image (smoke check)
4. Start the container and curl /healthz and /ready
5. Deploy to local Kind¶
1 2 3 | |
Then in Backstage → Create → Deploy Service to local Kind cluster:
- Pick the service from the catalog
- Set image tag (latest)
- Click Create
Or via CLI:
1 2 3 4 5 | |
Access the service at http://<name>.idp.local (add to /etc/hosts if needed).
6. Deploy to AWS (when ready)¶
Add repo secrets:
| Secret | Value |
|---|---|
AWS_ROLE_ARN |
terraform output github_actions_role_arn |
AWS_REGION |
us-east-1 |
ECR_REGISTRY |
<account>.dkr.ecr.us-east-1.amazonaws.com |
EKS_CLUSTER |
idp-mvp |
Then re-add the deploy job to .github/workflows/build-and-deploy.yml (see docs/getting-started.md).
7. Monitor your service¶
- Local: Grafana → http://grafana.idp.local (admin/admin)
- AWS: CloudWatch → Log Groups →
/aws/containerinsights/idp-mvp/application - Metrics: Grafana → IDP Services dashboard
Progressive delivery¶
Every service deploys through the same chart, which renders either a plain
Deployment or an Argo Rollout. Opt in with the Enable Progressive Delivery
scaffolder template — it opens a PR adding
services/<name>/helm-values-rollout-<env>.yaml, which the ArgoCD ApplicationSets
already list as an optional second values file. Nothing has to be hand-merged.
Which strategy¶
| Canary | Blue-green | |
|---|---|---|
| Traffic | Shifts gradually (20% → 50% → 100% by default) | All at once, on promotion |
| Extra capacity | One canary ReplicaSet, sized to the step weight | A full second stack |
| Analysis | Runs at each step; failure aborts and rolls back | Runs before promotion |
| Rollback | Abort mid-rollout; blast radius limited to the canary weight | Instant — active Service points back at the old ReplicaSet |
| Good for | Stateless HTTP services with useful per-request metrics | Session-affine or stateful services, expensive analysis, anything where you want to verify the new version end to end before any user sees it |
Blue-green doubles the pod count during promotion. On a single-node local cluster
set rollout.blueGreen.previewReplicaCount: 1, or the preview stack will evict
platform components.
Blue-green also keeps a human in the loop by default
(autoPromotionEnabled: false): the new version comes up fully and is reachable
on the -preview Service, and you promote with
1 | |
Rollback thresholds¶
rollout.analysis.errorRateThreshold and latencyThresholdSeconds are passed to
the cluster-scoped http-error-rate AnalysisTemplate as arguments, so the
thresholds are per-service while the PromQL stays in one place. They are
fractions, not percentages — 0.01 is 1%. The scaffolder form takes a percent
and converts, so this only matters when hand-editing.
The chart ships a values.schema.json covering the rollout block, so a typo like
strategy: blue-green fails at helm lint rather than producing a Rollout with
no strategy — which Argo accepts and then never progresses.
Conventions Reference¶
| Convention | Local | AWS |
|---|---|---|
| Namespace (legacy services) | services |
services |
| Namespace (team services) | team-<name> |
team-<name> |
| Service values path (legacy) | services/<name>/helm-values-local.yaml |
services/<name>/helm-values-aws.yaml |
| Service values path (team) | teams/<name>/services/<svc>/helm-values-local.yaml |
teams/<name>/services/<svc>/helm-values-aws.yaml |
| Image registry | localhost:5003/<name> |
<account>.dkr.ecr.<region>.amazonaws.com/idp-mvp/<name> |
| Image tag | latest (local push) |
<git-sha-short> |
| Ingress class | nginx |
alb (but ingress.enabled: false by default — scaffolded services get no ALB unless opted in) |
| Replicas | 1 | 2 |
| CPU request | 50m | 100m |
| Memory request | 32Mi | 128Mi |
| Team label (K8s) | team: <name> |
team: <name> |
| Team tag (AWS) | idp:team=<name> (Kyverno auto-injects on Crossplane claims) |
same |
| Secret path | n/a (local uses ConfigMaps) | /<teamName>/<secret-name> in Secrets Manager |
CLI Alternative (without Backstage)¶
The CLI scaffolder covers the three base runtimes —
nodejs,python,go. The specialist templates (Ruby, JVM, React, LLM App, MCP server, KAgent agent) are Backstage-only by design: they carry skeletons the CLI's local generator does not duplicate, which keeps the two implementations from drifting.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
CLI: Scaffold a test suite¶
The idp scaffold test-suite command generates a ready-to-run QA suite alongside your service. It supports 16 test types covering every pyramid layer.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Generated suites land in test-suites/<name>/ and are auto-registered in the Backstage catalog via catalog-info.yaml.
See CLI Reference for all 16 test suite types and their flags.