Skip to content

Commit 8bafae8

Browse files
committed
chore: wip
chore: wip
1 parent 8dfd81d commit 8bafae8

File tree

6 files changed

+478
-222
lines changed

6 files changed

+478
-222
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* eslint-disable no-new */
2+
import { NestedStack, RemovalPolicy, Tags, aws_backup as backup, aws_iam as iam, aws_s3 as s3, aws_s3_deployment as s3deploy } from 'aws-cdk-lib'
3+
import type { Construct } from 'constructs'
4+
import type { NestedCloudProps } from './index'
5+
6+
export class StorageStack extends NestedStack {
7+
constructor(scope: Construct, props: NestedCloudProps) {
8+
super(scope, 'Deploy', props)
9+
10+
new s3deploy.BucketDeployment(this, 'DeployWebsite', {
11+
sources: [s3deploy.Source.asset(this.websiteSource)],
12+
destinationBucket: props.publicBucket,
13+
distribution: props.cdn,
14+
distributionPaths: ['/*'],
15+
})
16+
}
17+
}

.stacks/core/cloud/src/cloud/docs.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,8 @@ import type { NestedStackProps } from 'aws-cdk-lib'
22
import { NestedStack } from 'aws-cdk-lib'
33
import type { Construct } from 'constructs'
44

