Operational procedures for DevSecOps-Project02: deploy, promote, roll back, tear down, rotate secrets, and handle on-call. Pair with
troubleshooting.mdfor symptom-driven fixes.
Internet → ALB (public subnets, :80)
→ Auto Scaling Group (private subnets, Spring Boot :8080)
→ RDS PostgreSQL 16 (isolated subnets, :5432)
State: S3 (devsecops-p02-remote-state-storage-99) + native S3 locking, KMS-encrypted
Auth: GitHub OIDC → devsecops-p02-github-actions-role (no static keys)
| Environment | State key | Multi-AZ | Deployed by |
|---|---|---|---|
staging |
staging/ (CI overrides to ephemeral/) |
yes | CI pipeline on PR |
dev |
dev/ |
no | Developers, on demand |
bootstrap |
bootstrap/ |
n/a | One-time, manual |
Only run if the state bucket / OIDC role do not yet exist. Uses local state.
cd terraform/bootstrap
terraform init
terraform apply # creates state S3 bucket, KMS key, GitHub OIDC role
terraform output # note github_actions_role_arn + s3_bucket_nameThe state_bucket has prevent_destroy = true — deleting it is a deliberate,
multi-step operation, never routine.
The pipeline uses the two-phase staging bucket pattern because runners have no network path into the private subnets:
cd terraform/environments/staging
terraform init -reconfigure -backend-config="key=ephemeral/terraform.tfstate"
terraform validate
# Phase 1 — create ONLY the artifact bucket
terraform apply -target=module.compute.aws_s3_bucket.app_deploy \
-auto-approve -var="db_username=postgres"
# Stage artifacts
BUCKET=$(terraform output -raw app_deploy_bucket_name)
# Paths are relative to terraform/environments/staging (3 levels below repo root)
aws s3 cp ../../../target/headless-ecommerce-api-1.0.0.jar s3://$BUCKET/app.jar
aws s3 cp <staging Dockerfile> s3://$BUCKET/Dockerfile
aws s3 cp ../../../docker-compose.yml s3://$BUCKET/docker-compose.yml
# Phase 2 — apply the full stack; instances pull artifacts on boot
terraform apply -auto-approve -var="db_username=postgres"
# Grab the entrypoint
echo "http://$(terraform output -raw alb_dns_name)"ALB=$(terraform output -raw alb_dns_name)
curl -fsS "http://$ALB/actuator/health" # expect {"status":"UP"}Give the ASG instances a few minutes: user_data installs Docker, pulls the JAR,
writes /app/.env, and runs docker compose up before the target turns healthy.
cd terraform/environments/dev
terraform init
terraform applyThe stack is immutable and artifact-driven — "rollback" = re-point at the previous known-good artifact and refresh instances.
# Re-stage the previous good JAR under the same key
aws s3 cp app-PREVIOUS.jar s3://$BUCKET/app.jar
# Force the ASG to replace instances (they re-pull on boot)
aws autoscaling start-instance-refresh \
--auto-scaling-group-name "$(terraform output -raw autoscaling_group_name 2>/dev/null || echo devsecops-p02-asg)"For an infra rollback, git revert the offending Terraform change and re-run the
pipeline (or terraform apply locally). Because state is remote and versioned, the
previous configuration re-converges cleanly.
# staging (ephemeral copy CI uses)
cd terraform/environments/staging
terraform init -reconfigure -backend-config="key=ephemeral/terraform.tfstate"
terraform destroy -auto-approve -var="db_username=postgres"
# dev
cd terraform/environments/dev && terraform destroyforce_destroy = true on the artifact and flow-log buckets and
skip_final_snapshot = true on RDS make teardown clean and cost-free. Never run
destroy against bootstrap.
The password is a random_password mirrored to SSM
(/config/ecommerce-api/spring.datasource.password).
cd terraform/environments/staging
terraform apply -replace="module.database.random_password.db_password" \
-var="db_username=postgres"
# RDS master password + SSM SecureString update together; refresh instances so the
# app re-reads the new credential.- Is it up?
curl http://$ALB/actuator/health. - Are targets healthy? EC2 → Target Groups →
devsecops-p02-app-tg. - Did the last deploy pass? Check the PR's pipeline run +
secure-validation-gate. - App logs: SSM Session Manager onto an instance →
docker compose logsand/var/log/user-data.log. - Network: VPC flow logs bucket (
devsecops-p02-vpc-flow-logs-99) for REJECTs.
| Symptom | See |
|---|---|
| Targets unhealthy / 502 from ALB | troubleshooting |
| DB connection refused | troubleshooting |
| Pipeline red on Checkov/Semgrep/Trivy/ZAP | troubleshooting |
| Terraform state lock stuck | troubleshooting |
Include: environment, ALB URL, failing job/run URL, last terraform apply output,
and /var/log/user-data.log from an affected instance. Roll back (§3) first if a
recent deploy is the likely cause; investigate second.
| Cadence | Task |
|---|---|
| Per PR | Pipeline runs all security gates automatically |
| Weekly | Review Trivy/Semgrep findings trend; bump pinned dep versions in pom.xml |
| Monthly | Review Checkov skip_check list — retire any no-longer-needed skips |
| Quarterly | Rotate DB secret (§5); review IAM policies for least privilege |