@ossy/deployment-tools 1.19.0 → 1.20.1

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
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
+ ## 1.20.1 (2026-05-26)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **deployment-tools:** Update Caddy service setup and Ubuntu AMI version ([6b3f4f7](https://github.com/ossy-se/ossy/commit/6b3f4f755901450be744d4402ec1f314418d101e))
12
+
13
+
14
+
15
+
16
+
17
+ # 1.20.0 (2026-05-25)
18
+
19
+
20
+ ### Features
21
+
22
+ * **deployment-tools:** Refactor container service management and update documentation ([8e2bc9c](https://github.com/ossy-se/ossy/commit/8e2bc9c186dbc9a16ae6127fad755baffc07d0be))
23
+
24
+
25
+
26
+
27
+
6
28
  # 1.19.0 (2026-05-25)
7
29
 
8
30
 
package/README.md CHANGED
@@ -21,38 +21,139 @@ Caddy uses **on-demand TLS**: before issuing a certificate for any domain, it ca
21
21
 
22
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.
23
23
 
24
- | Service | Unit file | Port | Auto-start | Image |
25
- |---|---|---|---|---|
26
- | Caddy | `caddy-route53.service` | 80, 443 | Yes | — |
27
- | Ossy API | `ossy-api.service` | 3001 | Yes | `ghcr.io/ossy-se/api:latest` |
28
- | Ossy Platform | `ossy-runtime.service` | 3000 | Yes | `ghcr.io/ossy-se/platform:latest` |
29
- | website-ossy | `ossy-website-ossy.service` | 3002 | Yes | `ghcr.io/ossy-se/website-ossy:latest` |
30
- | website-plexus-sanitas | `ossy-website-plexus-sanitas.service` | 3003 | Yes | `ghcr.io/ossy-se/website-plexus-sanitas:latest` |
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_ |
31
30
 
32
- > **TODO (website-ossy, website-plexus-sanitas):** These are temporary services. Once `ossy app publish` is validated for each site, remove their service files and Caddy blocks so traffic flows through the platform runtime instead.
33
-
34
- > **TODO:** Pin `@ossy/platform` to a specific version at CDK deploy time (thread through `PlatformConfig`) rather than always pulling `@latest`.
31
+ Additional services are generated automatically from the `services` array in `platforms.json` see the Configuration section below.
35
32
 
36
33
  ## Configuration — `platforms.json`
37
34
 
38
- `packages/infrastructure/platforms.json` is the single source of truth for a platform. It contains:
35
+ `packages/infrastructure/platforms.json` is the single source of truth for a platform. Each entry becomes a CDK `Stage` with its own set of stacks.
36
+
37
+ | Field | Required | Description |
38
+ |---|---|---|
39
+ | `platformName` | Yes | Unique name, used as the CDK stage name and CloudFormation stack prefix |
40
+ | `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` |
44
+ | `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) |
47
+ | `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
+
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.
53
+
54
+ #### HTTP service (default)
55
+
56
+ Generates a Caddy reverse-proxy block with Route53 TLS. The container must listen on port 3000.
57
+
58
+ ```json
59
+ "services": [
60
+ {
61
+ "name": "ossy-website-ossy",
62
+ "domain": "ossy.se",
63
+ "image": "ghcr.io/ossy-se/website-ossy:latest"
64
+ }
65
+ ]
66
+ ```
67
+
68
+ | Field | Description |
69
+ |---|---|
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. |
73
+
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.
75
+
76
+ #### TCP/UDP service
77
+
78
+ No Caddy block. Ports are mapped directly (`HOST:CONTAINER`) and opened in the EC2 security group. Use this for game servers or any raw socket protocol.
79
+
80
+ ```json
81
+ "services": [
82
+ {
83
+ "name": "minecraft",
84
+ "type": "tcp",
85
+ "image": "itzg/minecraft-server",
86
+ "ports": [25565],
87
+ "protocol": "tcp"
88
+ }
89
+ ]
90
+ ```
91
+
92
+ | Field | Description |
93
+ |---|---|
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"`. |
99
+
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)
39
103
 
40
- - **AWS config**account ID, key pair, S3 bucket name, IAM role
41
- - **`domains`** — list of domains to create Route53 A records for, all pointing to the EC2 host IP
42
- - **`env`** — environment variables written to `/etc/environment` on the EC2 host at boot time. **These are never uploaded to S3.**
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`.
43
105
 
44
- To add or change environment variables, update the `env` block in `platforms.json` and redeploy. To add a new domain, add it to `domains` and redeploy.
106
+ **Wildcard subdomains**: Add `*.ossy.se` to `domains` for a Route53 wildcard A record. This covers any `*.ossy.se` subdomain without individual records.
45
107
 
46
- ## Bootstrapping a new host
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`.
47
109
 
48
- CDK provisions the instance, writes all service files, writes `/etc/environment` from `platforms.json`, and starts all services automatically. No manual SSH steps required.
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.
111
+
112
+ ## CDK Stacks
113
+
114
+ Each platform is a CDK `Stage` containing five stacks:
115
+
116
+ | Stack | Description |
117
+ |---|---|
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. |
121
+ | `ses` | SES email sending identity and DKIM records. |
122
+
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.
124
+
125
+ ## Bootstrapping a new platform
49
126
 
50
127
  ```bash
51
128
  cd packages/deployment-tools
52
- npx cdk deploy ossybot-deployment-target --require-approval never
129
+ npx cdk deploy 'ossybot/**' --profile ossybot --require-approval never
53
130
  ```
54
131
 
55
- > 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 the changes manually.
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
+ ## Deploying changes
137
+
138
+ Target a single platform:
139
+ ```bash
140
+ npx cdk deploy 'ossybot/**' --profile ossybot
141
+ ```
142
+
143
+ Target all platforms:
144
+ ```bash
145
+ npx cdk deploy '**' --profile ossybot
146
+ ```
147
+
148
+ Preview changes without deploying:
149
+ ```bash
150
+ npx cdk diff 'ossybot/**' --profile ossybot
151
+ ```
152
+
153
+ List all stacks:
154
+ ```bash
155
+ npx cdk list
156
+ ```
56
157
 
57
158
  ## Redeploying after a code change
58
159
 
@@ -74,15 +175,20 @@ sudo systemctl restart ossy-website-plexus-sanitas
74
175
 
75
176
  ## Environment variables
76
177
 
77
- All env vars are defined in the `env` block of `platforms.json` and written to `/etc/environment` at boot. Reference:
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:
179
+
180
+ | Variable | Source |
181
+ |---|---|
182
+ | `MEDIA_REPOSITORY` | S3 bucket name from `storage-static` stack |
183
+ | `MEDIA_CDN_DOMAIN_NAME` | CloudFront domain from `storage-static` stack |
184
+
185
+ All other env vars:
78
186
 
79
187
  | Variable | Used by | Description |
80
188
  |---|---|---|
81
189
  | `DB_URL` | API | Atlas MongoDB connection string |
82
190
  | `DB_NAME` | API | MongoDB database name |
83
191
  | `TOKEN_SECRET` | API | JWT signing secret |
84
- | `MEDIA_REPOSITORY` | API | S3 bucket name for media uploads |
85
- | `MEDIA_CDN_DOMAIN_NAME` | API | CloudFront domain for public media |
86
192
  | `AWS_ACCESS_KEY_ID` | API | AWS credentials for S3 |
87
193
  | `AWS_SECRET_ACCESS_KEY` | API | AWS credentials for S3 |
88
194
  | `OSSY_API_KEY` | Platform, websites | API JWT for CMS reads |
@@ -90,22 +196,11 @@ All env vars are defined in the `env` block of `platforms.json` and written to `
90
196
  | `OPENAI_API_KEY` | Worker | OpenAI API key |
91
197
  | `OPENAI_ORGANIZATION` | Worker | OpenAI organisation ID |
92
198
 
93
- ## Infrastructure (CDK)
94
-
95
- CDK stacks are defined in `src/infrastructure/`. Run via `cdk deploy` from the `deployment-tools` package.
96
-
97
- | Stack | Description |
98
- |---|---|
99
- | `trust-ci` | IAM role for CI to deploy infrastructure |
100
- | `deployment-target` | EC2 host, S3 bucket, CloudFront CDN for media |
101
- | `dns` | Route53 A records for all domains in `platforms.json` |
102
- | `ses` | SES email sending |
103
-
104
199
  ## Useful commands
105
200
 
106
201
  **SSH into instance**
107
202
  ```bash
108
- ssh -i path/to/keys ubuntu@<ip>
203
+ ssh -i path/to/keys ubuntu@<elastic-ip>
109
204
  ```
110
205
 
111
206
  **View service logs**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/deployment-tools",
3
- "version": "1.19.0",
3
+ "version": "1.20.1",
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",
@@ -12,11 +12,7 @@
12
12
  },
13
13
  "author": "Ossy",
14
14
  "license": "ISC",
15
- "bin": "./src/index.cli.js",
16
15
  "dependencies": {
17
- "@actions/core": "^1.10.0",
18
- "@aws-sdk/client-sts": "^3.245.0",
19
- "arg": "^5.0.2",
20
16
  "aws-cdk": "^2.73.0",
21
17
  "aws-cdk-lib": "^2.73.0",
22
18
  "constructs": "^10.1.304",
@@ -26,5 +22,5 @@
26
22
  "devDependencies": {
27
23
  "jest": "^27.5.1"
28
24
  },
29
- "gitHead": "a40e3b439ced37f52db9526f799189e6d0809349"
25
+ "gitHead": "b063e40ba5b374e04a5911a4fec92ec6b9014409"
30
26
  }
@@ -5,15 +5,29 @@
5
5
  * @property {string[]=} sesDomains - list of domains to configure AWS SES email identity and DKIM records for
6
6
  * @property {string[]=} domains - list of domains to create Route53 A records for, pointing to the EC2 host
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.
9
+ *
10
+ * @typedef {Object} HttpServiceConfig
11
+ * @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
14
+ * @property {string} image - Docker image (e.g. ghcr.io/ossy-se/website-ossy:latest)
15
+ *
16
+ * @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)
19
+ * @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
21
+ * @property {'tcp'|'udp'|'both'} [protocol] - defaults to 'tcp'
8
22
  * @property {Record<string,string>=} env - environment variables written to /etc/environment on the EC2 host at boot time; never uploaded to S3
9
23
  *
10
24
  * @property {string} awsAccountId - Aws account id
11
- * @property {string=} awsRegion - ?
12
- * @property {string=} awsKeyPairName - ?
13
- * @property {string} awsRoleToAssume - ?
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
14
29
  *
15
30
  * @property {string=} ciDockerNetworkName - ?
16
- * @property {string} ciGithubActionsRepo - organisation/repoName
17
31
  */
18
32
 
19
33
  const SupportedRegions = {
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /* eslint-disable no-new */
3
3
  const { App } = require('aws-cdk-lib')
4
- const { TrustCiStack } = require('./trust-ci-stack')
5
- const { DeploymentTargetStack } = require('./deployment-target-stack')
6
- const { DnsStack } = require('./dns-stack')
7
- const { SesStack } = require('./ses-stack')
4
+ const { PlatformStage } = require('./platform-stage')
8
5
  const { PlatformTemplateService } = require('../template')
9
6
  const { PlatformConfigService } = require('../config')
10
7
 
@@ -15,25 +12,12 @@ PlatformTemplateService
15
12
  const app = new App()
16
13
 
17
14
  configs.forEach(config => {
18
-
19
- const stackProps = {
15
+ new PlatformStage(app, config.platformName, {
20
16
  config,
21
17
  env: {
22
18
  account: config.awsAccountId,
23
19
  region: config.awsRegion
24
20
  }
25
- }
26
-
27
- new TrustCiStack(app, `${config.platformName}-trust-ci`, stackProps)
28
-
29
- const deploymentTarget = new DeploymentTargetStack(app, `${config.platformName}-deployment-target`, stackProps)
30
-
31
- new DnsStack(app, `${config.platformName}-dns`, {
32
- ...stackProps,
33
- containerDeploymentTargetPublicIp: deploymentTarget.containerDeploymentTargetPublicIp
34
21
  })
35
-
36
- new SesStack(app, `${config.platformName}-ses`, stackProps)
37
-
38
22
  })
39
23
  })
@@ -1,4 +1,16 @@
1
- const caddyfile = `
1
+ /**
2
+ * Builds the full Caddyfile content.
3
+ * - Global block: on-demand TLS pointing at the API ask endpoint
4
+ * - API block: explicit Route53 TLS for api.ossy.se
5
+ * - One block per ContainerService (from platforms.json `services`)
6
+ * - Catch-all :443 block: on-demand TLS, routes to the platform runtime
7
+ *
8
+ * @param {import('./container-service').ContainerService[]} services
9
+ */
10
+ const buildCaddyfile = (services) => {
11
+ const serviceBlocks = services.map(s => s.caddyBlock).join('\n')
12
+
13
+ return `
2
14
  {
3
15
  on_demand_tls {
4
16
  ask http://localhost:3001/api/v0/apps/ask
@@ -14,32 +26,7 @@ api.ossy.se {
14
26
  }
15
27
  reverse_proxy localhost:3001
16
28
  }
17
-
18
- # TODO: remove this block once ossy.se traffic flows through the platform runtime.
19
- # Once ossy app publish is validated for ossy.se, delete this block and the
20
- # ossy-website-ossy.service so the catch-all below serves it via the platform.
21
- ossy.se {
22
- tls {
23
- dns route53 {
24
- max_retries 10
25
- profile ci-client
26
- }
27
- }
28
- reverse_proxy localhost:3002
29
- }
30
-
31
- # TODO: remove this block once www.plexus-sanitas.com traffic flows through the platform runtime.
32
- # Once ossy app publish is validated for plexus-sanitas, delete this block and the
33
- # ossy-website-plexus-sanitas.service so the catch-all below serves it via the platform.
34
- www.plexus-sanitas.com {
35
- tls {
36
- dns route53 {
37
- max_retries 10
38
- profile ci-client
39
- }
40
- }
41
- reverse_proxy localhost:3003
42
- }
29
+ ${serviceBlocks}
43
30
 
44
31
  :443 {
45
32
  tls {
@@ -52,6 +39,7 @@ www.plexus-sanitas.com {
52
39
  reverse_proxy localhost:3000
53
40
  }
54
41
  `
42
+ }
55
43
 
56
44
  const systemdServiceFile = `
57
45
  # caddy-route53.service — Caddy with Route53 DNS plugin for on-demand TLS.
@@ -84,14 +72,19 @@ WantedBy=multi-user.target cloud-init.target
84
72
  */
85
73
  class CaddyService {
86
74
 
87
- static install() {
75
+ /**
76
+ * @param {import('./container-service').ContainerService[]} services
77
+ */
78
+ static install(services = []) {
79
+ const caddyfile = buildCaddyfile(services)
88
80
  return [
89
81
  // Write Caddyfile
90
82
  'sudo mkdir -p /etc/caddy',
91
83
  `sudo tee /etc/caddy/Caddyfile > /dev/null << 'CADDYEOF'\n${caddyfile}\nCADDYEOF`,
92
84
  // Write systemd unit
93
85
  `sudo tee /etc/systemd/system/caddy-route53.service > /dev/null << 'UNITEOF'\n${systemdServiceFile}\nUNITEOF`,
94
- // Install Caddy + xcaddy + Route53 plugin
86
+ // Create caddy system user (apt install caddy may not run on all Ubuntu versions)
87
+ 'sudo useradd --system --home /var/lib/caddy --shell /usr/sbin/nologin --create-home caddy || true',
95
88
  'sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https',
96
89
  'sudo curl -1sLf \'https://dl.cloudsmith.io/public/caddy/stable/gpg.key\' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg',
97
90
  'sudo curl -1sLf \'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt\' | sudo tee /etc/apt/sources.list.d/caddy-stable.list',
@@ -11,7 +11,9 @@ const {
11
11
  Port,
12
12
  UserData,
13
13
  KeyPair,
14
- BlockDeviceVolume
14
+ BlockDeviceVolume,
15
+ CfnEIP,
16
+ CfnEIPAssociation
15
17
  } = require('aws-cdk-lib/aws-ec2')
16
18
  const { Role, ServicePrincipal, Policy, PolicyStatement, Effect } = require('aws-cdk-lib/aws-iam')
17
19
  const { Source, BucketDeployment } = require('aws-cdk-lib/aws-s3-deployment')
@@ -19,8 +21,7 @@ const { getInstallNodeJs, getInstallNpm, getInstallDocker } = require('./user-da
19
21
  const { CaddyService } = require('./caddy.service')
20
22
  const { OssyRuntimeService } = require('./ossy-runtime.service')
21
23
  const { OssyApiService } = require('./ossy-api.service')
22
- const { OssyWebsiteOssyService } = require('./ossy-website-ossy.service')
23
- const { OssyWebsitePlexusSanitasService } = require('./ossy-website-plexus-sanitas.service')
24
+ const { fromConfig: buildContainerServices } = require('./container-service')
24
25
  const { AwsProfile } = require('./aws-profile')
25
26
  const { SupportedRegions } = require('../../config')
26
27
 
@@ -33,7 +34,7 @@ const { SupportedRegions } = require('../../config')
33
34
  */
34
35
 
35
36
  const InstanceImages = {
36
- UBUNTU: 'ami-092cce4a19b438926'
37
+ UBUNTU: 'ami-067bcf851477ebb78' // Ubuntu Server 24.04 LTS (HVM), eu-north-1
37
38
  }
38
39
 
39
40
  /**
@@ -70,8 +71,18 @@ class ContainerDeploymentTarget extends Construct {
70
71
  'allow HTTPS traffic from anywhere'
71
72
  )
72
73
 
74
+ // Build container services from config — opens additional security group ports for TCP/UDP services
75
+ const containerServices = buildContainerServices(props.config.services)
76
+
77
+ containerServices.forEach(service => {
78
+ service.securityGroupPorts.forEach(({ port, protocol }) => {
79
+ const portRule = protocol === 'udp' ? Port.udp(port) : Port.tcp(port)
80
+ securityGroup.addIngressRule(Peer.anyIpv4(), portRule, `${service.name} ${protocol.toUpperCase()} ${port}`)
81
+ })
82
+ })
83
+
73
84
  const platformConfigDeployment = new BucketDeployment(this, 'PlatformConfigDeployment', {
74
- sources: [Source.jsonData('platform-config.json', { ...props.config, awsRoleToAssume: undefined, env: undefined })],
85
+ sources: [Source.jsonData('platform-config.json', { ...props.config, env: undefined })],
75
86
  destinationBucket: props.bucket,
76
87
  // Default prune:true would delete every other object (e.g. media/*) on each infra deploy.
77
88
  prune: false
@@ -125,11 +136,10 @@ class ContainerDeploymentTarget extends Construct {
125
136
  `sudo tee /etc/environment << 'ENVEOF'\n${envLines.join('\n')}\nENVEOF`,
126
137
  // Create shared Docker network for inter-container communication
127
138
  'docker network create ossy-network || true',
128
- ...CaddyService.install(),
139
+ ...CaddyService.install(containerServices.filter(s => s.caddyBlock !== null)),
129
140
  ...OssyRuntimeService.install(),
130
141
  ...OssyApiService.install(),
131
- ...OssyWebsiteOssyService.install(),
132
- ...OssyWebsitePlexusSanitasService.install()
142
+ ...containerServices.flatMap(s => s.install())
133
143
  )
134
144
 
135
145
  userData.addS3DownloadCommand({
@@ -146,21 +156,42 @@ class ContainerDeploymentTarget extends Construct {
146
156
  ...OssyRuntimeService.start(),
147
157
  ...OssyApiService.enable(),
148
158
  ...OssyApiService.start(),
149
- ...OssyWebsiteOssyService.enable(),
150
- ...OssyWebsiteOssyService.start(),
151
- ...OssyWebsitePlexusSanitasService.enable(),
152
- ...OssyWebsitePlexusSanitasService.start()
159
+ ...containerServices.flatMap(s => [...s.enable(), ...s.start()])
153
160
  )
154
161
 
162
+ const instanceClassMap = {
163
+ t2: InstanceClass.T2,
164
+ t3: InstanceClass.T3,
165
+ t3a: InstanceClass.T3A,
166
+ t4g: InstanceClass.T4G,
167
+ m5: InstanceClass.M5,
168
+ m6i: InstanceClass.M6I,
169
+ c5: InstanceClass.C5,
170
+ c6i: InstanceClass.C6I,
171
+ }
172
+
173
+ const instanceSizeMap = {
174
+ nano: InstanceSize.NANO,
175
+ micro: InstanceSize.MICRO,
176
+ small: InstanceSize.SMALL,
177
+ medium: InstanceSize.MEDIUM,
178
+ large: InstanceSize.LARGE,
179
+ xlarge: InstanceSize.XLARGE,
180
+ '2xlarge': InstanceSize.XLARGE2,
181
+ }
182
+
183
+ const instanceClass = instanceClassMap[props.config.awsInstanceClass ?? 't3']
184
+ const instanceSize = instanceSizeMap[props.config.awsInstanceSize ?? 'small']
185
+
186
+ if (!instanceClass) throw new Error(`[ContainerDeploymentTarget] Unknown awsInstanceClass: ${props.config.awsInstanceClass}`)
187
+ if (!instanceSize) throw new Error(`[ContainerDeploymentTarget] Unknown awsInstanceSize: ${props.config.awsInstanceSize}`)
188
+
155
189
  const ec2Instance = new Instance(this, 'Ec2Instance', {
156
190
  vpc,
157
191
  securityGroup,
158
192
  userData,
159
193
  role,
160
- instanceType: InstanceType.of(
161
- InstanceClass.T3,
162
- InstanceSize.SMALL
163
- ),
194
+ instanceType: InstanceType.of(instanceClass, instanceSize),
164
195
  machineImage: new GenericLinuxImage({
165
196
  [SupportedRegions.North]: InstanceImages.UBUNTU
166
197
  }),
@@ -175,7 +206,15 @@ class ContainerDeploymentTarget extends Construct {
175
206
 
176
207
  props.bucket.grantRead(ec2Instance, '*')
177
208
 
178
- this.instancePublicIp = ec2Instance.instancePublicIp
209
+ // Elastic IP gives the instance a stable public IP that survives instance replacements,
210
+ // preventing CloudFormation cross-stack export conflicts when the EC2 instance is updated.
211
+ const eip = new CfnEIP(this, 'ElasticIp', { domain: 'vpc' })
212
+ new CfnEIPAssociation(this, 'EipAssociation', {
213
+ instanceId: ec2Instance.instanceId,
214
+ allocationId: eip.attrAllocationId
215
+ })
216
+
217
+ this.instancePublicIp = eip.attrPublicIp
179
218
 
180
219
  }
181
220
  }