@ossy/deployment-tools 1.19.0 → 1.20.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
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.0 (2026-05-25)
7
+
8
+
9
+ ### Features
10
+
11
+ * **deployment-tools:** Refactor container service management and update documentation ([8e2bc9c](https://github.com/ossy-se/ossy/commit/8e2bc9c186dbc9a16ae6127fad755baffc07d0be))
12
+
13
+
14
+
15
+
16
+
6
17
  # 1.19.0 (2026-05-25)
7
18
 
8
19
 
package/README.md CHANGED
@@ -21,38 +21,111 @@ 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
+ Each entry in `services` generates a systemd unit that pulls and runs a Docker image, and a Caddy block that routes a domain to it. All variables from `env` are forwarded to the container via `--env-file /etc/environment`.
53
+
54
+ ```json
55
+ "services": [
56
+ {
57
+ "name": "ossy-website-ossy",
58
+ "domain": "ossy.se",
59
+ "image": "ghcr.io/ossy-se/website-ossy:latest"
60
+ }
61
+ ]
62
+ ```
63
+
64
+ | Field | Description |
65
+ |---|---|
66
+ | `name` | Unique service name. Used as the Docker container name and systemd unit name (`<name>.service`). |
67
+ | `domain` | Domain this service handles. Gets its own Caddy block with Route53 TLS. |
68
+ | `image` | Docker image to pull and run. |
69
+
70
+ Host ports are auto-assigned starting at `3002` based on the order in the array (`services[0]` → `3002`, `services[1]` → `3003`, etc.). Appending new entries is safe; avoid reordering existing ones.
71
+
72
+ **Fixed built-in services** (always present, not configurable via `services`):
73
+ - `ossy-api` — the Ossy API (`ghcr.io/ossy-se/api:latest`, port 3001)
74
+ - `ossy-runtime` — the platform runtime serving all CMS-published apps (`ghcr.io/ossy-se/platform:latest`, port 3000, catch-all)
75
+
76
+ **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`.
77
+
78
+ **Wildcard subdomains**: Add `*.ossy.se` to `domains` for a Route53 wildcard A record. This covers any `*.ossy.se` subdomain without individual records.
39
79
 
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.**
80
+ `MEDIA_REPOSITORY` and `MEDIA_CDN_DOMAIN_NAME` are derived automatically from the provisioned S3 bucket and CloudFront distribution — do not set them in `env`.
43
81
 
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.
82
+ 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.
45
83
 
46
- ## Bootstrapping a new host
84
+ ## CDK Stacks
47
85
 
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.
86
+ Each platform is a CDK `Stage` containing five stacks:
87
+
88
+ | Stack | Description |
89
+ |---|---|
90
+ | `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. |
91
+ | `deployment-target` | EC2 instance with Elastic IP. Imports the S3 bucket from `storage-static` via cross-stack reference. |
92
+ | `dns` | Route53 A records for all domains in `platforms.json`, pointing to the Elastic IP. |
93
+ | `ses` | SES email sending identity and DKIM records. |
94
+
95
+ The Elastic IP ensures the EC2 instance can be replaced (instance type changes, user data updates) without breaking DNS or CloudFormation cross-stack exports.
96
+
97
+ ## Bootstrapping a new platform
49
98
 
50
99
  ```bash
51
100
  cd packages/deployment-tools
52
- npx cdk deploy ossybot-deployment-target --require-approval never
101
+ npx cdk deploy 'ossybot/**' --profile ossybot --require-approval never
53
102
  ```
54
103
 
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.
104
+ 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.
105
+
106
+ > 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.
107
+
108
+ ## Deploying changes
109
+
110
+ Target a single platform:
111
+ ```bash
112
+ npx cdk deploy 'ossybot/**' --profile ossybot
113
+ ```
114
+
115
+ Target all platforms:
116
+ ```bash
117
+ npx cdk deploy '**' --profile ossybot
118
+ ```
119
+
120
+ Preview changes without deploying:
121
+ ```bash
122
+ npx cdk diff 'ossybot/**' --profile ossybot
123
+ ```
124
+
125
+ List all stacks:
126
+ ```bash
127
+ npx cdk list
128
+ ```
56
129
 
57
130
  ## Redeploying after a code change
58
131
 
@@ -74,15 +147,20 @@ sudo systemctl restart ossy-website-plexus-sanitas
74
147
 
75
148
  ## Environment variables
76
149
 
77
- All env vars are defined in the `env` block of `platforms.json` and written to `/etc/environment` at boot. Reference:
150
+ 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:
151
+
152
+ | Variable | Source |
153
+ |---|---|
154
+ | `MEDIA_REPOSITORY` | S3 bucket name from `storage-static` stack |
155
+ | `MEDIA_CDN_DOMAIN_NAME` | CloudFront domain from `storage-static` stack |
156
+
157
+ All other env vars:
78
158
 
79
159
  | Variable | Used by | Description |
80
160
  |---|---|---|
81
161
  | `DB_URL` | API | Atlas MongoDB connection string |
82
162
  | `DB_NAME` | API | MongoDB database name |
83
163
  | `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
164
  | `AWS_ACCESS_KEY_ID` | API | AWS credentials for S3 |
87
165
  | `AWS_SECRET_ACCESS_KEY` | API | AWS credentials for S3 |
88
166
  | `OSSY_API_KEY` | Platform, websites | API JWT for CMS reads |
@@ -90,22 +168,11 @@ All env vars are defined in the `env` block of `platforms.json` and written to `
90
168
  | `OPENAI_API_KEY` | Worker | OpenAI API key |
91
169
  | `OPENAI_ORGANIZATION` | Worker | OpenAI organisation ID |
92
170
 
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
171
  ## Useful commands
105
172
 
106
173
  **SSH into instance**
107
174
  ```bash
108
- ssh -i path/to/keys ubuntu@<ip>
175
+ ssh -i path/to/keys ubuntu@<elastic-ip>
109
176
  ```
110
177
 
111
178
  **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.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",
@@ -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": "50d7e273859b6b77606d8530913b971c7445a34a"
30
26
  }
@@ -5,15 +5,16 @@
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<{name: string, domain: string, image: string}>=} services - per-platform container services; each entry generates a systemd unit and a Caddy domain block. Host ports are auto-assigned starting at 3002.
8
9
  * @property {Record<string,string>=} env - environment variables written to /etc/environment on the EC2 host at boot time; never uploaded to S3
