diff --git a/README.md b/README.md index a2b0232..f38164a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ Contains both a public API that can be consumed by anyone, and a private API use Aiming to replace [Sleepingpill](https://github.com/javaBin/sleepingPillCore) +# Deployment + +moresleep is moving onto the javaBin platform: `app.yaml` describes the infrastructure and +`.github/workflows/deploy.yml` builds, tests and deploys on merge to `master`. +See [docs/platform.md](docs/platform.md). + # Setup ## Database diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..80d4922 --- /dev/null +++ b/app.yaml @@ -0,0 +1,52 @@ +# javaBin platform app definition. The platform pipeline turns this file into +# Terraform (terraform/, generated, do not edit) and deploys it. +# Guide: docs/platform.md. Field reference: +# https://github.com/javaBin/platform/blob/main/docs/app-yaml-reference.md +name: moresleep +team: moresleep + +compute: + cpu: 256 # same size as the old CDK service (0.25 vCPU) + memory: 512 # MB + port: 5000 # SERVER_PORT below + desired_count: 0 # no tasks until the secrets are set (raised to 2 in a later PR) + health_check: / # serves webapp/index.html + +# Parallel run on a new hostname first. sleepingpill.javazone.no keeps pointing +# at the old stack until the cutover PR. +routing: + host: moresleep.javazone.no + priority: 120 # ALB listener rule priority, unique on the platform ALB + +resources: + databases: + # The existing database. Imported into Terraform state, never recreated: + # the platform only adds tags and turns on deletion protection. + - name: main + engine: postgres + adopt: dbmoresleep + host_env: DBHOST + port_env: DBPORT + deletion_protection: true + # SSM SecureStrings, created with the value PLACEHOLDER. Set real values with + # `javabin secret set ` (see docs/platform.md). + secrets: + - name: db-name + env: DATASOURCENAME + - name: db-user + env: DBUSER + - name: db-password + env: DBPASSWORD + - name: allaccess-user + env: ALLACCESS_USER + - name: read-user + env: READ_USER + +# Plain (non-secret) configuration, read by Setup.kt like before. +environment: + SERVER_PORT: "5000" + RUN_FROM_JAR: "true" + DATABASE_TYPE: POSTGRES + DO_FLYWAY_MIGRATION: "false" # production schema is migrated deliberately, as today + LOAD_FROM_SLEEPINGPILL: "false" # the old sleepingPillCore import stays off + ALL_OPEN_MODE: "false" # the code default is "true" (no auth). Must stay explicit. diff --git a/docs/platform.md b/docs/platform.md new file mode 100644 index 0000000..caf1ff1 --- /dev/null +++ b/docs/platform.md @@ -0,0 +1,132 @@ +# moresleep on the javaBin platform + +This guide explains how moresleep runs on the javaBin platform. It is written for whoever maintains +moresleep and wants to know what happens to their code after a merge. + +**What does not change:** the database (`dbmoresleep`, same data, same credentials), the public +hostname `https://sleepingpill.javazone.no`, the Basic-auth model, and the env-var configuration +read by `Setup.kt`. No consumer (submit, cake-redux, cupcake, the JavaZone web sites, ...) needs to +change anything. + +**What changes:** the code is built and deployed by a shared pipeline instead of `cdk deploy`. The +container runs in the platform's ECS cluster behind the platform's load balancer, and secrets come +from SSM Parameter Store instead of a SOPS file baked into the image. + +--- + +## 1. The big picture + +``` + app.yaml ──(expand)──► terraform/ ──(plan + review)──► AWS resources + │ ECR repo, target group, ALB rule, + │ DNS record, ECS service, IAM role, + │ SSM secrets, alarms, + adopted RDS + Dockerfile ──(docker build)──► ECR image ──(ecs deploy)──► 2 Fargate tasks on port 5000 + pom.xml ──(mvn verify against postgres:17)──► tests must pass before anything deploys +``` + +You describe *what* the app needs in **`app.yaml`**. The platform turns that into Terraform, plans +it, has the plan reviewed, applies it, builds the Docker image and rolls it out. There is no +`cdk deploy` and no AWS credentials in the repo. GitHub Actions gets short-lived credentials +through OIDC, scoped to the `moresleep` team. + +The platform's own docs live in [javaBin/platform/docs](https://github.com/javaBin/platform/tree/main/docs). +The most useful ones are [app-yaml-reference.md](https://github.com/javaBin/platform/blob/main/docs/app-yaml-reference.md) +and [reusable-workflows.md](https://github.com/javaBin/platform/blob/main/docs/reusable-workflows.md). + +## 2. app.yaml, field by field + +| Section | What it creates | moresleep value | +|---|---|---| +| `name`, `team` | Resource names are `-`, here `moresleep-moresleep`. `team` must be a team in [javaBin/registry](https://github.com/javaBin/registry), and the repo must belong to that GitHub team. | `moresleep` / `moresleep` | +| `compute` | ECS Fargate task definition + service (x86, private subnets, read-only root filesystem with a writable `/tmp`, runs as uid 1000) | 256 CPU, 512 MB, port 5000, health check `/` | +| `routing` | ALB target group, a listener rule on the shared HTTPS load balancer (`priority` must be unique), and a Route53 A record. The wildcard certificate already covers `*.javazone.no`. | `moresleep.javazone.no` during the parallel run, `sleepingpill.javazone.no` after the cutover | +| `resources.databases` | Normally a *new* RDS instance. `adopt:` instead **imports the existing one**: Terraform takes it into its state, adds tags and turns on deletion protection, and changes nothing else (class, storage, network, security group, password all stay). | `adopt: dbmoresleep`, with `DBHOST`/`DBPORT` injected from the instance | +| `resources.secrets` | One SSM SecureString per entry, injected as an env var when a task starts. Created with the value `PLACEHOLDER`. | DB name/user/password, ALLACCESS_USER, READ_USER | +| `environment` | Plain env vars | the non-secret settings that used to be in the SOPS file | + +The generated Terraform lands in `terraform/`. Do not edit it by hand: it is regenerated from +`app.yaml` on every run, and a pull request shows the regenerated files in its diff. You may add your +own extra `.tf` files next to the generated ones; the generator only overwrites its own files. + +## 3. The pipeline (`.github/workflows/deploy.yml`) + +`deploy.yml` is a few lines that call the shared workflow `javaBin/platform/.github/workflows/javabin.yml@main`. +It detects what is in the repo and runs: + +| Job | On a pull request | On `master` (merge or manual run) | +|---|---|---| +| **build-jvm** | `mvn verify` with a throwaway `postgres:17` container on `localhost:5432` (db `moresleepunit`, user `localdevuser`), the same setup as `docker/postgres` | same | +| **expand** | runs the generator; if `terraform/` changed, it commits the result to the PR branch | same | +| **tf-plan** | `terraform plan`. An AI reviewer rates the risk, and the plan and review are posted as a PR comment | same; the plan is stored for apply | +| **tf-apply** | — | applies exactly the reviewed plan (a HIGH risk rating blocks it until a platform owner approves an override) | +| **docker-build** | — | builds `Dockerfile`, pushes to ECR `moresleep-moresleep` with tags `sha-` and `latest` | +| **ecs-deploy** | — | rolls the ECS service onto the new image and waits until it is stable | + +So: **open a PR to see what would change, merge to deploy.** Nothing touches AWS from a PR. + +## 4. Secrets + +Secrets live in SSM Parameter Store under `/javabin/apps/moresleep/moresleep/`. To set or change +one: + +```bash +brew install javaBin/tap/javabin # once +aws sso login --profile javabin # your SSO user, permission set javabin-team-moresleep +cd moresleep +javabin secret list # shows which ones still hold PLACEHOLDER +javabin secret set db-password # prompts without echo +``` + +The platform restarts the service within about a minute of a secret changing, so the tasks pick up +the new value. Terraform never overwrites a value you have set. + +For the migration, the values come from today's SOPS file (`sops -d conf/.enc.env`): + +| SOPS variable | platform secret | +|---|---| +| `DATASOURCENAME` | `db-name` | +| `DBUSER` | `db-user` | +| `DBPASSWORD` | `db-password` | +| `ALLACCESS_USER` | `allaccess-user` | +| `READ_USER` | `read-user` | + +`DBHOST` is injected from the adopted instance. The other SOPS values (`SERVER_PORT`, `RUN_FROM_JAR`, +`ALL_OPEN_MODE`, `LOAD_FROM_SLEEPINGPILL`) are plain `environment:` entries in `app.yaml`. Check them +against the decrypted file before the cutover. `SLEEPINGPILL_AUTH` is only used to import from the +retired sleepingPillCore, so it is dropped. + +## 5. Day-to-day + +| I want to... | Do this | +|---|---| +| deploy a code change | merge to `master` | +| see logs | CloudWatch log group `/ecs/moresleep/moresleep` (AWS console via [aws.javazone.no](https://aws.javazone.no), or `aws logs tail /ecs/moresleep/moresleep --follow`) | +| see status and cost | `javabin status` | +| scale | change `compute.desired_count` / `cpu` / `memory` in `app.yaml`, open a PR, merge | +| add an env var | `environment:` in `app.yaml` (plain) or `resources.secrets` (secret) | +| roll back code | revert the commit on `master`; the pipeline deploys the previous code | +| run a schema migration | as today: `DO_FLYWAY_MIGRATION` stays `false` in production. Run migrations deliberately, e.g. by setting it to `"true"` for one deploy | + +Alarms (CPU, memory, 5xx, unhealthy targets) go to the platform alert channel. +The team's monthly budget (500 NOK) is set in [javaBin/registry](https://github.com/javaBin/registry) `teams/moresleep.yaml`. + +## 6. Network and database + +Platform tasks run in private subnets of the platform VPC and reach the internet through its NAT +gateway. `dbmoresleep` stays where it is (default VPC, public endpoint, TLS forced by the +`default.postgres17` parameter group), so the tasks connect to it the same way other clients do. +Nothing about the database moves. Because both the old stack and the platform use the same database, +the parallel run and the cutover need no data copy and no write freeze. + +## 7. Migration steps + +Each step is one pull request in this stack. Merge them in order, and only when the step's +precondition holds: + +1. `chore: make master match the deployed awsdocker line`. Production runs `awsdocker`, so master must contain it first. +2. `docs(platform): add app.yaml and a platform guide`. This file. It changes nothing on its own. +3. `ci: deploy with the javaBin platform pipeline`. The first apply imports the DB and creates the service with 0 tasks. +4. `feat(platform): run two tasks on moresleep.javazone.no`. Merge after the secrets are set. +5. `feat(platform): serve sleepingpill.javazone.no from the platform`. The cutover, after the parallel run checks out. +6. `chore: remove CDK, Elastic Beanstalk and SOPS deploy bits`. Merge 14 days after the cutover, once the old stack is deleted. diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000..85f3574 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,8 @@ +# Terraform working files — never commit these +.terraform/ +.terraform.lock.hcl +*.tfstate +*.tfstate.backup +*.tfplan +override.tf +override.tf.json diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 0000000..5963b5e --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,10 @@ +# GENERATED FROM app.yaml — do not edit, changes will be overwritten +terraform { + backend "s3" { + bucket = "javabin-terraform-state-553637109631" + key = "apps/moresleep/moresleep/terraform.tfstate" + region = "eu-central-1" + dynamodb_table = "javabin-terraform-app-locks" + encrypt = true + } +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..bc2f13b --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,169 @@ +# GENERATED FROM app.yaml — do not edit, changes will be overwritten +module "platform" { + source = "git::https://github.com/javaBin/platform//terraform/modules/platform-data?ref=main" + + project = "javabin" + domain = "javazone.no" + team = "moresleep" +} + +module "ecr" { + source = "git::https://github.com/javaBin/platform//terraform/modules/ecr-repo?ref=main" + + name = "moresleep" + team = "moresleep" +} + +module "routing" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-routing?ref=main" + + name = "moresleep" + team = "moresleep" + vpc_id = module.platform.vpc_id + port = 5000 + health_check_path = "/" + health_check_matcher = "200" + https_listener_arn = module.platform.https_listener_arn + listener_rule_priority = 120 + host_header = "moresleep.javazone.no" + route53_zone_id = module.platform.route53_zone_ids["javazone.no"] + alb_dns_name = module.platform.alb_dns_name + alb_zone_id = module.platform.alb_zone_id + deregistration_delay = 30 +} + +module "rds_main" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-rds-adopted?ref=main" + + identifier = "dbmoresleep" + deletion_protection = true +} + +module "secret_db-name" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-secret?ref=main" + + name = "db-name" + team = "moresleep" + project = "javabin" + service = "moresleep" + description = "" +} + +module "secret_db-user" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-secret?ref=main" + + name = "db-user" + team = "moresleep" + project = "javabin" + service = "moresleep" + description = "" +} + +module "secret_db-password" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-secret?ref=main" + + name = "db-password" + team = "moresleep" + project = "javabin" + service = "moresleep" + description = "" +} + +module "secret_allaccess-user" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-secret?ref=main" + + name = "allaccess-user" + team = "moresleep" + project = "javabin" + service = "moresleep" + description = "" +} + +module "secret_read-user" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-secret?ref=main" + + name = "read-user" + team = "moresleep" + project = "javabin" + service = "moresleep" + description = "" +} + +module "task_role" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-role?ref=main" + + name = "moresleep" + team = "moresleep" + region = "eu-central-1" + aws_account_id = "553637109631" + permissions_boundary_arn = module.platform.developer_boundary_arn + additional_policy_jsons = { + secret_db-name = module.secret_db-name.access_policy_json + secret_db-user = module.secret_db-user.access_policy_json + secret_db-password = module.secret_db-password.access_policy_json + secret_allaccess-user = module.secret_allaccess-user.access_policy_json + secret_read-user = module.secret_read-user.access_policy_json + } +} + +module "service" { + source = "git::https://github.com/javaBin/platform//terraform/modules/ecs-service?ref=main" + + name = "moresleep" + team = "moresleep" + cluster_id = module.platform.ecs_cluster_id + image = module.ecr.repository_url + cpu = 256 + memory = 512 + port = 5000 + desired_count = 0 + execution_role_arn = module.platform.execution_role_arn + task_role_arn = module.task_role.role_arn + subnet_ids = module.platform.private_subnet_ids + security_group_ids = [module.platform.ecs_tasks_security_group_id] + region = "eu-central-1" + container_user = "1000" + readonly_root_filesystem = true + target_group_arn = module.routing.target_group_arn + environment = { + SERVER_PORT = "5000" + RUN_FROM_JAR = "true" + DATABASE_TYPE = "POSTGRES" + DO_FLYWAY_MIGRATION = "false" + LOAD_FROM_SLEEPINGPILL = "false" + ALL_OPEN_MODE = "false" + DBHOST = module.rds_main.address + DBPORT = module.rds_main.port + } + secrets = { + DATASOURCENAME = module.secret_db-name.parameter_arn + DBUSER = module.secret_db-user.parameter_arn + DBPASSWORD = module.secret_db-password.parameter_arn + ALLACCESS_USER = module.secret_allaccess-user.parameter_arn + READ_USER = module.secret_read-user.parameter_arn + } +} + +module "alarms" { + source = "git::https://github.com/javaBin/platform//terraform/modules/service-alarm?ref=main" + + name = "moresleep" + team = "moresleep" + service = "moresleep" + cluster_name = module.platform.ecs_cluster_name + sns_topic_arns = [data.aws_sns_topic.alerts.arn] + cpu_threshold = 80 + memory_threshold = 80 + error_5xx_threshold = 10 + target_group_arn_suffix = module.routing.target_group_arn_suffix + alb_arn_suffix = module.platform.alb_arn_suffix +} + +data "aws_sns_topic" "alerts" { + name = "javabin-alerts" +} + +import { + to = module.rds_main.aws_db_instance.this + id = "dbmoresleep" +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..37563c0 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,20 @@ +# GENERATED FROM app.yaml — do not edit, changes will be overwritten +output "service_url" { + value = "https://moresleep.javazone.no" +} + +output "ecr_url" { + value = module.ecr.repository_url +} + +output "service_name" { + value = module.service.service_name +} + +output "task_role_arn" { + value = module.task_role.role_arn +} + +output "log_group" { + value = module.service.log_group_name +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..29229a7 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,25 @@ +# GENERATED FROM app.yaml — do not edit, changes will be overwritten +terraform { + required_version = ">= 1.5" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = "eu-central-1" + + default_tags { + tags = { + team = "moresleep" + service = "moresleep" + repo = "javaBin/moresleep" + environment = "production" + managed-by = "terraform" + } + } +}