@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 +22 -0
- package/README.md +129 -34
- package/package.json +2 -6
- package/src/config/platform-config.js +18 -4
- package/src/infrastructure/cli.js +2 -18
- package/src/infrastructure/container-deployment-target/caddy.service.js +22 -29
- package/src/infrastructure/container-deployment-target/container-deployment-target.js +56 -17
- package/src/infrastructure/container-deployment-target/container-service.js +163 -0
- package/src/infrastructure/deployment-target-stack.js +29 -76
- package/src/infrastructure/platform-stage.js +40 -0
- package/src/infrastructure/storage-static-stack.js +87 -0
- package/src/template/platform-template.js +0 -3
- package/src/aws-credentials/aws-credentials.js +0 -87
- package/src/aws-credentials/cli.js +0 -55
- package/src/aws-credentials/index.js +0 -1
- package/src/index.cli.js +0 -11
- package/src/infrastructure/container-deployment-target/ossy-website-ossy.service.js +0 -52
- package/src/infrastructure/container-deployment-target/ossy-website-plexus-sanitas.service.js +0 -52
- package/src/infrastructure/trust-ci-stack.js +0 -76
- package/src/template/cli.js +0 -24
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {'http'|'tcp'} ServiceType
|
|
3
|
+
*
|
|
4
|
+
* @typedef {Object} HttpServiceConfig
|
|
5
|
+
* @property {'http'} [type] - defaults to 'http'
|
|
6
|
+
* @property {string} name - unique name, used for container name and systemd unit
|
|
7
|
+
* @property {string} domain - domain this service handles (Caddy block + Route53)
|
|
8
|
+
* @property {string} image - Docker image to run
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} TcpServiceConfig
|
|
11
|
+
* @property {'tcp'} type - marks this as a raw TCP/UDP service (no HTTP/Caddy)
|
|
12
|
+
* @property {string} name - unique name, used for container name and systemd unit
|
|
13
|
+
* @property {string} image - Docker image to run
|
|
14
|
+
* @property {number[]} ports - ports to expose directly (same inside and outside container)
|
|
15
|
+
* @property {'tcp'|'udp'|'both'} [protocol] - defaults to 'tcp'
|
|
16
|
+
*
|
|
17
|
+
* @typedef {HttpServiceConfig|TcpServiceConfig} ContainerServiceConfig
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const BASE_HTTP_PORT = 3002 // 3000 = platform runtime, 3001 = API
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// HTTP service — reverse-proxied through Caddy, container listens on port 3000
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
class HttpContainerService {
|
|
27
|
+
constructor({ name, domain, image }, httpIndex) {
|
|
28
|
+
this.name = name
|
|
29
|
+
this.domain = domain
|
|
30
|
+
this.image = image
|
|
31
|
+
this.hostPort = BASE_HTTP_PORT + httpIndex
|
|
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.hostPort}: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.hostPort}
|
|
69
|
+
}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Ports to open in the EC2 security group. HTTP services use Caddy on 80/443 — no extra rules needed. */
|
|
73
|
+
get securityGroupPorts() {
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get unitName() { return `${this.name}.service` }
|
|
78
|
+
install() { return [`sudo tee /etc/systemd/system/${this.unitName} > /dev/null << 'EOF'\n${this.systemdUnitFile}\nEOF`] }
|
|
79
|
+
enable() { return [`sudo systemctl enable ${this.name}`] }
|
|
80
|
+
start() { return [`sudo systemctl start ${this.name}`] }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// TCP/UDP service — bypasses Caddy, ports exposed directly on the host.
|
|
85
|
+
// Used for game servers and any other raw socket services.
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
class TcpContainerService {
|
|
89
|
+
constructor({ name, image, ports, protocol = 'tcp' }) {
|
|
90
|
+
this.name = name
|
|
91
|
+
this.image = image
|
|
92
|
+
this.ports = ports
|
|
93
|
+
this.protocol = protocol // 'tcp' | 'udp' | 'both'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get systemdUnitFile() {
|
|
97
|
+
const portMappings = this.ports.map(p => `-p ${p}:${p}`).join(' \\\n ')
|
|
98
|
+
return `
|
|
99
|
+
[Unit]
|
|
100
|
+
Description=Container service: ${this.name} (${this.protocol.toUpperCase()} ${this.ports.join(', ')})
|
|
101
|
+
After=network.target docker.service
|
|
102
|
+
Requires=docker.service
|
|
103
|
+
|
|
104
|
+
[Service]
|
|
105
|
+
EnvironmentFile=/etc/environment
|
|
106
|
+
ExecStartPre=-/usr/bin/docker rm -f ${this.name}
|
|
107
|
+
ExecStartPre=/usr/bin/docker pull ${this.image}
|
|
108
|
+
ExecStart=/usr/bin/docker run --name ${this.name} \\
|
|
109
|
+
--network ossy-network \\
|
|
110
|
+
${portMappings} \\
|
|
111
|
+
--env-file /etc/environment \\
|
|
112
|
+
${this.image}
|
|
113
|
+
ExecStop=/usr/bin/docker stop ${this.name}
|
|
114
|
+
Restart=on-failure
|
|
115
|
+
RestartSec=5
|
|
116
|
+
|
|
117
|
+
[Install]
|
|
118
|
+
WantedBy=multi-user.target
|
|
119
|
+
`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** TCP services bypass Caddy — no Caddy block. */
|
|
123
|
+
get caddyBlock() { return null }
|
|
124
|
+
|
|
125
|
+
/** Each port needs an explicit security group ingress rule. */
|
|
126
|
+
get securityGroupPorts() {
|
|
127
|
+
return this.ports.flatMap(port => {
|
|
128
|
+
if (this.protocol === 'both') return [{ port, protocol: 'tcp' }, { port, protocol: 'udp' }]
|
|
129
|
+
return [{ port, protocol: this.protocol }]
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get unitName() { return `${this.name}.service` }
|
|
134
|
+
install() { return [`sudo tee /etc/systemd/system/${this.unitName} > /dev/null << 'EOF'\n${this.systemdUnitFile}\nEOF`] }
|
|
135
|
+
enable() { return [`sudo systemctl enable ${this.name}`] }
|
|
136
|
+
start() { return [`sudo systemctl start ${this.name}`] }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Factory
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build service instances from the `services` array in platforms.json.
|
|
145
|
+
* HTTP services auto-assign host ports starting at 3002 (counted separately
|
|
146
|
+
* from TCP services so reordering TCP entries doesn't shift HTTP ports).
|
|
147
|
+
*
|
|
148
|
+
* @param {ContainerServiceConfig[]} services
|
|
149
|
+
* @returns {(HttpContainerService|TcpContainerService)[]}
|
|
150
|
+
*/
|
|
151
|
+
const fromConfig = (services = []) => {
|
|
152
|
+
let httpIndex = 0
|
|
153
|
+
return services.map(s => {
|
|
154
|
+
if (s.type === 'tcp') return new TcpContainerService(s)
|
|
155
|
+
return new HttpContainerService(s, httpIndex++)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
HttpContainerService,
|
|
161
|
+
TcpContainerService,
|
|
162
|
+
fromConfig
|
|
163
|
+
}
|
|
@@ -1,97 +1,50 @@
|
|
|
1
1
|
/* eslint-disable no-new */
|
|
2
|
-
const { CfnOutput, Stack
|
|
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
|
-
|
|
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
|
|
26
|
+
throw ('[DeploymentTargetStack] No config provided')
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
80
|
-
exportName:
|
|
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
|
-
}
|