9
10
  *
10
11
  * @property {string} awsAccountId - Aws account id
11
- * @property {string=} awsRegion - ?
12
- * @property {string=} awsKeyPairName - ?
13
- * @property {string} awsRoleToAssume - ?
12
+ * @property {string=} awsRegion - AWS region, defaults to eu-north-1
13
+ * @property {string=} awsKeyPairName - EC2 key pair name for SSH access
14
+ * @property {string=} awsInstanceClass - EC2 instance class (e.g. t3, t4g, m5), defaults to t3
15
+ * @property {string=} awsInstanceSize - EC2 instance size (e.g. small, medium, large), defaults to small
14
16
  *
15
17
  * @property {string=} ciDockerNetworkName - ?
16
- * @property {string} ciGithubActionsRepo - organisation/repoName
17
18
  */
18
19
 
19
20
  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,7 +72,11 @@ 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',
@@ -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 { ContainerService } = require('./container-service')
24
25
  const { AwsProfile } = require('./aws-profile')
25
26
  const { SupportedRegions } = require('../../config')
26
27
 
@@ -71,7 +72,7 @@ class ContainerDeploymentTarget extends Construct {
71
72
  )
72
73
 
73
74
  const platformConfigDeployment = new BucketDeployment(this, 'PlatformConfigDeployment', {
74
- sources: [Source.jsonData('platform-config.json', { ...props.config, awsRoleToAssume: undefined, env: undefined })],
75
+ sources: [Source.jsonData('platform-config.json', { ...props.config, env: undefined })],
75
76
  destinationBucket: props.bucket,
76
77
  // Default prune:true would delete every other object (e.g. media/*) on each infra deploy.
77
78
  prune: false
@@ -111,6 +112,9 @@ class ContainerDeploymentTarget extends Construct {
111
112
  // Write platform env vars to /etc/environment so all systemd services pick them up.
112
113
  const envLines = Object.entries(props.config.env ?? {}).map(([k, v]) => `${k}=${v}`)
113
114
 
115
+ // Build generic container services from config.services
116
+ const containerServices = ContainerService.fromConfig(props.config.services)
117
+
114
118
  userData.addCommands(
115
119
  'sudo groupadd docker',
116
120
  'sudo usermod -aG docker ubuntu',
@@ -125,11 +129,10 @@ class ContainerDeploymentTarget extends Construct {
125
129
  `sudo tee /etc/environment << 'ENVEOF'\n${envLines.join('\n')}\nENVEOF`,
126
130
  // Create shared Docker network for inter-container communication
127
131
  'docker network create ossy-network || true',
128
- ...CaddyService.install(),
132
+ ...CaddyService.install(containerServices),
129
133
  ...OssyRuntimeService.install(),
130
134
  ...OssyApiService.install(),
131
- ...OssyWebsiteOssyService.install(),
132
- ...OssyWebsitePlexusSanitasService.install()
135
+ ...containerServices.flatMap(s => s.install())
133
136
  )
134
137
 
135
138
  userData.addS3DownloadCommand({
@@ -146,21 +149,42 @@ class ContainerDeploymentTarget extends Construct {
146
149
  ...OssyRuntimeService.start(),
147
150
  ...OssyApiService.enable(),
148
151
  ...OssyApiService.start(),
149
- ...OssyWebsiteOssyService.enable(),
150
- ...OssyWebsiteOssyService.start(),
151
- ...OssyWebsitePlexusSanitasService.enable(),
152
- ...OssyWebsitePlexusSanitasService.start()
152
+ ...containerServices.flatMap(s => [...s.enable(), ...s.start()])
153
153
  )
154
154
 
155
+ const instanceClassMap = {
156
+ t2: InstanceClass.T2,
157
+ t3: InstanceClass.T3,
158
+ t3a: InstanceClass.T3A,
159
+ t4g: InstanceClass.T4G,
160
+ m5: InstanceClass.M5,
161
+ m6i: InstanceClass.M6I,
162
+ c5: InstanceClass.C5,
163
+ c6i: InstanceClass.C6I,
164
+ }
165
+
166
+ const instanceSizeMap = {
167
+ nano: InstanceSize.NANO,
168
+ micro: InstanceSize.MICRO,
169
+ small: InstanceSize.SMALL,
170
+ medium: InstanceSize.MEDIUM,
171
+ large: InstanceSize.LARGE,
172
+ xlarge: InstanceSize.XLARGE,
173
+ '2xlarge': InstanceSize.XLARGE2,
174
+ }
175
+
176
+ const instanceClass = instanceClassMap[props.config.awsInstanceClass ?? 't3']
177
+ const instanceSize = instanceSizeMap[props.config.awsInstanceSize ?? 'small']
178
+
179
+ if (!instanceClass) throw new Error(`[ContainerDeploymentTarget] Unknown awsInstanceClass: ${props.config.awsInstanceClass}`)
180
+ if (!instanceSize) throw new Error(`[ContainerDeploymentTarget] Unknown awsInstanceSize: ${props.config.awsInstanceSize}`)
181
+
155
182
  const ec2Instance = new Instance(this, 'Ec2Instance', {
156
183
  vpc,
157
184
  securityGroup,
158
185
  userData,
159
186
  role,
160
- instanceType: InstanceType.of(
161
- InstanceClass.T3,
162
- InstanceSize.SMALL
163
- ),
187
+ instanceType: InstanceType.of(instanceClass, instanceSize),
164
188
  machineImage: new GenericLinuxImage({
165
189
  [SupportedRegions.North]: InstanceImages.UBUNTU
166
190
  }),
@@ -175,7 +199,15 @@ class ContainerDeploymentTarget extends Construct {
175
199
 
176
200
  props.bucket.grantRead(ec2Instance, '*')
177
201
 
178
- this.instancePublicIp = ec2Instance.instancePublicIp
202
+ // Elastic IP gives the instance a stable public IP that survives instance replacements,
203
+ // preventing CloudFormation cross-stack export conflicts when the EC2 instance is updated.
204
+ const eip = new CfnEIP(this, 'ElasticIp', { domain: 'vpc' })
205
+ new CfnEIPAssociation(this, 'EipAssociation', {
206
+ instanceId: ec2Instance.instanceId,
207
+ allocationId: eip.attrAllocationId
208
+ })
209
+
210
+ this.instancePublicIp = eip.attrPublicIp
179
211
 
180
212
  }
181
213
  }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ContainerServiceConfig
3
+ * @typedef {Object} ContainerServiceConfig
4
+ * @property {string} name - Unique service name, used for the container name and systemd unit
5
+ * @property {string} domain - Domain this service is reachable on (used in Caddy block)
6
+ * @property {string} image - Docker image to run (e.g. ghcr.io/ossy-se/website-ossy:latest)
7
+ */
8
+
9
+ const BASE_PORT = 3002 // 3000 = platform runtime, 3001 = API
10
+
11
+ /**
12
+ * ContainerService generates a systemd unit file and a Caddy reverse-proxy block
13
+ * for a domain → Docker container mapping defined in platforms.json `services`.
14
+ *
15
+ * Host ports are auto-assigned starting at 3002 based on the service's index in
16
+ * the array. All containers are expected to listen on port 3000 internally.
17
+ *
18
+ * All environment variables from /etc/environment are forwarded to the container
19
+ * via --env-file, so no per-service env wiring is needed.
20
+ */
21
+ class ContainerService {
22
+
23
+ /**
24
+ * @param {ContainerServiceConfig} config
25
+ * @param {number} index - position in the services array, used to derive host port
26
+ */
27
+ constructor({ name, domain, image }, index) {
28
+ this.name = name
29
+ this.domain = domain
30
+ this.image = image
31
+ this.port = BASE_PORT + index
32
+ }
33
+
34
+ get systemdUnitFile() {
35
+ return `
36
+ [Unit]
37
+ Description=Container service: ${this.name} (${this.domain})
38
+ After=network.target docker.service
39
+ Requires=docker.service
40
+
41
+ [Service]
42
+ EnvironmentFile=/etc/environment
43
+ ExecStartPre=-/usr/bin/docker rm -f ${this.name}
44
+ ExecStartPre=/usr/bin/docker pull ${this.image}
45
+ ExecStart=/usr/bin/docker run --name ${this.name} \\
46
+ --network ossy-network \\
47
+ -p ${this.port}:3000 \\
48
+ --env-file /etc/environment \\
49
+ ${this.image}
50
+ ExecStop=/usr/bin/docker stop ${this.name}
51
+ Restart=on-failure
52
+ RestartSec=5
53
+
54
+ [Install]
55
+ WantedBy=multi-user.target
56
+ `
57
+ }
58
+
59
+ get caddyBlock() {
60
+ return `
61
+ ${this.domain} {
62
+ tls {
63
+ dns route53 {
64
+ max_retries 10
65
+ profile ci-client
66
+ }
67
+ }
68
+ reverse_proxy localhost:${this.port}
69
+ }`
70
+ }
71
+
72
+ get unitName() {
73
+ return `${this.name}.service`
74
+ }
75
+
76
+ install() {
77
+ return [`sudo tee /etc/systemd/system/${this.unitName} > /dev/null << 'EOF'\n${this.systemdUnitFile}\nEOF`]
78
+ }
79
+
80
+ enable() {
81
+ return [`sudo systemctl enable ${this.name}`]
82
+ }
83
+
84
+ start() {
85
+ return [`sudo systemctl start ${this.name}`]
86
+ }
87
+
88
+ /**
89
+ * Build ContainerService instances from the `services` array in platforms.json.
90
+ * @param {ContainerServiceConfig[]} services
91
+ * @returns {ContainerService[]}
92
+ */
93
+ static fromConfig(services = []) {
94
+ return services.map((s, index) => new ContainerService(s, index))
95
+ }
96
+
97
+ }
98
+
99
+ module.exports = {
100
+ ContainerService
101
+ }
@@ -1,97 +1,50 @@
1
1
  /* eslint-disable no-new */
2
- const { CfnOutput, Stack, RemovalPolicy } = require('aws-cdk-lib')
3
- const { BackupPlan, BackupVault, BackupResource } = require('aws-cdk-lib/aws-backup')
4
- const { Bucket, BlockPublicAccess } = require('aws-cdk-lib/aws-s3')
5
- const { Distribution, PriceClass, ResponseHeadersPolicy } = require('aws-cdk-lib/aws-cloudfront')
6
- const { S3BucketOrigin } = require('aws-cdk-lib/aws-cloudfront-origins')
2
+ const { CfnOutput, Stack } = require('aws-cdk-lib')
7
3
  const { ContainerDeploymentTarget } = require('./container-deployment-target')
8
4
 
9
5
  /**
10
- * @class
11
- */
6
+ * DeploymentTargetStackProps
7
+ * @typedef {Object} DeploymentTargetStackProps
8
+ * @property {PlatformConfig} config - platform config
9
+ * @property {import('aws-cdk-lib/aws-s3').IBucket} bucket - static assets bucket (from StorageStaticStack)
10
+ * @property {string} cdnDomainName - CloudFront domain name (from StorageStaticStack)
11
+ */
12
+
13
+ /**
14
+ * DeploymentTargetStack provisions the EC2 instance (with Elastic IP) that runs the
15
+ * platform services. The S3 bucket lives in StorageStaticStack so that EC2 replacements
16
+ * never trigger CloudFormation cross-stack export conflicts on storage resources.
17
+ *
18
+ * MEDIA_REPOSITORY and MEDIA_CDN_DOMAIN_NAME are derived from the actual bucket and CDN
19
+ * resources rather than being hardcoded in platforms.json.
20
+ */
12
21
  class DeploymentTargetStack extends Stack {
13
22
  constructor(scope, id, props) {
14
23
  super(scope, id, props)
15
24
 
16
25
  if (!props?.config) {
17
- throw ('[DeploymentTargetStack] No template provided')
26
+ throw ('[DeploymentTargetStack] No config provided')
18
27
  }
19
28
 
20
- // TODO: Check if the bucket already exists and use it
21
- // instead of having the whole deployment fail
22
- const staticDeploymentTarget =
23
- new Bucket(this, 'StaticDeploymentTarget', {
24
- bucketName: props.config.awsStaticBucketName,
25
- blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
26
- removalPolicy: RemovalPolicy.RETAIN,
27
- autoDeleteObjects: false,
28
- versioned: true,
29
- cors: [{
30
- allowedHeaders: ['*'],
31
- allowedMethods: ['GET', 'PUT', 'POST' ],
32
- allowedOrigins: [ '*' ],
33
- exposeHeaders: []
34
- }]
35
- })
36
-
37
- const mediaBackupVault = new BackupVault(this, 'StaticMediaBackupVault', {
38
- backupVaultName: `${props.config.platformName}-static-media`,
39
- removalPolicy: RemovalPolicy.RETAIN
40
- })
41
-
42
- const mediaBackupPlan = BackupPlan.daily35DayRetention(this, 'StaticMediaBackupPlan', mediaBackupVault)
43
-
44
- mediaBackupPlan.addSelection('StaticDeploymentTargetBucket', {
45
- backupSelectionName: `${props.config.platformName}-s3-static-media`,
46
- resources: [BackupResource.fromArn(staticDeploymentTarget.bucketArn)],
47
- allowRestores: true
48
- })
49
-
50
- const mediaCDN = new Distribution(this, 'Media', {
51
- defaultBehavior: {
52
- origin: S3BucketOrigin.withOriginAccessControl(staticDeploymentTarget, { originPath: '/media' }),
53
- // S3 bucket CORS does not add headers on viewer responses; without this, browser fetch()
54
- // to the CDN is cross-origin and Chrome may block with net::ERR_BLOCKED_BY_ORB.
55
- responseHeadersPolicy:
56
- ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT
57
- },
58
- priceClass: PriceClass.PRICE_CLASS_100
59
- })
60
-
61
- const containerDeploymentTarget =
62
- new ContainerDeploymentTarget(this, 'ContainerDeploymentTarget', {
63
- config: props.config,
64
- bucket: staticDeploymentTarget
29
+ const containerDeploymentTarget = new ContainerDeploymentTarget(this, 'ContainerDeploymentTarget', {
30
+ config: {
31
+ ...props.config,
32
+ env: {
33
+ ...props.config.env,
34
+ MEDIA_REPOSITORY: props.bucket.bucketName,
35
+ MEDIA_CDN_DOMAIN_NAME: `https://${props.cdnDomainName}`
36
+ }
37
+ },
38
+ bucket: props.bucket
65
39
  })
66
40
 
67
41
  this.containerDeploymentTargetPublicIp = containerDeploymentTarget.instancePublicIp
68
- this.staticDeploymentTargetBucketName = staticDeploymentTarget.bucketName
69
- this.staticDeploymentTargetBucketArn = staticDeploymentTarget.bucketArn
70
-
71
- new CfnOutput(this, 'MediaCDNDomainName', {
72
- value: mediaCDN.domainName,
73
- description: 'Domain name of CDN distribution',
74
- exportName: 'MediaCDNDomainName'
75
- })
76
42
 
77
43
  new CfnOutput(this, 'ContainerDeploymentTargetPublicIp', {
78
44
  value: containerDeploymentTarget.instancePublicIp,
79
- description: 'Public ip of the ec2 instance',
80
- exportName: 'awsContainerDeploymentTargetPublicIp'
81
- })
82
-
83
- new CfnOutput(this, 'StaticDeploymentTargetBucketName', {
84
- value: staticDeploymentTarget.bucketName,
85
- description: 'Name of static deployment target bucket',
86
- exportName: 'awsStaticDeploymentTargetBucketName'
87
- })
88
-
89
- new CfnOutput(this, 'StaticDeploymentTargetBucketArn', {
90
- value: staticDeploymentTarget.bucketArn,
91
- description: 'Name of static deployment target bucket',
92
- exportName: 'awsStaticDeploymentTargetBucketArn'
45
+ description: 'Public IP of the EC2 instance (Elastic IP)',
46
+ exportName: `${props.config.platformName}-ContainerDeploymentTargetPublicIp`
93
47
  })
94
-
95
48
  }
96
49
  }
97
50
 
@@ -0,0 +1,40 @@
1
+ /* eslint-disable no-new */
2
+ const { Stage } = require('aws-cdk-lib')
3
+ const { StorageStaticStack } = require('./storage-static-stack')
4
+ const { DeploymentTargetStack } = require('./deployment-target-stack')
5
+ const { DnsStack } = require('./dns-stack')
6
+ const { SesStack } = require('./ses-stack')
7
+
8
+ /**
9
+ * PlatformStage groups all stacks that make up a single platform deployment.
10
+ * Using a Stage lets you target one platform at a time:
11
+ *
12
+ * cdk deploy 'ossybot/**' --profile ossybot
13
+ *
14
+ * Stack names within the stage are automatically prefixed with the platform name,
15
+ * e.g. ossybot/trust-ci, ossybot/storage-static, ossybot/deployment-target, etc.
16
+ */
17
+ class PlatformStage extends Stage {
18
+ constructor(scope, id, props) {
19
+ super(scope, id, props)
20
+
21
+ const storageStatic = new StorageStaticStack(this, 'storage-static', props)
22
+
23
+ const deploymentTarget = new DeploymentTargetStack(this, 'deployment-target', {
24
+ ...props,
25
+ bucket: storageStatic.bucket,
26
+ cdnDomainName: storageStatic.cdnDomainName
27
+ })
28
+
29
+ new DnsStack(this, 'dns', {
30
+ ...props,
31
+ containerDeploymentTargetPublicIp: deploymentTarget.containerDeploymentTargetPublicIp
32
+ })
33
+
34
+ new SesStack(this, 'ses', props)
35
+ }
36
+ }
37
+
38
+ module.exports = {
39
+ PlatformStage
40
+ }
@@ -0,0 +1,87 @@
1
+ /* eslint-disable no-new */
2
+ const { CfnOutput, Stack, RemovalPolicy } = require('aws-cdk-lib')
3
+ const { BackupPlan, BackupVault, BackupResource } = require('aws-cdk-lib/aws-backup')
4
+ const { Bucket, BlockPublicAccess } = require('aws-cdk-lib/aws-s3')
5
+ const { Distribution, PriceClass, ResponseHeadersPolicy } = require('aws-cdk-lib/aws-cloudfront')
6
+ const { S3BucketOrigin } = require('aws-cdk-lib/aws-cloudfront-origins')
7
+
8
+ /**
9
+ * StorageStaticStack provisions the S3 bucket used for static assets and media,
10
+ * a CloudFront CDN distribution serving the /media prefix, and a daily backup plan.
11
+ *
12
+ * This stack is intentionally kept separate from the deployment-target stack so that
13
+ * the long-lived storage resources are never touched when the EC2 instance is updated
14
+ * or replaced, avoiding CloudFormation cross-stack export conflicts.
15
+ *
16
+ * Exports (consumed by deployment-target-stack via cross-stack reference):
17
+ * - `bucket` — the S3 Bucket construct
18
+ * - `cdnDomainName` — the CloudFront distribution domain name
19
+ */
20
+ class StorageStaticStack extends Stack {
21
+ constructor(scope, id, props) {
22
+ super(scope, id, props)
23
+
24
+ if (!props?.config) {
25
+ throw ('[StorageStaticStack] No config provided')
26
+ }
27
+
28
+ const bucket = new Bucket(this, 'StaticDeploymentTarget', {
29
+ blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
30
+ removalPolicy: RemovalPolicy.RETAIN,
31
+ autoDeleteObjects: false,
32
+ versioned: true,
33
+ cors: [{
34
+ allowedHeaders: ['*'],
35
+ allowedMethods: ['GET', 'PUT', 'POST'],
36
+ allowedOrigins: ['*'],
37
+ exposeHeaders: []
38
+ }]
39
+ })
40
+
41
+ const backupVault = new BackupVault(this, 'StaticMediaBackupVault', {
42
+ removalPolicy: RemovalPolicy.RETAIN
43
+ })
44
+
45
+ const backupPlan = BackupPlan.daily35DayRetention(this, 'StaticMediaBackupPlan', backupVault)
46
+
47
+ backupPlan.addSelection('StaticDeploymentTargetBucket', {
48
+ resources: [BackupResource.fromArn(bucket.bucketArn)],
49
+ allowRestores: true
50
+ })
51
+
52
+ const mediaCDN = new Distribution(this, 'Media', {
53
+ defaultBehavior: {
54
+ origin: S3BucketOrigin.withOriginAccessControl(bucket, { originPath: '/media' }),
55
+ // S3 bucket CORS does not add headers on viewer responses; without this, browser fetch()
56
+ // to the CDN is cross-origin and Chrome may block with net::ERR_BLOCKED_BY_ORB.
57
+ responseHeadersPolicy: ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT
58
+ },
59
+ priceClass: PriceClass.PRICE_CLASS_100
60
+ })
61
+
62
+ this.bucket = bucket
63
+ this.cdnDomainName = mediaCDN.domainName
64
+
65
+ new CfnOutput(this, 'MediaCDNDomainName', {
66
+ value: mediaCDN.domainName,
67
+ description: 'Domain name of CDN distribution',
68
+ exportName: `${props.config.platformName}-MediaCDNDomainName`
69
+ })
70
+
71
+ new CfnOutput(this, 'StaticDeploymentTargetBucketName', {
72
+ value: bucket.bucketName,
73
+ description: 'Name of static deployment target bucket',
74
+ exportName: `${props.config.platformName}-StaticDeploymentTargetBucketName`
75
+ })
76
+
77
+ new CfnOutput(this, 'StaticDeploymentTargetBucketArn', {
78
+ value: bucket.bucketArn,
79
+ description: 'ARN of static deployment target bucket',
80
+ exportName: `${props.config.platformName}-StaticDeploymentTargetBucketArn`
81
+ })
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ StorageStaticStack
87
+ }
@@ -10,10 +10,7 @@ const { logError, logInfo } = require('../log')
10
10
  * @property {string} awsAccountId - Aws account id
11
11
  * @property {string=} awsRegion - ?
12
12
  * @property {string=} awsKeyPairName - ?
13
- * @property {string} awsRoleToAssume - ?
14
13
  * @property {string=} awsDeploymentSqsArn - ?
15
- *
16
- * @property {string} ciGithubActionsRepo - organisation/repoName
17
14
  */
18
15
 
19
16
  /**
@@ -1,87 +0,0 @@
1
- const core = require('@actions/core')
2
- const { STSClient, AssumeRoleWithWebIdentityCommand } = require('@aws-sdk/client-sts')
3
-
4
- const { logInfo, logError } = require('../log')
5
-
6
- /**
7
- * @class
8
- */
9
- class AwsCredentialsService {
10
-
11
- static resolveAwsCredentials(platformConfig) {
12
- // If awsRoleToAssume is present, then we assume we run in a github workflow
13
- // If awsRoleToAssume is not present, then we assume they are resolved localy by aws-sdk
14
- if (!platformConfig.awsRoleToAssume) {
15
- logInfo({ message: '[AwsCredentialsService] No aws role to assume was found, leaving auth logic to @aws-sdk package' })
16
- return Promise.resolve(undefined)
17
- }
18
-
19
- const stsClient = new STSClient({ region: platformConfig.awsRegion })
20
-
21
- logInfo({ message: '[AwsCredentialsService] Fetching GitHub ID token' })
22
- return core.getIDToken('sts.amazonaws.com')
23
- .then(webIdentityToken => {
24
- logInfo({ message: `[AwsCredentialsService] Attempting to resolve aws credentials by assuming the role: ${platformConfig.awsRoleToAssume}` })
25
- return stsClient.send(new AssumeRoleWithWebIdentityCommand({
26
- RoleArn: `arn:aws:iam::${platformConfig.awsAccountId}:role/${platformConfig.awsRoleToAssume}`,
27
- RoleSessionName: 'GitHubActions',
28
- DurationSeconds: 15 * 60,
29
- WebIdentityToken: webIdentityToken
30
- }))
31
- })
32
- .then(responseData => ({
33
- // Don't ask
34
- AccessKeyId: responseData.Credentials.AccessKeyId,
35
- SessionToken: responseData.Credentials.SessionToken,
36
- SecretAccessKey: responseData.Credentials.SecretAccessKey,
37
- accessKeyId: responseData.Credentials.AccessKeyId,
38
- sessionToken: responseData.Credentials.SessionToken,
39
- secretAccessKey: responseData.Credentials.SecretAccessKey
40
- }))
41
- .then(x => AwsCredentialsService.exportCredentialsToGithubWorkflow({ ...x, awsRegion: platformConfig.awsRegion }))
42
- .catch(error => {
43
- logError({ message: '[AwsCredentialsService] Could not resolve temporary credentials', error })
44
- return undefined
45
- })
46
- }
47
-
48
- static exportCredentialsToGithubWorkflow(params) {
49
- // Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets.
50
- // Setting the credentials as secrets masks them in Github Actions logs
51
- const { accessKeyId, secretAccessKey, sessionToken, awsRegion } = params
52
-
53
- // AWS_ACCESS_KEY_ID:
54
- // Specifies an AWS access key associated with an IAM user or role
55
- core.setSecret(accessKeyId)
56
- core.exportVariable('AWS_ACCESS_KEY_ID', accessKeyId)
57
-
58
- // AWS_SECRET_ACCESS_KEY:
59
- // Specifies the secret key associated with the access key. This is essentially the "password" for the access key.
60
- core.setSecret(secretAccessKey)
61
- core.exportVariable('AWS_SECRET_ACCESS_KEY', secretAccessKey)
62
-
63
- // AWS_SESSION_TOKEN:
64
- // Specifies the session token value that is required if you are using temporary security credentials.
65
- if (sessionToken) {
66
- core.setSecret(sessionToken)
67
- core.exportVariable('AWS_SESSION_TOKEN', sessionToken)
68
- } else if (process.env.AWS_SESSION_TOKEN) {
69
- // clear session token from previous credentials action
70
- core.exportVariable('AWS_SESSION_TOKEN', '')
71
- }
72
-
73
- if (awsRegion) {
74
- core.exportVariable('AWS_REGION', awsRegion)
75
- } else if (process.env.AWS_REGION) {
76
- // clear AWS_REGION from previous credentials action
77
- core.exportVariable('AWS_REGION', '')
78
- }
79
-
80
- return params
81
- }
82
-
83
- }
84
-
85
- module.exports = {
86
- AwsCredentialsService
87
- }
@@ -1,55 +0,0 @@
1
- const arg = require('arg')
2
- const { AwsCredentialsService } = require('./aws-credentials')
3
- const { PlatformTemplateService } = require('../template')
4
- const { PlatformConfigService } = require('../config')
5
- const { logInfo, logError } = require('../log')
6
-
7
- const resolveCredentials = options => {
8
- logInfo({ message: 'resolve-credentials' })
9
-
10
- const parsedArgs = arg({
11
- '--access-key-id': String,
12
- '--session-token': String,
13
- '--secret-access-key': String
14
- }, { argv: options })
15
-
16
- AwsCredentialsService.exportCredentialsToGithubWorkflow({
17
- accessKeyId: parsedArgs['--access-key-id'],
18
- sessionToken: parsedArgs['--session-token'],
19
- secretAccessKey: parsedArgs['--secret-access-key']
20
- })
21
- }
22
-
23
- const assumeRole = options => {
24
- logInfo({ message: 'assume-role' })
25
-
26
- const parsedArgs = arg({
27
- '--platforms': String,
28
- '--target-platform': String
29
- }, { argv: options })
30
-
31
- const platformName = parsedArgs['--target-platform'] || ''
32
-
33
- PlatformTemplateService.readFromFile(parsedArgs['--platforms'] || process.env.PLATFORMS)
34
- .then(templates => templates.map(PlatformConfigService.from))
35
- .then(configs => configs.find(x => x.platformName === platformName))
36
- .then(targetConfig => {
37
-
38
- if (!targetConfig) {
39
- return logError({ message: 'No configuration found' })
40
- }
41
-
42
- AwsCredentialsService.resolveAwsCredentials(targetConfig)
43
- .then(() => logInfo({ message: `Assumed role for ${targetConfig.platformName}` }))
44
-
45
- })
46
-
47
- }
48
-
49
- module.exports = {
50
- handler: ([command, ...options]) => {
51
- !!command
52
- ? { 'resolve-credentials': resolveCredentials, 'assume-role': assumeRole }[command](options)
53
- : logError({ message: 'No command provided' })
54
- }
55
- }
@@ -1 +0,0 @@
1
- module.exports = require('./aws-credentials')
package/src/index.cli.js DELETED
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env node
2
- /* eslint-disable global-require, no-unused-vars */
3
-
4
- const [_, __, handlerName, ...restArgs] = process.argv
5
-
6
- const loadHandler = {
7
- aws: () => require('./aws-credentials/cli.js'),
8
- template: () => require('./template/cli.js'),
9
- }[handlerName]
10
-
11
- !!loadHandler && loadHandler().handler(restArgs)
@@ -1,52 +0,0 @@
1
- // TODO: remove this service once ossy.se traffic flows through the platform runtime.
2
- // The platform runtime (ghcr.io/ossy-se/platform) is the long-term host for all
3
- // tenant sites including ossy.se. This service is a stopgap while the CMS-based
4
- // publish flow is validated in production.
5
-
6
- const systemdServiceFile = `
7
- [Unit]
8
- Description=Ossy Website (ossy.se)
9
- After=network.target docker.service
10
- Requires=docker.service
11
-
12
- [Service]
13
- EnvironmentFile=/etc/environment
14
- ExecStartPre=-/usr/bin/docker rm -f ossy-website-ossy
15
- ExecStartPre=/usr/bin/docker pull ghcr.io/ossy-se/website-ossy:latest
16
- ExecStart=/usr/bin/docker run --name ossy-website-ossy \\
17
- --network ossy-network \\
18
- -p 3002:3000 \\
19
- -e PORT=3000 \\
20
- -e OSSY_API_URL=http://ossy-api:3001 \\
21
- -e OSSY_COOKIE_SECRET=\${OSSY_COOKIE_SECRET} \\
22
- ghcr.io/ossy-se/website-ossy:latest
23
- ExecStop=/usr/bin/docker stop ossy-website-ossy
24
- Restart=on-failure
25
- RestartSec=5
26
-
27
- [Install]
28
- WantedBy=multi-user.target
29
- `
30
-
31
- /**
32
- * @class
33
- */
34
- class OssyWebsiteOssyService {
35
-
36
- static install() {
37
- return [`sudo tee /etc/systemd/system/ossy-website-ossy.service > /dev/null << 'EOF'\n${systemdServiceFile}\nEOF`]
38
- }
39
-
40
- static enable() {
41
- return ['sudo systemctl enable ossy-website-ossy']
42
- }
43
-
44
- static start() {
45
- return ['sudo systemctl start ossy-website-ossy']
46
- }
47
-
48
- }
49
-
50
- module.exports = {
51
- OssyWebsiteOssyService
52
- }
@@ -1,52 +0,0 @@
1
- // TODO: remove this service once www.plexus-sanitas.com traffic flows through the platform runtime.
2
- // The platform runtime (ghcr.io/ossy-se/platform) is the long-term host for all
3
- // tenant sites including plexus-sanitas. This service is a stopgap while the CMS-based
4
- // publish flow is validated in production.
5
-
6
- const systemdServiceFile = `
7
- [Unit]
8
- Description=Ossy Website (www.plexus-sanitas.com)
9
- After=network.target docker.service
10
- Requires=docker.service
11
-
12
- [Service]
13
- EnvironmentFile=/etc/environment
14
- ExecStartPre=-/usr/bin/docker rm -f ossy-website-plexus-sanitas
15
- ExecStartPre=/usr/bin/docker pull ghcr.io/ossy-se/website-plexus-sanitas:latest
16
- ExecStart=/usr/bin/docker run --name ossy-website-plexus-sanitas \\
17
- --network ossy-network \\
18
- -p 3003:3000 \\
19
- -e PORT=3000 \\
20
- -e OSSY_API_URL=http://ossy-api:3001 \\
21
- -e OSSY_COOKIE_SECRET=\${OSSY_COOKIE_SECRET} \\
22
- ghcr.io/ossy-se/website-plexus-sanitas:latest
23
- ExecStop=/usr/bin/docker stop ossy-website-plexus-sanitas
24
- Restart=on-failure
25
- RestartSec=5
26
-
27
- [Install]
28
- WantedBy=multi-user.target
29
- `
30
-
31
- /**
32
- * @class
33
- */
34
- class OssyWebsitePlexusSanitasService {
35
-
36
- static install() {
37
- return [`sudo tee /etc/systemd/system/ossy-website-plexus-sanitas.service > /dev/null << 'EOF'\n${systemdServiceFile}\nEOF`]
38
- }
39
-
40
- static enable() {
41
- return ['sudo systemctl enable ossy-website-plexus-sanitas']
42
- }
43
-
44
- static start() {
45
- return ['sudo systemctl start ossy-website-plexus-sanitas']
46
- }
47
-
48
- }
49
-
50
- module.exports = {
51
- OssyWebsitePlexusSanitasService
52
- }
@@ -1,76 +0,0 @@
1
- const { Stack, Duration, CfnOutput } = require('aws-cdk-lib')
2
- const {
3
- OpenIdConnectProvider,
4
- OpenIdConnectPrincipal,
5
- Role,
6
- Effect,
7
- PolicyDocument,
8
- PolicyStatement
9
- } = require('aws-cdk-lib/aws-iam')
10
-
11
- /**
12
- * @class
13
- */
14
- class TrustCiStack extends Stack {
15
- /**
16
- * Establishes trust between GithHub and Amazon Web Services.
17
- * This is needed so that we can interact with Amazon Web Services through
18
- * GitHub Actions without having to manually provide credentials every time we
19
- * run a workflow
20
- *
21
- * @param {object} scope - scope
22
- * @param {string} id - id
23
- * @param {PlatformTemplate} props - PlatformTemplate
24
- */
25
- constructor(scope, id, props) {
26
- super(scope, id, props)
27
-
28
- const provider = new OpenIdConnectProvider(this, 'GitHubProvider', {
29
- url: 'https://token.actions.githubusercontent.com',
30
- clientIds: ['sts.amazonaws.com']
31
- })
32
-
33
- const GitHubPrincipal = new OpenIdConnectPrincipal(provider)
34
- .withConditions({
35
- StringLike: {
36
- 'token.actions.githubusercontent.com:sub': `repo:${props.config.ciGithubActionsRepo}:*`
37
- },
38
- StringEquals: {
39
- 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com'
40
- }
41
- })
42
-
43
- // eslint-disable-next-line no-new
44
- const role = new Role(this, 'GitHubActionsRole', {
45
- assumedBy: GitHubPrincipal,
46
- description: 'Role assumed by GitHubPrincipal for deploying from CI using aws cdk',
47
- maxSessionDuration: Duration.hours(1),
48
- inlinePolicies: {
49
- CdkDeploymentPolicy: new PolicyDocument({
50
- assignSids: true,
51
- statements: [
52
- new PolicyStatement({
53
- effect: Effect.ALLOW,
54
- actions: ['sts:AssumeRole', 'sqs:SendMessage'],
55
- resources: [
56
- `arn:aws:iam::${props.config.awsAccountId}:role/cdk-*`,
57
- `arn:aws:sqs:${props.config.awsRegion}:${props.config.awsAccountId}:*`
58
- ]
59
- })
60
- ]
61
- })
62
- }
63
- })
64
-
65
- new CfnOutput(this, 'CiAwsRoleToAssume', {
66
- value: role.roleName,
67
- description: 'Name of role to be assumed in CI/CD pipelines on github',
68
- exportName: 'awsRoleToAssume'
69
- })
70
-
71
- }
72
- }
73
-
74
- module.exports = {
75
- TrustCiStack
76
- }
@@ -1,24 +0,0 @@
1
- const arg = require('arg')
2
- const { PlatformTemplateService } = require('./platform-template')
3
-
4
- const { logInfo, logError } = require('../log')
5
-
6
- const validate = options => {
7
- logInfo({ message: 'Running validate command' })
8
-
9
- const parsedArgs = arg({
10
- '--platforms': String,
11
- '-p': '--platforms'
12
- }, { argv: options })
13
-
14
- PlatformTemplateService.readFromFile(parsedArgs['--platforms'])
15
- .then(() => logInfo({ message: 'Template is valid' }))
16
- }
17
-
18
- module.exports = {
19
- handler: ([command, ...options]) => {
20
- !!command
21
- ? { validate }[command](options)
22
- : logError({ message: 'No command provided' })
23
- }
24
- }