5-
interface ResourceNestedStackProps extends NestedStackProps {
6-
env: {
7-
account: string
8-
region: string
9-
}
10-
}
11-
125
export class DocsStack extends NestedStack {
13-
constructor(scope: Construct, props: ResourceNestedStackProps) {
6+
constructor(scope: Construct, props: NestedStackProps) {
147
super(scope, 'Docs', props)
158
// ...
169
}

.stacks/core/cloud/src/cloud/index.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,53 @@
11
/* eslint-disable no-new */
22
import type { Construct } from 'constructs'
33
import { Stack } from 'aws-cdk-lib'
4-
import type { StackProps } from 'aws-cdk-lib'
4+
import type { NestedStackProps, StackProps, aws_certificatemanager as acm, aws_cloudfront as cloudfront, aws_route53 as route53, aws_s3 as s3, aws_wafv2 as wafv2 } from 'aws-cdk-lib'
55
import { CdnStack } from './cdn'
66
import { DocsStack } from './docs'
77
import { StorageStack } from './storage'
88

9-
interface EnvProps extends StackProps {
9+
export interface CloudProps extends StackProps {
1010
env: {
1111
account: string
1212
region: string
1313
}
14+
appName: string
15+
appEnv: string
16+
domain: string
17+
partialAppKey: string
18+
zone: route53.HostedZone
19+
certificate: acm.Certificate
20+
logBucket: s3.Bucket
21+
firewall: wafv2.CfnWebACL
22+
storage: {
23+
publicBucket: s3.Bucket
24+
accessPoint: s3.CfnAccessPoint | undefined
25+
}
26+
cdn: cloudfront.Distribution
27+
}
28+
29+
export interface NestedCloudProps extends NestedStackProps {
30+
env: {
31+
account: string
32+
region: string
33+
}
34+
appName: string
35+
appEnv: string
36+
domain: string
37+
partialAppKey: string
38+
zone: route53.HostedZone
39+
certificate: acm.Certificate
40+
logBucket: s3.Bucket
41+
firewall: wafv2.CfnWebACL
42+
storage: {
43+
publicBucket: s3.Bucket
44+
accessPoint: s3.CfnAccessPoint | undefined
45+
}
46+
cdn: cloudfront.Distribution
1447
}
1548

16-
export class Stacks extends Stack {
17-
constructor(scope: Construct, id: string, props: EnvProps) {
49+
export class Cloud extends Stack {
50+
constructor(scope: Construct, id: string, props: CloudProps) {
1851
super(scope, id, props)
1952

2053
// please beware: be careful changing the order of the stacks creation below
Lines changed: 224 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,230 @@
1-
import type { NestedStackProps } from 'aws-cdk-lib'
2-
import { NestedStack } from 'aws-cdk-lib'
1+
/* eslint-disable no-new */
2+
import { NestedStack, RemovalPolicy, Tags, aws_backup as backup, aws_iam as iam, aws_s3 as s3, aws_s3_deployment as s3deploy } from 'aws-cdk-lib'
33
import type { Construct } from 'constructs'
4-
5-
interface ResourceNestedStackProps extends NestedStackProps {
6-
env: {
7-
account: string
8-
region: string
9-
}
10-
}
4+
import type { NestedCloudProps } from './index'
115

126
export class StorageStack extends NestedStack {
13-
constructor(scope: Construct, props: ResourceNestedStackProps) {
7+
websiteSource: string
8+
bucketPrefix: string
9+
docsSource: string = '../../../storage/docs'
10+
publicBucket: s3.Bucket | s3.IBucket
11+
privateBucket: s3.Bucket | s3.IBucket
12+
logBucket: s3.Bucket | s3.IBucket
13+
14+
constructor(scope: Construct, props: NestedCloudProps) {
1415
super(scope, 'Storage', props)
15-
// ...
16+
this.websiteSource = config.app.docMode ? this.docsSource : '../../../storage/public'
17+
this.bucketPrefix = `${props.appName}-${props.appEnv}`
18+
19+
20+
}
21+
22+
async manageStorage() {
23+
// the bucketName should not contain the domainName because when the APP_URL is changed,
24+
// we want it to deploy properly, and this way we would not force a recreation of the
25+
// resources that contain the domain name
26+
this.storage.publicBucket = await this.getOrCreateBucket()
27+
// for each redirect, create a bucket & redirect it to the APP_URL
28+
config.dns.redirects?.forEach((redirect) => {
29+
// TODO: use string-ts function here instead
30+
const slug = redirect.split('.').map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join('') // creates a CamelCase slug from the redirect
31+
const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { domainName: redirect })
32+
const redirectBucket = new s3.Bucket(this, `RedirectBucket${slug}`, {
33+
bucketName: `${redirect}-redirect`,
34+
websiteRedirect: {
35+
hostName: this.domain,
36+
protocol: s3.RedirectProtocol.HTTPS,
37+
},
38+
removalPolicy: RemovalPolicy.DESTROY,
39+
autoDeleteObjects: true,
40+
})
41+
new route53.CnameRecord(this, `RedirectRecord${slug}`, {
42+
zone: hostedZone,
43+
recordName: 'redirect',
44+
domainName: redirectBucket.bucketWebsiteDomainName,
45+
})
46+
})
47+
48+
this.storage.privateBucket = await this.getOrCreateBucket('private')
49+
const bucketPrefix = `${this.appName}-${appEnv}`
50+
51+
this.storage.logBucket = new s3.Bucket(this, 'LogsBucket', {
52+
bucketName: `${bucketPrefix}-logs-${partialAppKey}`,
53+
removalPolicy: RemovalPolicy.DESTROY,
54+
autoDeleteObjects: true,
55+
blockPublicAccess: new s3.BlockPublicAccess({
56+
blockPublicAcls: false,
57+
ignorePublicAcls: true,
58+
blockPublicPolicy: true,
59+
restrictPublicBuckets: true,
60+
}),
61+
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
62+
})
63+
Tags.of(this.storage.logBucket).add('daily-backup', 'true')
64+
65+
const backupRole = this.createBackupRole()
66+
67+
// Daily 35 day retention
68+
const vault = new backup.BackupVault(this, 'BackupVault', {
69+
backupVaultName: `${this.appName}-${appEnv}-daily-backup-vault`,
70+
encryptionKey: this.encryptionKey,
71+
removalPolicy: RemovalPolicy.DESTROY,
72+
})
73+
const plan = backup.BackupPlan.daily35DayRetention(this, 'BackupPlan', vault)
74+
75+
plan.addSelection('Selection', {
76+
role: backupRole,
77+
resources: [backup.BackupResource.fromTag('daily-backup', 'true')],
78+
})
79+
80+
this.storage = {
81+
publicBucket: this.storage.publicBucket,
82+
privateBucket: this.storage.privateBucket,
83+
// emailBucket: this.storage.emailBucket,
84+
logBucket: this.storage.logBucket,
85+
}
86+
}
87+
88+
async getOrCreateBucket(type?: 'public' | 'private' | 'email'): Promise<s3.Bucket | s3.IBucket> {
89+
const bucketPrefix = `${this.appName}-${appEnv}`
90+
let bucket: s3.Bucket
91+
92+
if (type === 'private')
93+
bucket = this.handlePrivateBucket(bucketPrefix)
94+
else if (type === 'email')
95+
bucket = this.handleEmailBucket(bucketPrefix)
96+
else
97+
bucket = this.handlePublicBucket(bucketPrefix)
98+
99+
return bucket
100+
}
101+
102+
handlePublicBucket(bucketPrefix?: string): s3.Bucket {
103+
if (!bucketPrefix)
104+
bucketPrefix = `${this.appName}-${appEnv}`
105+
106+
const bucket = new s3.Bucket(this, 'PublicBucket', {
107+
bucketName: `${bucketPrefix}-${partialAppKey}`,
108+
versioned: true,
109+
autoDeleteObjects: true,
110+
removalPolicy: RemovalPolicy.DESTROY,
111+
encryption: s3.BucketEncryption.S3_MANAGED,
112+
})
113+
114+
Tags.of(bucket).add('daily-backup', 'true')
115+
116+
return bucket
117+
}
118+
119+
handlePrivateBucket(bucketPrefix?: string): s3.Bucket {
120+
if (!bucketPrefix)
121+
bucketPrefix = `${this.appName}-${appEnv}`
122+
123+
const bucket = new s3.Bucket(this, 'PrivateBucket', {
124+
bucketName: `${bucketPrefix}-private-${partialAppKey}`,
125+
versioned: true,
126+
removalPolicy: RemovalPolicy.DESTROY,
127+
autoDeleteObjects: true,
128+
encryption: s3.BucketEncryption.S3_MANAGED,
129+
enforceSSL: true,
130+
publicReadAccess: false,
131+
blockPublicAccess: {
132+
blockPublicAcls: true,
133+
blockPublicPolicy: true,
134+
ignorePublicAcls: true,
135+
restrictPublicBuckets: true,
136+
},
137+
})
138+
139+
Tags.of(bucket).add('daily-backup', 'true')
140+
141+
return bucket
142+
}
143+
144+
createBackupRole() {
145+
const backupRole = new iam.Role(this, 'BackupRole', {
146+
assumedBy: new iam.ServicePrincipal('backup.amazonaws.com'),
147+
})
148+
backupRole.addToPolicy(
149+
new iam.PolicyStatement({
150+
actions: [
151+
's3:GetInventoryConfiguration',
152+
's3:PutInventoryConfiguration',
153+
's3:ListBucketVersions',
154+
's3:ListBucket',
155+
's3:GetBucketVersioning',
156+
's3:GetBucketNotification',
157+
's3:PutBucketNotification',
158+
's3:GetBucketLocation',
159+
's3:GetBucketTagging',
160+
],
161+
resources: ['arn:aws:s3:::*'],
162+
sid: 'S3BucketBackupPermissions',
163+
}),
164+
)
165+
backupRole.addToPolicy(
166+
new iam.PolicyStatement({
167+
actions: [
168+
's3:GetObjectAcl',
169+
's3:GetObject',
170+
's3:GetObjectVersionTagging',
171+
's3:GetObjectVersionAcl',
172+
's3:GetObjectTagging',
173+
's3:GetObjectVersion',
174+
],
175+
resources: ['arn:aws:s3:::*/*'],
176+
sid: 'S3ObjectBackupPermissions',
177+
}),
178+
)
179+
backupRole.addToPolicy(
180+
new iam.PolicyStatement({
181+
actions: ['s3:ListAllMyBuckets'],
182+
resources: ['*'],
183+
sid: 'S3GlobalPermissions',
184+
}),
185+
)
186+
backupRole.addToPolicy(
187+
new iam.PolicyStatement({
188+
actions: ['s3:ListAllMyBuckets'],
189+
resources: ['*'],
190+
sid: 'S3GlobalPermissions',
191+
}),
192+
)
193+
backupRole.addToPolicy(
194+
new iam.PolicyStatement({
195+
actions: ['kms:Decrypt', 'kms:DescribeKey'],
196+
resources: ['*'],
197+
sid: 'KMSBackupPermissions',
198+
conditions: {
199+
StringLike: {
200+
'kms:ViaService': 's3.*.amazonaws.com',
201+
},
202+
},
203+
}),
204+
)
205+
backupRole.addToPolicy(
206+
new iam.PolicyStatement({
207+
actions: [
208+
'events:DescribeRule',
209+
'events:EnableRule',
210+
'events:PutRule',
211+
'events:DeleteRule',
212+
'events:PutTargets',
213+
'events:RemoveTargets',
214+
'events:ListTargetsByRule',
215+
'events:DisableRule',
216+
],
217+
resources: ['arn:aws:events:*:*:rule/AwsBackupManagedRule*'],
218+
sid: 'EventsPermissions',
219+
}),
220+
)
221+
backupRole.addToPolicy(
222+
new iam.PolicyStatement({
223+
actions: ['cloudwatch:GetMetricData', 'events:ListRules'],
224+
resources: ['*'],
225+
sid: 'EventsMetricsGlobalPermissions',
226+
}),
227+
)
228+
return backupRole
16229
}
17230
}

.stacks/core/cloud/src/deploy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import process from 'node:process'
33
import { config } from '@stacksjs/config'
44
import { env } from '@stacksjs/env'
55
import * as cdk from 'aws-cdk-lib'
6-
import { Stacks as StacksCloud } from './cloud/'
6+
import { Cloud } from './cloud/'
77

88
const app = new cdk.App()
99
const appEnv = config.app.env === 'local' ? 'dev' : config.app.env
@@ -21,7 +21,7 @@ const usEnv = {
2121
region,
2222
}
2323

24-
new StacksCloud(app, cloudName, {
24+
new Cloud(app, cloudName, {
2525
env: usEnv,
2626
})
2727

0 commit comments

Comments
 (0)