Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions app.yaml
Original file line number Diff line number Diff line change
@@ -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 <name>` (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.
132 changes: 132 additions & 0 deletions docs/platform.md
Original file line number Diff line number Diff line change
@@ -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 `<team>-<name>`, 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-<commit>` 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/<name>`. 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.
8 changes: 8 additions & 0 deletions terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Terraform working files — never commit these
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfplan
override.tf
override.tf.json
10 changes: 10 additions & 0 deletions terraform/backend.tf
Original file line number Diff line number Diff line change
@@ -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
}
}
169 changes: 169 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -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"
}
Loading
Loading