@ossy/deployment-tools 3.0.8 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +160 -113
  3. package/jest.config.js +5 -0
  4. package/package.json +6 -4
  5. package/src/config/platform-config.js +16 -16
  6. package/src/ecs/platform-ecs-services.js +111 -0
  7. package/src/ecs/platform-ecs-services.spec.js +77 -0
  8. package/src/edge/platform-edge-domains.js +101 -0
  9. package/src/edge/platform-edge-domains.spec.js +87 -0
  10. package/src/index.js +33 -1
  11. package/src/infrastructure/dns-stack.js +47 -3
  12. package/src/infrastructure/dns-stack.spec.js +15 -0
  13. package/src/infrastructure/platform-ci-stack.js +151 -0
  14. package/src/infrastructure/platform-ecs-stack.js +326 -0
  15. package/src/infrastructure/platform-edge-stack.js +213 -0
  16. package/src/infrastructure/platform-secrets-stack.js +103 -0
  17. package/src/infrastructure/platform-stage.js +32 -5
  18. package/src/infrastructure/storage-static-stack.js +4 -5
  19. package/src/secrets/platform-secret-services.js +32 -0
  20. package/src/secrets/platform-secret-services.spec.js +40 -0
  21. package/src/secrets/sync-platform-secrets.js +136 -0
  22. package/src/services/platform-services.js +94 -0
  23. package/src/services/platform-services.spec.js +80 -0
  24. package/src/template/platform-template.js +5 -5
  25. package/src/infrastructure/container-deployment-target/aws-profile.js +0 -25
  26. package/src/infrastructure/container-deployment-target/caddy.service.js +0 -117
  27. package/src/infrastructure/container-deployment-target/container-deployment-target.js +0 -222
  28. package/src/infrastructure/container-deployment-target/container-service.js +0 -162
  29. package/src/infrastructure/container-deployment-target/index.js +0 -3
  30. package/src/infrastructure/container-deployment-target/ossy-api.service.js +0 -54
  31. package/src/infrastructure/container-deployment-target/ossy-runtime.service.js +0 -49
  32. package/src/infrastructure/container-deployment-target/user-data-commands.js +0 -31
  33. package/src/infrastructure/deployment-target-stack.js +0 -53
