Skip to content

Commit f72aa77

Browse files
committed
Autogenerate db password, enable secrets factory
1 parent b9966fb commit f72aa77

File tree

5 files changed

+105
-12
lines changed

5 files changed

+105
-12
lines changed

README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ type DatabaseService = {
9292
serviceName: string;
9393
dbName: pulumi.Input<string>;
9494
username: pulumi.Input<string>;
95-
password: pulumi.Input<string>;
95+
password?: pulumi.Input<string>;
9696
applyImmediately?: pulumi.Input<boolean>;
9797
skipFinalSnapshot?: pulumi.Input<boolean>;
9898
allocatedStorage?: pulumi.Input<number>;
@@ -131,7 +131,7 @@ export type WebServerService = {
131131
environment?:
132132
| aws.ecs.KeyValuePair[]
133133
| ((services: Services) => aws.ecs.KeyValuePair[]);
134-
secrets?: aws.ecs.Secret[];
134+
secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]);
135135
image: pulumi.Input<string>;
136136
port: pulumi.Input<number>;
137137
domain: pulumi.Input<string>;
@@ -203,6 +203,33 @@ const project = new studion.Project('demo-project', {
203203
});
204204
```
205205

206+
```ts
207+
const project = new studion.Project('demo-project', {
208+
environment: 'DEVELOPMENT',
209+
services: [
210+
{
211+
type: 'REDIS',
212+
serviceName: 'redis',
213+
dbName: 'test-db',
214+
},
215+
{
216+
type: 'WEB_SERVER',
217+
serviceName: 'api',
218+
image: imageUri,
219+
port: 3000,
220+
domain: 'api.my-domain.com',
221+
secrets: (services: Services) => {
222+
const redisServiceName = 'redis';
223+
const redis = services[redisServiceName];
224+
return [
225+
{ name: 'REDIS_PASSWORD', valueFrom: redis.passwordSecret.arn },
226+
];
227+
},
228+
},
229+
],
230+
});
231+
```
232+
206233
### Database
207234

208235
AWS RDS Postgres instance.
@@ -229,8 +256,8 @@ new Database(name: string, args: DatabaseArgs, opts?: pulumi.CustomResourceOptio
229256
type DatabaseArgs = {
230257
dbName: pulumi.Input<string>;
231258
username: pulumi.Input<string>;
232-
password: pulumi.Input<string>;
233259
vpc: awsx.ec2.Vpc;
260+
password?: pulumi.Input<string>;
234261
applyImmediately?: pulumi.Input<boolean>;
235262
skipFinalSnapshot?: pulumi.Input<boolean>;
236263
allocatedStorage?: pulumi.Input<number>;
@@ -242,6 +269,10 @@ type DatabaseArgs = {
242269
};
243270
```
244271

272+
If a password is not specified, it will be autogenerated and stored as a secret
273+
inside AWS Secret Manager. The secret will be available on the `Database` resource
274+
as `passwordSecret`.
275+
245276
### Redis
246277

