Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/detect.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ jobs:
echo "has_pnpm=$(test -f pnpm-lock.yaml && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "has_eb=$(test -d .elasticbeanstalk && echo true || echo false)" >> "$GITHUB_OUTPUT"
if [ -f app.yaml ]; then
if [ -z "$(yq '.name // ""' app.yaml)" ]; then
echo "::error file=app.yaml::name is required (e.g. name: my-app): lowercase letters, digits and hyphens, starting with a letter, max 20 chars"
exit 1
fi
echo "app_name=$(yq '.name // ""' app.yaml)" >> "$GITHUB_OUTPUT"
echo "app_team=$(yq '.team // ""' app.yaml)" >> "$GITHUB_OUTPUT"
else
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/expand-terraform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ jobs:

- name: Generate modules from app.yaml
env:
APP_SERVICE: ${{ github.event.repository.name }}
AWS_ACCOUNT_ID: ${{ inputs.aws_account_id }}
AWS_REGION: ${{ inputs.aws_region }}
TF_ROOT: ${{ inputs.tf_root }}
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/tf-apply.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,4 @@ jobs:
- name: Auth doctor
if: hashFiles('app.yaml') != ''
continue-on-error: true
env:
APP_SERVICE: ${{ github.event.repository.name }}
run: python3 .platform/scripts/auth-doctor.py --app-yaml app.yaml --wait 180
15 changes: 15 additions & 0 deletions .github/workflows/tf-plan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ jobs:
if: steps.aws.outputs.available == 'true'
run: .platform/scripts/run-plan.sh "${{ inputs.tf_root }}" -lock-timeout=5m

# ALB priorities are unique per listener and AWS only says so at
# apply (PriorityInUse): check the planned rules against the live ones.
# A conflict goes into the PR comment; the last step fails the job.
- name: Check ALB rule priorities
id: alb_priorities
if: steps.plan.outputs.has_changes == 'true'
continue-on-error: true
run: python3 .platform/scripts/check-alb-priorities.py "${{ inputs.tf_root }}"

- name: Upload plan to S3
id: upload
if: steps.plan.outputs.has_changes == 'true'
Expand Down Expand Up @@ -169,3 +178,9 @@ jobs:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: sh .platform/scripts/post-plan-comment.sh "${{ inputs.tf_root }}/plan-output.txt" "${{ steps.plan.outputs.has_changes }}"

- name: Fail on ALB rule priority conflict
if: steps.alb_priorities.outcome == 'failure'
run: |
echo "::error file=app.yaml::ALB rule priority conflict (see the Check ALB rule priorities step); apply would fail with PriorityInUse"
exit 1
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ terraform/state/
|--------|------|
| `scripts/bootstrap.sh` | One-time: create state bucket + lock tables |
| `scripts/generate-modules.py` | CI: validates app.yaml, generates fmt-clean module blocks into terraform/ (tests: `scripts/tests/`) |
| `scripts/check-alb-priorities.py` | CI (tf-plan): fails the plan when a listener rule would take an ALB priority already in use (tests: `scripts/tests/`) |
| `scripts/registry.py` | Module sources + boilerplate templates for generate-modules.py |
| `scripts/resolve-tool-version.sh` | CI: resolve node/java version from the app repo's version files |
| `scripts/sync-registered-teams.py` | CI: registry `teams/*.yaml` → `registered-teams.auto.tfvars` (needs App token) |
Expand Down
36 changes: 22 additions & 14 deletions docs/app-yaml-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ name: moresleep
team: core
routing:
host: moresleep.javazone.no
priority: 120
```

Without `routing:` the service runs with no load balancer (a worker).
Expand Down Expand Up @@ -58,7 +57,7 @@ alarms:

### name (required)

Service name. Starts with a letter; lowercase letters, digits, and hyphens. Max 20 characters. Defaults to the repo name.
Service name. Starts with a letter; lowercase letters, digits, and hyphens. Max 20 characters. Required: there is no default from the repository name (repository names such as `2027.javazone.no` are not valid names), and the pipeline's detect step, the generator and the `javabin` CLI all fail on an `app.yaml` without one.

```yaml
name: moresleep
Expand All @@ -84,10 +83,10 @@ Container compute configuration. All fields have defaults.