package/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.1.0](https://github.com/ossy-se/ossy/compare/v3.0.9...v3.1.0) (2026-08-12)
7
+
8
+ ### Features
9
+
10
+ * **deployment-tools:** add CloudFront WAF and ACM edge in front of ALB ([#574](https://github.com/ossy-se/ossy/issues/574)) ([2dd1a69](https://github.com/ossy-se/ossy/commit/2dd1a69f4facfcbcc0e30c931915e6d58c916ad7))
11
+ * **deployment-tools:** add ECS Fargate and ALB for platform services ([#573](https://github.com/ossy-se/ossy/issues/573)) ([42ef998](https://github.com/ossy-se/ossy/commit/42ef9986a1f96b422f0351b311e906dadd4db26b)), closes [#557](https://github.com/ossy-se/ossy/issues/557) [#572](https://github.com/ossy-se/ossy/issues/572) [#560](https://github.com/ossy-se/ossy/issues/560)
12
+ * **deployment-tools:** add per-service Secrets Manager secrets ([#572](https://github.com/ossy-se/ossy/issues/572)) ([021ad55](https://github.com/ossy-se/ossy/commit/021ad5535d20722c819c154f41239bf8e583b9c3)), closes [#555](https://github.com/ossy-se/ossy/issues/555)
13
+ * **deployment-tools:** cut over DNS to CloudFront and retire EC2/Caddy ([#570](https://github.com/ossy-se/ossy/issues/570)) ([99c4323](https://github.com/ossy-se/ossy/commit/99c43236adfee4ea68534f3245d2d9d420c51ce4)), closes [#560](https://github.com/ossy-se/ossy/issues/560) [#560](https://github.com/ossy-se/ossy/issues/560)
14
+
15
+
16
+ ## 3.0.9 (2026-08-11)
17
+
18
+ **Note:** Version bump only for package @ossy/deployment-tools
19
+
20
+
21
+
22
+
23
+
6
24
  ## 3.0.8 (2026-08-07)
7
25
 
8
26
  **Note:** Version bump only for package @ossy/deployment-tools
package/README.md CHANGED
@@ -1,34 +1,39 @@
1
1
  # @ossy/deployment-tools
2
2
 
3
- Infrastructure and CDK tooling for the Ossy platform EC2 host.
3
+ Infrastructure and CDK tooling for the Ossy platform.
4
4
 
5
- The host runs Caddy as a reverse proxy plus several Docker containers managed by systemd. Apps are deployed by running `ossy app publish`.
5
+ **Architecture** (epic #553): **CloudFront WAF ALB ECS Fargate**, with per-service Secrets Manager and GitHub Actions rollouts via OIDC. EC2 + Caddy + EIP are **removed from CDK** (#560).
6
6
 
7
7
  ## Architecture
8
8
 
9
9
  ```
10
- Internet → Caddy (:443)
11
- ├── api.ossy.se → ossy-api (:3001)
12
- ├── ossy.se ossy-website-ossy (:3002) ← temporary
13
- ├── www.plexus-sanitas.comossy-website-plexus-sanitas (:3003) ← temporary
14
- └── *.* (on-demand TLS) ossy-platform (:3000)
15
- └── loads build from CMS on first request
10
+ Internet → CloudFront (ACM TLS, us-east-1) → WAF → ALB (:80)
11
+ ├── host rules from platforms.json `services[].hosts|domain`
12
+ │ e.g. ossy.se, api.ossy.se → website-ossy Fargate service
13
+ ├── other known hosts / *.ossy.seplatform-runtime Fargate service
14
+ └── default actionplatform-runtime
16
15
  ```
17
16
 
18
- Caddy uses **on-demand TLS**: before issuing a certificate for any domain, it calls `GET http://localhost:3001/api/v0/apps/ask?domain=<domain>`. If the domain is registered (i.e. `ossy app publish` has been run for it), Caddy issues the cert and proxies to the platform runtime.
17
+ - Website images include the API **no** separate `ossy-api` service.
18
+ - Task definitions inject env from Secrets Manager (`{platform}/{serviceKey}`) and pull images from GHCR using `{platform}/ghcr-pull`.
19
+ - Health checks use `GET /health` on each target group.
20
+ - App CloudFront is **separate** from the `/media` CloudFront in `storage-static`.
21
+ - Viewer `Host` is forwarded to the ALB (`OriginRequestPolicy.ALL_VIEWER`) so host rules match.
22
+ - ACM terminates TLS at CloudFront; Caddy is not part of this path.
23
+ - Route53 for known domains aliases to CloudFront (A + AAAA), not an Elastic IP.
19
24
 
20
- ## Services
25
+ Customer nameserver / custom-domain handoff at a registrar is **out of scope** (follow-up).
21
26
 
22
- All services run as Docker containers managed by systemd. Service files are written to the EC2 host by CDK user data at instance creation.
27
+ ## Health checks (ALB / ECS)
23
28
 
24
- | Service | Unit file | Port | Image |
25
- |---|---|---|---|
26
- | Caddy | `caddy-route53.service` | 80, 443 | — |
27
- | Ossy API | `ossy-api.service` | 3001 | `ghcr.io/ossy-se/api:latest` |
28
- | Ossy Platform | `ossy-runtime.service` | 3000 | `ghcr.io/ossy-se/platform:latest` |
29
- | _configured via `services`_ | `<name>.service` | _per config_ | _per config_ |
29
+ Every deployable Ossy HTTP image exposes an unauthenticated liveness probe at **`GET /health`** (also accepts `HEAD`). It returns `200` when the Node process can accept traffic.
30
30
 
31
- Additional services are generated automatically from the `services` array in `platforms.json` — see the Configuration section below.
31
+ | Image | How `/health` is provided |
32
+ |---|---|
33
+ | `ghcr.io/ossy-se/platform` (runtime) | Built into `@ossy/platform` `startRuntime` — does not load a CMS site |
34
+ | Website / app images (`website-ossy`, other `services[]`) | Built into `@ossy/platform` `startServer` (and/or a local `health.api.js` until the platform package is bumped) |
35
+
36
+ Use path `/health` for ALB target-group and ECS container health checks. Do not point probes at `/`, `/status`, or authenticated routes.
32
37
 
33
38
  ## Configuration — `platforms.json`
34
39
 
@@ -38,101 +43,179 @@ Additional services are generated automatically from the `services` array in `pl
38
43
  |---|---|---|
39
44
  | `platformName` | Yes | Unique name, used as the CDK stage name and CloudFormation stack prefix |
40
45
  | `awsAccountId` | Yes | AWS account ID |
41
- | `awsKeyPairName` | Yes | EC2 key pair name for SSH access |
42
- | `awsInstanceClass` | No | EC2 instance class (e.g. `t3`, `t4g`, `m5`). Defaults to `t3` |
43
- | `awsInstanceSize` | No | EC2 instance size (e.g. `small`, `medium`, `large`). Defaults to `small` |
46
+ | `awsRegion` | No | Defaults to `eu-north-1` |
44
47
  | `sesDomains` | No | Domains to configure SES email identity for |
45
- | `domains` | No | Domains to create Route53 A records for, pointing to the EC2 Elastic IP. Supports wildcards (e.g. `*.ossy.se`). |
46
- | `services` | No | Per-platform container services (see below) |
48
+ | `domains` | No | Domains for Route53 aliases CloudFront and ACM coverage. Supports wildcards (e.g. `*.ossy.se`). |
49
+ | `services` | No | Per-platform HTTP container services (see below) |
47
50
  | `dnsRecords` | No | Additional DNS records by root domain (currently supports MX) |
48
- | `env` | No | Environment variables written to `/etc/environment` on the EC2 host at boot. **Never uploaded to S3.** |
49
-
50
- ### `services` container/domain mapping
51
+ | `env` | No | Env vars upserted into per-service Secrets Manager secrets (`npm run sync-secrets`). **Never uploaded to S3.** Values remain in git for now. |
52
+ | `githubDeployRepos` | Yes (for `platform-ci`) | `owner/repo` list trusted by the GitHub OIDC deploy role. Set in `platforms.json` (e.g. `ossy-se/ossy`, `ossy-se/website-ossy`). |
53
+ | `awsKeyPairName` / `awsInstanceClass` / `awsInstanceSize` | No | **Unused** after EC2 decommission (#560). Harmless if left in JSON. |
51
54
 
52
- Services come in two flavours: **HTTP** (routed through Caddy) and **TCP/UDP** (raw socket, bypasses Caddy). Both generate a systemd unit that pulls the image on every restart and forwards `/etc/environment` to the container.
55
+ ### `services` HTTP containers on ECS
53
56
 
54
- #### HTTP service (default)
55
-
56
- Generates a Caddy reverse-proxy block with Route53 TLS. The container must listen on port 3000.
57
+ HTTP services (default) become Fargate services with ALB host rules. The container must listen on port 3000.
57
58
 
58
59
  ```json
59
60
  "services": [
60
61
  {
61
62
  "name": "ossy-website-ossy",
62
63
  "domain": "ossy.se",
63
- "image": "ghcr.io/ossy-se/website-ossy:latest"
64
+ "image": "ghcr.io/ossy-se/website-ossy:latest",
65
+ "hosts": ["ossy.se", "api.ossy.se"]
64
66
  }
65
67
  ]
66
68
  ```
67
69
 
68
70
  | Field | Description |
69
71
  |---|---|
70
- | `name` | Unique service name — Docker container name and systemd unit (`<name>.service`). |
71
- | `domain` | Hostname routed by Caddy to this container (Route53 TLS via `dns route53`). |
72
- | `image` | Docker image to pull and run. |
72
+ | `name` | Unique service name — Secrets Manager / ECS key strips a leading `ossy-` (`website-ossy`). |
73
+ | `domain` | Primary hostname (default ALB host when `hosts` is omitted). |
74
+ | `hosts` | Optional ALB host-header list. Example: `["ossy.se","api.ossy.se"]` so the website image serves apex and API host. Defaults to `[domain]`. |
75
+ | `image` | Docker image to pull and run (GHCR). |
73
76
 
74
- Host ports are auto-assigned starting at `3002`, counting only HTTP services (TCP entries are skipped). Appending new HTTP entries is safe; avoid reordering existing ones.
77
+ **Built-in (always present):**
78
+ - `runtime` — platform runtime (`ghcr.io/ossy-se/platform:latest`) serving CMS-published apps; ALB default + unclaimed `domains`.
75
79
 
76
- #### TCP/UDP service
80
+ **Not deployed:** separate `ossy-api` container; TCP/UDP `services[]` entries (ignored by the ECS path).
77
81
 
78
- No Caddy block. Ports are mapped directly (`HOST:CONTAINER`) and opened in the EC2 security group. Use this for game servers or any other raw socket protocol.
82
+ **Wildcard subdomains:** Add `*.ossy.se` to `domains` for a Route53 wildcard alias to CloudFront.
79
83
 
80
- ```json
81
- "services": [
82
- {
83
- "name": "minecraft",
84
- "type": "tcp",
85
- "image": "itzg/minecraft-server",
86
- "ports": [25565],
87
- "protocol": "tcp"
88
- }
89
- ]
84
+ `MEDIA_REPOSITORY` and `MEDIA_CDN_DOMAIN_NAME` are derived from the provisioned S3 bucket and media CloudFront — do not set them in `env`.
85
+
86
+ To add a new platform, add an entry to `platforms.json`. No bucket name or CDN domain is needed; these are auto-generated by CloudFormation.
87
+
88
+ ## Platform secrets (Secrets Manager)
89
+
90
+ Each platform service gets **one** Secrets Manager secret (JSON object of env keys). Values still live in `platforms.json` for now; CDK owns the secret resources and IAM, and deploy upserts values.
91
+
92
+ | Secret name | Source |
93
+ |---|---|
94
+ | `{platform}/runtime` | Built-in platform-runtime |
95
+ | `{platform}/website-ossy` | HTTP entry in `services[]` named `ossy-website-ossy` (leading `ossy-` stripped) |
96
+ | `{platform}/…` | Other HTTP `services[]` entries |
97
+
98
+ **Not created:** `ossy-api`. TCP/UDP `services[]` entries are skipped.
99
+
100
+ Each secret has a matching IAM role `{platform}-{key}-task` assumed by `ecs-tasks.amazonaws.com` with read access to that secret only.
101
+
102
+ Also created: `{platform}/ghcr-pull` — Docker Hub-style credentials JSON (`username` + `password` PAT with `read:packages`) for private GHCR pulls. **Not** filled by `sync-secrets`; set manually once after the secrets stack deploys.
103
+
104
+ ### Sync values after CDK deploy
105
+
106
+ ```bash
107
+ cd packages/deployment-tools
108
+
109
+ # Create / update secret resources + IAM (part of the stage)
110
+ npx cdk deploy 'ossybot/platform-secrets' --profile ossybot
111
+
112
+ # Upsert secret *values* from platforms.json (never prints secret contents)
113
+ npm run sync-secrets -- --profile ossybot
114
+
115
+ # Optional: one platform, or dry-run
116
+ npm run sync-secrets -- --profile ossybot --platform ossybot
117
+ npm run sync-secrets -- --dry-run
90
118
  ```
91
119
 
92
- | Field | Description |
120
+ Do **not** remove secrets from `platforms.json` yet.
121
+
122
+ ## ECS Fargate + ALB
123
+
124
+ ```bash
125
+ cd packages/deployment-tools
126
+
127
+ # Secrets + IAM first, then sync values + set ghcr-pull
128
+ npx cdk deploy 'ossybot/platform-secrets' --profile ossybot
129
+ npm run sync-secrets -- --profile ossybot
130
+ # then put GHCR credentials into ossybot/ghcr-pull
131
+
132
+ # VPC, cluster, Fargate services, ALB host rules
133
+ npx cdk deploy 'ossybot/platform-ecs' --profile ossybot
134
+ ```
135
+
136
+ | Resource | Notes |
93
137
  |---|---|
94
- | `name` | Unique service name Docker container name and systemd unit. |
95
- | `type` | Must be `"tcp"` to enable this mode. |
96
- | `image` | Docker image to pull and run. |
97
- | `ports` | Array of port numbers to expose on the host and open in the security group. |
98
- | `protocol` | `"tcp"` (default), `"udp"`, or `"both"`. |
138
+ | VPC | Public + private subnets, 1 NAT gateway |
139
+ | ECS cluster | `{platform}-platform` |
140
+ | Fargate services | `runtime` (`ghcr.io/ossy-se/platform:latest`) + each HTTP `services[]` entry |
141
+ | ALB | Internet-facing HTTP :80; host rules; default runtime; health check `/health` |
142
+ | Task env | Secrets Manager JSON keys from `platforms.json` `env`; `MEDIA_*` from storage stack |
99
143
 
100
- **Fixed built-in services** (always present, not configurable via `services`):
101
- - `ossy-api` — the Ossy API (`ghcr.io/ossy-se/api:latest`, port 3001)
102
- - `ossy-runtime` — the platform runtime serving all CMS-published apps (`ghcr.io/ossy-se/platform:latest`, port 3000, catch-all)
144
+ ## CloudFront + WAF + ACM
103
145
 
104
- **Custom domains**: For customer-owned domains (e.g. `woodhill.com`), the customer adds an A record pointing to the Elastic IP at their registrar. No infrastructure change needed Caddy's on-demand TLS issues the cert automatically once the domain is registered via `ossy app publish`.
146
+ App traffic uses its own CloudFront distribution (do **not** overload the `/media` CDN). The edge stack is deployed in **`us-east-1`** because CloudFront viewer certificates and `CLOUDFRONT`-scope WAFv2 web ACLs must live there.
105
147
 
106
- **Wildcard subdomains**: Add `*.ossy.se` to `domains` for a Route53 wildcard A record. This covers any `*.ossy.se` subdomain without individual records.
148
+ ```bash
149
+ # After platform-ecs exists (ALB DNS used as origin)
150
+ npx cdk deploy 'ossybot/platform-edge' --profile ossybot
107
151
 
108
- `MEDIA_REPOSITORY` and `MEDIA_CDN_DOMAIN_NAME` are derived automatically from the provisioned S3 bucket and CloudFront distribution — do not set them in `env`.
152
+ # DNS aliases for known domains CloudFront
153
+ npx cdk deploy 'ossybot/dns' --profile ossybot
154
+ ```
109
155
 
110
- To add a new platform, add an entry to `platforms.json`. No bucket name or CDN domain is needed; these are auto-generated by CloudFormation.
156
+ | Piece | Detail |
157
+ |---|---|
158
+ | Origin | ALB DNS name from `platform-ecs` (`LoadBalancerDnsName`), HTTP :80 |
159
+ | Certificate | ACM in `us-east-1`, DNS-validated; covers `domains` + HTTP service hosts (apex + `*.ossy.se` etc.) |
160
+ | WAF | WAFv2 web ACL (`AWSManagedRulesCommonRuleSet` + `KnownBadInputs`) associated via `webAclId` |
161
+ | Cache | `CACHING_DISABLED` + `ALL_VIEWER` origin request policy (forwards `Host`) |
162
+ | DNS | Route53 alias A/AAAA for known domains → CloudFront |
163
+
164
+ ## Release to ECS from GitHub Actions
165
+
166
+ Images stay on **GHCR**. After a push to `main`, Publish workflows tag the image with the git SHA (and `:latest`), assume the platform deploy role via **GitHub OIDC** (no long-lived AWS keys), register a new task definition revision with the SHA tag, and update the ECS service.
167
+
168
+ ```bash
169
+ # After platform-ecs exists — creates OIDC provider + deploy role
170
+ npx cdk deploy 'ossybot/platform-ci' --profile ossybot
171
+ ```
172
+
173
+ | Piece | Detail |
174
+ |---|---|
175
+ | Role | `{platform}-github-deploy` (e.g. `ossybot-github-deploy`) |
176
+ | Trust | OIDC `token.actions.githubusercontent.com`, `sub` = `repo:<owner>/<repo>:ref:refs/heads/main` for each entry in `platforms.json` → `githubDeployRepos` |
177
+ | Cluster / services | `{platform}-platform` / `{platform}-{serviceKey}` (e.g. `ossybot-runtime`, `ossybot-website-ossy`) |
178
+ | Image tags | `ghcr.io/ossy-se/<image>:latest` and `:<git-sha>` |
179
+ | Workflows | each trusted repo’s Publish workflow rolls its ECS service |
180
+
181
+ Add another deploy source by appending `owner/repo` to `githubDeployRepos` and redeploying `platform-ci`. Deploy that stack once before the first Actions rollout. Workflows wait for ECS service stability after update.
111
182
 
112
183
  ## CDK Stacks
113
184
 
114
- Each platform is a CDK `Stage` containing five stacks:
185
+ Each platform is a CDK `Stage` containing these stacks:
115
186
 
116
187
  | Stack | Description |
117
188
  |---|---|
118
- | `storage-static` | S3 bucket (auto-named), daily backup plan, CloudFront CDN for `/media`. Kept separate from the EC2 stack so storage resources are never touched during instance updates. |
119
- | `deployment-target` | EC2 instance with Elastic IP. Imports the S3 bucket from `storage-static` via cross-stack reference. |
120
- | `dns` | Route53 A records for all domains in `platforms.json`, pointing to the Elastic IP. |
189
+ | `storage-static` | S3 bucket (auto-named), daily backup plan, CloudFront CDN for `/media`. |
190
+ | `platform-secrets` | Secrets Manager secrets per service + ECS task roles (read IAM) + `ghcr-pull`. Values synced from `platforms.json` via `npm run sync-secrets`. |
191
+ | `platform-ecs` | VPC, ECS Fargate services, ALB with host-based routing. |
192
+ | `platform-ci` | GitHub OIDC provider + `{platform}-github-deploy` IAM role for ECS rollouts from Actions. |
193
+ | `platform-edge` | CloudFront + WAF + ACM in `us-east-1` in front of the ALB. |
194
+ | `dns` | Route53 alias records for known domains → CloudFront app distribution. |
121
195
  | `ses` | SES email sending identity and DKIM records. |
122
196
 
123
- The Elastic IP ensures the EC2 instance can be replaced (instance type changes, user data updates) without breaking DNS or CloudFormation cross-stack exports.
197
+ ### Cutover / decommission EC2 (#560)
198
+
199
+ CDK no longer synthesizes `deployment-target` (EC2 + EIP + Caddy + systemd units). After `platform-edge` and `dns` are live and traffic is healthy:
200
+
201
+ ```bash
202
+ # Destroy the leftover CloudFormation stack (name may be stage-prefixed)
203
+ npx cdk destroy 'ossybot/deployment-target' --profile ossybot
204
+ # or, if the stack still exists outside the app:
205
+ # aws cloudformation delete-stack --stack-name ossybot-deployment-target --profile ossybot
206
+ ```
207
+
208
+ Downtime during cutover is acceptable. Confirm known domains resolve to CloudFront before destroying EC2.
124
209
 
125
210
  ## Bootstrapping a new platform
126
211
 
127
212
  ```bash
128
213
  cd packages/deployment-tools
129
214
  npx cdk deploy 'ossybot/**' --profile ossybot --require-approval never
215
+ npm run sync-secrets -- --profile ossybot
216
+ # set {platform}/ghcr-pull credentials once
130
217
  ```
131
218
 
132
- CDK provisions the instance, writes all service files, writes `/etc/environment` from `platforms.json`, and starts all services automatically. No manual SSH steps are required.
133
-
134
- > Note: EC2 user data only runs on **first boot**. To apply infrastructure changes to an existing instance you must terminate and recreate it, or SSH in and apply changes manually.
135
-
136
219
  ## Deploying changes
137
220
 
138
221
  Target a single platform:
@@ -157,32 +240,18 @@ npx cdk list
157
240
 
158
241
  ## Redeploying after a code change
159
242
 
160
- All services pull the latest image from GHCR on every restart no manual build step needed.
161
-
162
- ```bash
163
- # API — push to main triggers CI → ghcr.io/ossy-se/api:latest
164
- sudo systemctl restart ossy-api
165
-
166
- # Platform runtime — merge to main in ossy monorepo → ghcr.io/ossy-se/platform:latest
167
- sudo systemctl restart ossy-runtime
168
-
169
- # website-ossy — push to main in website-ossy repo → ghcr.io/ossy-se/website-ossy:latest
170
- sudo systemctl restart ossy-website-ossy
171
-
172
- # website-plexus-sanitas — push to main → ghcr.io/ossy-se/website-plexus-sanitas:latest
173
- sudo systemctl restart ossy-website-plexus-sanitas
174
- ```
243
+ Merge to `main` Publish workflows push GHCR tags (`:latest` + `:<sha>`) and roll the matching ECS service. No SSH / `systemctl`.
175
244
 
176
245
  ## Environment variables
177
246
 
178
- Defined in the `env` block of `platforms.json`, written to `/etc/environment` at boot. The following variables are injected automatically by CDK and should **not** be set manually:
247
+ Defined in the `env` block of `platforms.json`, synced into Secrets Manager. The following are injected automatically by the ECS stack and should **not** be set manually:
179
248
 
180
249
  | Variable | Source |
181
250
  |---|---|
182
251
  | `MEDIA_REPOSITORY` | S3 bucket name from `storage-static` stack |
183
252
  | `MEDIA_CDN_DOMAIN_NAME` | CloudFront domain from `storage-static` stack |
184
253
 
185
- All other env vars:
254
+ Other common env vars:
186
255
 
187
256
  | Variable | Used by | Description |
188
257
  |---|---|---|
@@ -198,32 +267,10 @@ All other env vars:
198
267
 
199
268
  ## Useful commands
200
269
 
201
- **SSH into instance**
202
- ```bash
203
- ssh -i path/to/keys ubuntu@<elastic-ip>
204
- ```
205
-
206
- **View service logs**
207
- ```bash
208
- journalctl -u ossy-runtime.service -n 200 -f
209
- journalctl -u ossy-api.service -n 200 -f
210
- journalctl -u ossy-website-ossy.service -n 200 -f
211
- journalctl -u caddy-route53.service -n 200 -f
212
- ```
213
-
214
- **View running containers**
215
270
  ```bash
216
- docker ps
217
- ```
218
-
219
- **View container logs**
220
- ```bash
221
- docker logs ossy-runtime
222
- docker logs ossy-api
223
- docker logs ossy-website-ossy
224
- ```
225
-
226
- **Reload Caddy config**
227
- ```bash
228
- sudo systemctl reload caddy-route53.service
271
+ cd packages/deployment-tools
272
+ npm test
273
+ npx cdk list
274
+ npx cdk diff 'ossybot/**' --profile ossybot
275
+ npm run sync-secrets -- --profile ossybot --dry-run
229
276
  ```
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ testEnvironment: 'node',
4
+ testMatch: ['**/src/**/*.spec.js'],
5
+ }
package/package.json CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "@ossy/deployment-tools",
3
- "version": "3.0.8",
3
+ "version": "3.1.0",
4
4
  "description": "Collection of scripts and tools to aid deployment of containers and static files to Amazon Web Services through GitHub Actions",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
7
7
  "scripts": {
8
- "test": "echo \"Error: no test specified\" && exit 0",
8
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
9
9
  "build": "echo \"The build step is not required when using JavaScript!\" && exit 0",
10
10
  "build:docs": "jsdoc -c jsdoc.config.js",
11
- "cdk": "cdk"
11
+ "cdk": "cdk",
12
+ "sync-secrets": "node ./src/secrets/sync-platform-secrets.js"
12
13
  },
13
14
  "author": "Ossy",
14
15
  "license": "ISC",
15
16
  "dependencies": {
17
+ "@aws-sdk/client-secrets-manager": "^3.0.0",
16
18
  "aws-cdk": "^2.73.0",
17
19
  "aws-cdk-lib": "^2.73.0",
18
20
  "constructs": "^10.1.304",
@@ -22,5 +24,5 @@
22
24
  "devDependencies": {
23
25
  "jest": "^30.4.2"
24
26
  },
25
- "gitHead": "980b380c9dee0de78ef3ebabe86efcec1c223a9a"
27
+ "gitHead": "9b146fb6fd9769f800863806c5bd16016508b218"
26
28
  }
@@ -3,31 +3,31 @@
3
3
  * @typedef {Object} PlatformConfig
4
4
  * @property {string} platformName - Name of platform
5
5
  * @property {string[]=} sesDomains - list of domains to configure AWS SES email identity and DKIM records for
6
- * @property {string[]=} domains - list of domains to create Route53 A records for, pointing to the EC2 host
6
+ * @property {string[]=} domains - list of domains for Route53 aliases (CloudFront app edge) and ACM coverage
7
7
  * @property {object} dnsRecords - map of dns records by root domain, only supports MX records so that we can add dns records for our email service
8
- * @property {Array<HttpServiceConfig|TcpServiceConfig>=} services - per-platform container services; each entry generates a systemd unit. HTTP services also get a Caddy reverse-proxy block. TCP/UDP services get direct port exposure and security group rules instead.
8
+ * @property {Array<HttpServiceConfig|TcpServiceConfig>=} services - per-platform container services. HTTP entries become ECS Fargate services + ALB host rules. TCP/UDP entries are ignored by the ECS path (not deployed).
9
+ * @property {Record<string,string>=} env - environment variables upserted into per-service Secrets Manager secrets via `npm run sync-secrets`. Never uploaded to S3. Values stay in git for now (#555).
10
+ * @property {string} awsAccountId - Aws account id
11
+ * @property {string=} awsRegion - AWS region, defaults to eu-north-1
12
+ * @property {string=} awsKeyPairName - unused after EC2 decommission (#560); ignored by CDK
13
+ * @property {string=} awsInstanceClass - unused after EC2 decommission (#560); ignored by CDK
14
+ * @property {string=} awsInstanceSize - unused after EC2 decommission (#560); ignored by CDK
15
+ * @property {string[]=} githubDeployRepos - `owner/repo` list trusted by the GitHub OIDC deploy role (#559); required for platform-ci, set in platforms.json
16
+ * @property {string=} ciDockerNetworkName - ?
9
17
  *
10
18
  * @typedef {Object} HttpServiceConfig
11
19
  * @property {'http'} [type] - defaults to 'http' when omitted
12
- * @property {string} name - unique service name (container + systemd unit)
13
- * @property {string} domain - hostname routed by Caddy to this container
20
+ * @property {string} name - unique service name (ECS service key derived by stripping leading `ossy-`)
21
+ * @property {string} domain - primary hostname (default ALB host when `hosts` omitted)
22
+ * @property {string[]=} hosts - ALB host-header values (#557); defaults to `[domain]`
14
23
  * @property {string} image - Docker image (e.g. ghcr.io/ossy-se/website-ossy:latest)
15
24
  *
16
25
  * @typedef {Object} TcpServiceConfig
17
- * @property {'tcp'} type - marks a raw socket service; bypasses Caddy entirely
18
- * @property {string} name - unique service name (container + systemd unit)
26
+ * @property {'tcp'} type - raw socket service; not deployed on the ECS path (#560)
27
+ * @property {string} name - unique service name
19
28
  * @property {string} image - Docker image (e.g. itzg/minecraft-server)
20
- * @property {number[]} ports - ports to open on the host and in the EC2 security group
29
+ * @property {number[]} ports - ports (unused by current CDK stacks)
21
30
  * @property {'tcp'|'udp'|'both'} [protocol] - defaults to 'tcp'
22
- * @property {Record<string,string>=} env - environment variables written to /etc/environment on the EC2 host at boot time; never uploaded to S3
23
- *
24
- * @property {string} awsAccountId - Aws account id
25
- * @property {string=} awsRegion - AWS region, defaults to eu-north-1
26
- * @property {string=} awsKeyPairName - EC2 key pair name for SSH access
27
- * @property {string=} awsInstanceClass - EC2 instance class (e.g. t3, t4g, m5), defaults to t3
28
- * @property {string=} awsInstanceSize - EC2 instance size (e.g. small, medium, large), defaults to small
29
- *
30
- * @property {string=} ciDockerNetworkName - ?
31
31
  */
32
32
 
33
33
  const SupportedRegions = {
@@ -0,0 +1,111 @@
1
+ /**
2
+ * ECS/Fargate service targets derived from platforms.json.
3
+ *
4
+ * Service set and naming come from shared `platform-services`. No separate
5
+ * `ossy-api` — API lives in website images. TCP/UDP entries are skipped (not
6
+ * part of the ECS path after #560).
7
+ */
8
+
9
+ const {
10
+ listHttpServiceEntries,
11
+ listPlatformServices,
12
+ } = require('../services/platform-services')
13
+
14
+ const RUNTIME_IMAGE = 'ghcr.io/ossy-se/platform:latest'
15
+ const CONTAINER_PORT = 3000
16
+
17
+ /**
18
+ * @typedef {Object} PlatformEcsService
19
+ * @property {string} key - short key (`runtime`, `website-ossy`, …) — matches Secrets Manager
20
+ * @property {string} secretName - `{platform}/{key}`
21
+ * @property {string} source - origin label (`platform-runtime` or `services[].name`)
22
+ * @property {string} image - container image (GHCR)
23
+ * @property {string[]} hosts - ALB host-header values for this service (empty = default/catch-all)
24
+ * @property {boolean} isDefault - when true, ALB default action forwards here (runtime)
25
+ * @property {number} containerPort - container listen port
26
+ */
27
+
28
+ /**
29
+ * Host headers for an HTTP `services[]` entry.
30
+ * Uses optional `hosts` when set; otherwise `[domain]`.
31
+ *
32
+ * @param {{ domain?: string, hosts?: string[] }} entry
33
+ * @returns {string[]}
34
+ */
35
+ function hostsFromServiceEntry(entry) {
36
+ if (Array.isArray(entry.hosts) && entry.hosts.length > 0) {
37
+ return entry.hosts.filter(Boolean)
38
+ }
39
+ if (entry.domain) return [entry.domain]
40
+ return []
41
+ }
42
+
43
+ /**
44
+ * Hosts routed explicitly to runtime (platform domains not claimed by an HTTP service).
45
+ * Wildcards like `*.ossy.se` are included so CMS apps keep working behind the ALB.
46
+ *
47
+ * @param {{ domains?: string[] }} config
48
+ * @param {Array<{ domain?: string, hosts?: string[] }>} httpEntries
49
+ * @returns {string[]}
50
+ */
51
+ function runtimeHosts(config, httpEntries) {
52
+ const claimed = new Set(httpEntries.flatMap(hostsFromServiceEntry))
53
+ return (config.domains || []).filter(domain => !claimed.has(domain))
54
+ }
55
+
56
+ /**
57
+ * List Fargate services for a platform (runtime + HTTP services[]).
58
+ *
59
+ * @param {{
60
+ * platformName: string,
61
+ * domains?: string[],
62
+ * services?: Array<{ name: string, type?: string, domain?: string, hosts?: string[], image?: string }>
63
+ * }} config
64
+ * @returns {PlatformEcsService[]}
65
+ */
66
+ function listPlatformEcsServices(config) {
67
+ if (!config?.platformName) {
68
+ throw new Error('[platform-ecs-services] platformName is required')
69
+ }
70
+
71
+ const httpEntries = listHttpServiceEntries(config)
72
+ const runtimeHostList = runtimeHosts(config, httpEntries)
73
+
74
+ return listPlatformServices(config).map(service => {
75
+ if (service.key === 'runtime') {
76
+ return {
77
+ key: service.key,
78
+ secretName: service.secretName,
79
+ source: service.source,
80
+ image: RUNTIME_IMAGE,
81
+ hosts: runtimeHostList,
82
+ isDefault: true,
83
+ containerPort: CONTAINER_PORT,
84
+ }
85
+ }
86
+
87
+ if (!service.entry?.image) {
88
+ throw new Error(
89
+ `[platform-ecs-services] HTTP service "${service.key}" is missing image`
90
+ )
91
+ }
92
+
93
+ return {
94
+ key: service.key,
95
+ secretName: service.secretName,
96
+ source: service.source,
97
+ image: service.entry.image,
98
+ hosts: hostsFromServiceEntry(service.entry),
99
+ isDefault: false,
100
+ containerPort: CONTAINER_PORT,
101
+ }
102
+ })
103
+ }
104
+
105
+ module.exports = {
106
+ RUNTIME_IMAGE,
107
+ CONTAINER_PORT,
108
+ hostsFromServiceEntry,
109
+ listPlatformEcsServices,
110
+ runtimeHosts,
111
+ }