Skip to main contentUkasyah Nata Dewastoro
Cloud

Stage 2: Three environments and a pipeline that guards production

The product has users now, so changes need a path to production that cannot skip a check. Dev, staging and prod on AWS, a tag-only release pipeline with no static keys, and the monitoring that caught a $5,000-a-month mistake before the invoice did.

$111.52/mo saved
Role
Team of three; I owned production, alerts and security
Scale
3 environments, 1 VPC
Stack
CI/CD pipelines, AWS OIDC, SSM
Targets
Tag-only prod deploys, no static AWS keys, CIS Level 1

Context

Stage 1 got the product online. Stage 2 is what a startup faces once people depend on it and the team grows: more than one engineer changing things, a staging environment that has to match production, and an audit question about who can touch prod.

The team was three engineers. I owned production, alerting and security: the release pipeline's production path, the monitoring stack, and the CIS hardening of the hosts.

Constraints:

  • Three people, three environments, one VPC
  • No long-lived AWS keys in CI
  • Hosts hardened to CIS Level 1

Architecture and failover

Dev, staging and prod each have a web tier and a data tier inside one VPC. Production data moved to RDS, with credentials in Secrets Manager. Internal tools are reachable only over VPN.

The CI pipeline runs on every pull request: lint, tests, a Go vulnerability check, gitleaks for secrets, and a build. Merging to main must pass a blocking SonarQube gate. Production only deploys from a version tag. The pipeline assumes an AWS role through OIDC and runs the deploy on the servers through SSM, so nothing in CI holds a key that works on its own. Release, rollback and database restore each have their own pipeline.

Grafana Alloy on each host collects metrics, logs and traces. After the cardinality incident, the team moved from a hosted backend to a self-hosted Mimir, Loki and Tempo stack, with the label leak fixed in Alloy.

Path from a pull request to production
  1. 1
    GatePull request checks

    Lint, tests, Go vulnerability check, gitleaks secret scan, build.

    If it fails: The pull request cannot merge.

  2. 2
    Merge to main
  3. 3
    GateSonarQube quality gate

    If it fails: Main is blocked until the code is fixed.

  4. 4
    Push a version tag

    Only a v*.*.* tag starts the production pipeline.

  5. 5
    Assume an AWS role through OIDC

    Short-lived credentials for this run only.

  6. 6
    DeployDeploy through SSM

    Commands run on the servers; no SSH, no open port 22.

  7. 7
    GateHealth checks

    If it fails: Run the rollback pipeline to the previous tag.

Try it: replay the bot scan, or ship a tagged release. Times are approximate.

Three environments, one delivery pipeline, and a monitoring bill topologyThe CI pipeline tests every pull request and deploys over SSM using OIDC, with no static AWS keys. Dev, staging and prod each have a web tier and a data tier; prod data is on RDS. Grafana Alloy ships metrics, logs and traces to an LGTM stack.AWS: dev, staging, prodDelivery and monitoringmetricsalertsdeployWeb tierper environmentApp serversprodInternet trafficusers and botsGrafana Alloyscrapes metricsRDS Postgresprod dataCI pipelineOIDC + SSM deployMetrics backend~100k active seriesOn-call engineerowns prod and alerts
  • Primary / serving
  • Recovering
  • Degraded
  • Down
  • Replication / control

Pick a failure to see how the system detects and recovers from it.

Key decisions

OIDC and SSM instead of SSH keys in CI

  • security
  • CI/CD

The first pipeline deployed over SSH through a jump host, with long-lived keys stored in CI.

  • SSH with stored keysSimple, but a leaked CI variable is a working key to production.
  • OIDC role per pipeline, deploy through SSMChosenThe pipeline gets short-lived credentials per run, and no server needs port 22 open.

Trade-off: More IAM to set up, and debugging a deploy means reading SSM command output instead of a shell session.

Lifecycle

  1. Open a pull request. CI lints, tests, scans dependencies and secrets, and builds.
  2. Merge to main once the SonarQube gate passes.
  3. Push a version tag. The production pipeline assumes its role through OIDC and deploys through SSM.
  4. If health checks fail, run the rollback pipeline.

Failure modes

FailureBlast radiusDetectionRecovery
Metric cardinality explosionMonitoring cost, not availabilitySeries-count alertRelabel, scrape limits
Bad releaseProductionHealth checks after deployRollback pipeline
Leaked CI variableNone for AWS: no static keys existSecret scanningRotate the affected secret
Data loss in prodProduction dataRDS monitoringDatabase restore pipeline

Results

~100k → 1M+active series during the bot scan
$111.52/mosaved by self-hosting monitoring
0 criticalSecurity Hub findings after CIS Level 1 hardening
0static AWS keys in CI

What I'd change

We saw the warning a day before we acted and dismissed it as noise. I would alert on active series count from day one, before any cost forecast moves, and give that alert an owner.

I would treat every new metric label as a schema change that needs review. The URL path should never have become a label, and a review would have caught it before a bot did.

I would also set scrape sample and label limits before the first deploy, not after the first incident. A limit turns a runaway leak into a capped one.

Next stage: the startup's own product, built on infrastructure that never gets edited by hand.