```yaml
compute:
cpu: 512 # default: 512 (0.5 vCPU)
memory: 1024 # default: 1024 (1 GB)
port: 8080 # default: 8000
desired_count: 1 # default: 1 — applied by ecs-deploy, not tf-apply
cpu: 512 # default: 512 (0.5 vCPU); 256, 512, 1024, 2048, 4096, 8192 or 16384
memory: 1024 # default: 1024 (1 GB); MiB, must fit cpu (table below)
port: 8080 # default: 8000; 1-65535
desired_count: 1 # default: 1; 0 or more — applied by ecs-deploy, not tf-apply
health_check: /health # default: /health — ALB target group health check path
health_check_matcher: "200" # default: "200"
health_check_grace_period: 120 # default: 120 — seconds a new task may fail ALB health checks
Expand All @@ -102,6 +101,18 @@ compute:

Images that write outside `/tmp` (caches, uploads to local disk) need `readonly_root_filesystem: false`, or their tasks crash on start; the deployment circuit breaker then rolls the deploy back.

`cpu`, `memory`, `port` and `desired_count` must be whole numbers, and `cpu`/`memory` a [Fargate task size](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-tasks-services.html#fargate-tasks-size); anything else fails the `expand` step instead of the apply:

| cpu | memory (MiB) |
|-----|--------------|
| 256 | 512, 1024, 2048 |
| 512 | 1024-4096, steps of 1024 |
| 1024 | 2048-8192, steps of 1024 |
| 2048 | 4096-16384, steps of 1024 |
| 4096 | 8192-30720, steps of 1024 |
| 8192 | 16384-61440, steps of 4096 |
| 16384 | 32768-122880, steps of 8192 |

**`health_check_grace_period`** (0-3600, apps with `routing` only): for this long after a task starts, ECS ignores failing load balancer health checks. The target group checks every 10 s and marks a target unhealthy after 2 failures, so without it a task that takes more than about 20 s to start (JVM apps, migrations) is stopped and replaced before it is ready. Raise it for slow starters; the deploy still fails when the task never becomes healthy.

**`exec: true`** turns on [ECS Exec](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html) for the service and gives the task role the `ssmmessages` permissions it needs. It requires `readonly_root_filesystem: false`, because the agent ECS Exec runs inside the container writes to its filesystem (AWS does not support ECS Exec with a read-only root filesystem). Team members can then open a shell in their own team's tasks with the `javabin-team-{team}` SSO permission set, with the [Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) installed:
Expand All @@ -113,23 +124,22 @@ aws ecs execute-command --cluster javabin-platform --task <task-id> \

The setting applies to tasks started after the deploy that turns it on. Tasks of other teams are denied.

Valid CPU/memory combinations follow [Fargate task size constraints](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html).

### routing

ALB routing configuration.

```yaml
routing:
host: moresleep.javazone.no # required if routing is specified; also java.no / javabin.no
priority: 100 # required, unique 1-50000
priority: 120 # optional; default: derived from team and name
deregistration_delay: 30 # default: 30 (seconds)
adopt_dns: false # default: false — take over an existing record for host
```

- `host` must be exactly one label under `javazone.no`, `java.no` or `javabin.no` (e.g. `moresleep.javazone.no`, `medlem.java.no`, `styret.javabin.no`). Those are the domains with a wildcard cert on the platform ALB; deeper names like `a.b.java.no` are rejected because the wildcard cert does not cover them.
- `host` creates a Route53 A record (in the zone matching the host's domain) pointing to the platform ALB and an ALB listener rule matching the Host header.
- `priority` determines listener rule evaluation order. Must be unique across all apps. Coordinate via the registry.
- `priority` is the ALB listener rule priority, which must be unique on the shared listener. Leave it out: the generator derives a stable one from `team/name` in 1000-50000 (printed as a notice in the `expand` step), in a slot of 11 that keeps the 10 numbers below it free for `routing.login` rules. Set it only to keep an existing app's rule where it is (existing apps keep theirs, no rule is moved) or to move off a collision; explicit values must be 1-999, so they never land in the derived range.
- tf-plan checks every listener rule the plan creates or moves against the live rules on the listener (`scripts/check-alb-priorities.py`) and fails with the host that holds a taken priority and a free number to use, instead of apply failing with `PriorityInUse`. The conflict is shown at the top of the plan comment on the PR. Two derived priorities can collide (rarely); the same check catches that, and setting `priority` to the suggested number fixes it.
- `adopt_dns: true` takes over an A record for `host` that already exists (for example a hand-made record of an app moving onto the platform). It must be approved first: the host has to be listed for your team in `ADOPTIONS` in `scripts/registry.py` (a javaBin/platform PR), otherwise generation fails. Route53 records are not protected by team permissions, so this is what stops one team from taking over a hostname another app serves. Without `adopt_dns`, the apply fails because the record exists. With it, Route53 overwrites the record (UPSERT) to point at the platform ALB, and from then on Terraform manages it. The plan shows the record as a *create*. Changing `host` later replaces the record, and removing routing deletes it, as for any managed record. See [Adopting existing resources](#adopting-existing-resources).

#### routing.login — sign-in at the load balancer
Expand Down Expand Up @@ -179,7 +189,7 @@ What happens:

Rules and priorities:

- Each rule becomes its own ALB listener rule at the priorities **directly below** `routing.priority`: the first rule gets `priority - 1`, the next `priority - 2`, and so on. Leave that room free: if another app already uses one of those priorities, apply fails with `PriorityInUse`. `priority` must be larger than the number of rules. Nothing checks this across apps yet, so pick a `priority` with at least 10 free numbers below it (e.g. a multiple of 20 when registering a new app); apps whose priority is right next to another app's (e.g. 100 and 101) cannot add rules without moving.
- Each rule becomes its own ALB listener rule at the priorities **directly below** `routing.priority`: the first rule gets `priority - 1`, the next `priority - 2`, and so on. A derived priority always has that room. With an explicit `priority`, it must be larger than the number of rules, and if another app already uses one of the numbers below it, tf-plan fails naming that app's host and suggesting a free number; apps whose priority is right next to another app's (e.g. 100 and 101) cannot add rules without moving (or dropping `priority` to take a derived one, which moves the rule once).
- At most 10 rules. A path may appear only once, and a rule after a `/*` rule is an error (it could never match).
- Protect `/*` and open the exceptions with `public: true`, rather than protecting a few subpaths. The load balancer matches the raw request path: case-sensitively, without decoding `%xx` and without resolving `..`. An app or framework that does any of these can be reached past a subpath rule, e.g. `/Admin`, `/%61dmin/` or `/x/../admin/` past `/admin/*`. The generator warns when no rule covers `/*`.

Expand Down Expand Up @@ -479,12 +489,10 @@ Apps that need users from both pools should run two services for now.

### domain

Convenience alias for `routing.host`. If both are set, `routing.host` takes precedence. `routing.priority` is still required.
Convenience alias for `routing.host`. If both are set, `routing.host` takes precedence. `routing.priority` is optional, as with `routing.host`.

```yaml
domain: cake-redux.javazone.no
routing:
priority: 110
```

### environment
Expand Down
4 changes: 3 additions & 1 deletion docs/reusable-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ Checks for: `app.yaml`, `terraform/`, `Dockerfile`, `pom.xml`, `build.gradle(.kt

**Outputs:** `has_yaml`, `has_tf`, `has_docker`, `has_maven`, `has_gradle`, `has_pnpm`, `has_eb`, `app_name`, `app_team`

Fails when `app.yaml` exists without `name` (the name is never taken from the repository name).

### build-jvm.yml

Maven (`mvn verify`) or Gradle (`./gradlew build`) build + test. Uploads test reports.
Expand Down Expand Up @@ -172,7 +174,7 @@ Docker BuildKit build + push to ECR repo `{team}-{app_name}`. Uses GitHub Action

### tf-plan.yml

Plans the committed `terraform/` at the branch head (after expand), uploads the plan to S3 with its SHA256, has the `javabin-plan-review` Lambda review it (Bedrock Claude Haiku 5.5, platform credentials; it writes `risk.json`) and posts plan + review to the PR. A review that cannot run after retries is `FAILED`: re-run the workflow, or request an override if it persists.
Plans the committed `terraform/` at the branch head (after expand), uploads the plan to S3 with its SHA256, has the `javabin-plan-review` Lambda review it (Bedrock Claude Haiku 5.5, platform credentials; it writes `risk.json`) and posts plan + review to the PR. A review that cannot run after retries is `FAILED`: re-run the workflow, or request an override if it persists. `scripts/check-alb-priorities.py` checks whether a listener rule the plan creates or moves would take an ALB priority another rule already holds (read-only `describe-rules`); on a conflict the plan comment still gets posted, with the conflict at the top, and the last step fails the job.

**AWS role:** `javabin-ci-app-broker` (OIDC) → broker Lambda → `javabin-ci-plan-{team}` (read-only: refresh, state read, state lock, plan upload; tf-plan runs on any branch, so it cannot change resources); the review step uses the broker OIDC role to invoke `javabin-plan-review`
Repos that bring their own `terraform/` without `backend.tf`/`providers.tf` get them from `scripts/ensure-tf-boilerplate.sh`, with state key `apps/<team>/<repo>` and the team the broker verified (not a GitHub API lookup with the run's token). tf-apply generates the same files with the same team.
Expand Down
8 changes: 5 additions & 3 deletions scripts/auth-doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def main(argv=None):
p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
p.add_argument("--app-yaml", default="app.yaml")
p.add_argument("--team", help="default: app.yaml team")
p.add_argument("--app", help="default: app.yaml name, then $APP_SERVICE, then the repository name")
p.add_argument("--app", help="default: app.yaml name (required there)")
p.add_argument("--wait", type=int, default=0, help="seconds to wait for app-auth-sync to finish (CI: 180)")
p.add_argument("--no-tasks", action="store_true", help="skip the ECS task check")
p.add_argument("--json", action="store_true", help="print results as JSON")
Expand All @@ -374,8 +374,10 @@ def main(argv=None):
return 0
gen = _load_generator()
app = gen.load_yaml(args.app_yaml) or {}
name = args.app or app.get("name") or os.environ.get("APP_SERVICE") or \
os.environ.get("GITHUB_REPOSITORY", "").rsplit("/", 1)[-1]
name = args.app or app.get("name")
if not name:
print(f"{args.app_yaml}: name is required (or pass --app)", file=sys.stderr)
return 1
try:
report = diagnose(app, args.team, name, args.wait, tasks=not args.no_tasks)
except gen.AppYamlError as e:
Expand Down
Loading
Loading