247278
[Upstash](https://upstash.com) Redis instance.
@@ -283,6 +314,9 @@ interface RedisOptions extends pulumi.ComponentResourceOptions {
283314
}
284315
```
285316

317+
After creating the Redis resource, the `passwordSecret` AWS Secret Manager Secret
318+
will exist on the resource.
319+
286320
### Static Site
287321

288322
AWS S3 + Cloudfront static site.
@@ -467,4 +501,3 @@ const project = new studion.Project('demo-project', {
467501

468502
- [ ] Add worker service for executing tasks
469503
- [ ] Add MongoDB service
470-
- [ ] Make db username & password fields optional and autogenerate db username & password if they are not provided

src/components/database.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ export type DatabaseArgs = {
1111
* Username for the master DB user.
1212
*/
1313
username: pulumi.Input<string>;
14-
/**
15-
* Password for the master DB user.
16-
*/
17-
password: pulumi.Input<string>;
1814
/**
1915
* The awsx.ec2.Vpc resource.
2016
*/
2117
vpc: awsx.ec2.Vpc;
18+
/**
19+
* Password for the master DB user. If not specified, it will be autogenerated
20+
* and stored as a secret in AWS Secret Manager.
21+
*/
22+
password?: pulumi.Input<string>;
2223
/**
2324
* Specifies whether any database modifications are applied immediately, or during the next maintenance window. Default is false.
2425
*/
@@ -60,6 +61,7 @@ export class Database extends pulumi.ComponentResource {
6061
kms: aws.kms.Key;
6162
dbSubnetGroup: aws.rds.SubnetGroup;
6263
dbSecurityGroup: aws.ec2.SecurityGroup;
64+
passwordSecret?: aws.secretsmanager.Secret;
6365

6466
constructor(
6567
name: string,
@@ -68,6 +70,9 @@ export class Database extends pulumi.ComponentResource {
6870
) {
6971
super('studion:Database', name, {}, opts);
7072

73+
const project = pulumi.getProject();
74+
const stack = pulumi.getStack();
75+
7176
const argsWithDefaults = Object.assign({}, defaults, args);
7277

7378
this.dbSubnetGroup = new aws.rds.SubnetGroup(
@@ -107,6 +112,31 @@ export class Database extends pulumi.ComponentResource {
107112
{ parent: this },
108113
);
109114

115+
const password =
116+
argsWithDefaults.password ||
117+
aws.secretsmanager
118+
.getRandomPasswordOutput()
119+
.apply(res => res.randomPassword);
120+
121+
if (!argsWithDefaults.password) {
122+
this.passwordSecret = new aws.secretsmanager.Secret(
123+
`${name}-password-secret`,
124+
{
125+
name: `${stack}/${project}/DatabasePassword`,
126+
},
127+
{ parent: this },
128+
);
129+
130+
const passwordSecretValue = new aws.secretsmanager.SecretVersion(
131+
`${name}-password-secret-value`,
132+
{
133+
secretId: this.passwordSecret.id,
134+
secretString: password,
135+
},
136+
{ parent: this, dependsOn: [this.passwordSecret] },
137+
);
138+
}
139+
110140
this.instance = new aws.rds.Instance(
111141
`${name}-rds`,
112142
{
@@ -118,7 +148,7 @@ export class Database extends pulumi.ComponentResource {
118148
instanceClass: argsWithDefaults.instanceClass,
119149
dbName: argsWithDefaults.dbName,
120150
username: argsWithDefaults.username,
121-
password: argsWithDefaults.password,
151+
password,
122152
dbSubnetGroupName: this.dbSubnetGroup.name,
123153
vpcSecurityGroupIds: [this.dbSecurityGroup.id],
124154
storageEncrypted: true,

src/components/project.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ export type WebServerService = {
3232
environment?:
3333
| aws.ecs.KeyValuePair[]
3434
| ((services: Services) => aws.ecs.KeyValuePair[]);
35+
secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]);
3536
} & ServiceArgs &
36-
Omit<WebServerArgs, 'cluster' | 'vpc' | 'hostedZoneId' | 'environment'>;
37+
Omit<
38+
WebServerArgs,
39+
'cluster' | 'vpc' | 'hostedZoneId' | 'environment' | 'secrets'
40+
>;
3741

3842
export type ProjectArgs = {
3943
services: (
@@ -173,12 +177,15 @@ export class Project extends pulumi.ComponentResource {
173177
if (!this.cluster) return;
174178
if (!this.hostedZoneId) throw new MissingHostedZoneId(options.type);
175179

176-
const { serviceName, environment, ...ecsOptions } = options;
180+
const { serviceName, environment, secrets, ...ecsOptions } = options;
177181
const parsedEnv =
178182
typeof environment === 'function'
179183
? environment(this.services)
180184
: environment;
181185

186+
const parsedSecrets =
187+
typeof secrets === 'function' ? secrets(this.services) : secrets;
188+
182189
const service = new WebServer(
183190
serviceName,
184191
{
@@ -187,6 +194,7 @@ export class Project extends pulumi.ComponentResource {
187194
vpc: this.vpc,
188195
hostedZoneId: this.hostedZoneId,
189196
environment: parsedEnv,
197+
secrets: parsedSecrets,
190198
},
191199
{ parent: this },
192200
);

src/components/redis.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as pulumi from '@pulumi/pulumi';
22
import * as upstash from '@upstash/pulumi';
3+
import * as aws from '@pulumi/aws';
34

45
export type RedisArgs = {
56
/**
@@ -22,10 +23,15 @@ export interface RedisOptions extends pulumi.ComponentResourceOptions {
2223

2324
export class Redis extends pulumi.ComponentResource {
2425
instance: upstash.RedisDatabase;
26+
passwordSecret: aws.secretsmanager.Secret;
27+
username = 'default';
2528

2629
constructor(name: string, args: RedisArgs, opts: RedisOptions) {
2730
super('studion:Redis', name, {}, opts);
2831

32+
const project = pulumi.getProject();
33+
const stack = pulumi.getStack();
34+
2935
const argsWithDefaults = Object.assign({}, defaults, args);
3036

3137
this.instance = new upstash.RedisDatabase(
@@ -39,6 +45,23 @@ export class Redis extends pulumi.ComponentResource {
3945
{ provider: opts.provider, parent: this },
4046
);
4147

48+
this.passwordSecret = new aws.secretsmanager.Secret(
49+
`${name}-password-secret`,
50+
{
51+
name: `${stack}/${project}/RedisPassword`,
52+
},
53+
{ parent: this, dependsOn: [this.instance] },
54+
);
55+
56+
const passwordSecretValue = new aws.secretsmanager.SecretVersion(
57+
`${name}-password-secret-value`,
58+
{
59+
secretId: this.passwordSecret.id,
60+
secretString: this.instance.password,
61+
},
62+
{ parent: this, dependsOn: [this.passwordSecret] },
63+
);
64+
4265
this.registerOutputs();
4366
}
4467
}

src/components/static-site.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export class StaticSite extends pulumi.ComponentResource {
4444
const bucket = new aws.s3.Bucket(
4545
`${name}-bucket`,
4646
{
47-
bucket: name,
4847
website: {
4948
indexDocument: 'index.html',
5049
errorDocument: 'index.html',

0 commit comments

Comments
 (0)