diff --git a/package-lock.json b/package-lock.json index bb33cf3..981c5df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@pulumi/aws": "^6.66.3", + "@pulumi/aws-native": "^1.38.0", "@pulumi/awsx": "^2.21.0", "@pulumi/pulumi": "^3.146.0", "@pulumi/random": "^4.17.0", @@ -4806,6 +4807,16 @@ "mime": "^2.0.0" } }, + "node_modules/@pulumi/aws-native": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@pulumi/aws-native/-/aws-native-1.38.0.tgz", + "integrity": "sha512-XvxGif8qkZethAaVivsD+TmCsal22Ws8f9zkj+sz03KdsCX7gnjs4WcqYCCdh6e1vmD0kn9v751BD2eFRWUVJQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@pulumi/pulumi": "^3.142.0" + } + }, "node_modules/@pulumi/awsx": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/@pulumi/awsx/-/awsx-2.22.0.tgz", diff --git a/package.json b/package.json index a0cd057..90fd019 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "prettier": "@studion/prettier-config", "dependencies": { "@pulumi/aws": "^6.66.3", + "@pulumi/aws-native": "^1.38.0", "@pulumi/awsx": "^2.21.0", "@pulumi/pulumi": "^3.146.0", "@pulumi/random": "^4.17.0", diff --git a/src/v2/components/database/index.ts b/src/v2/components/database/index.ts new file mode 100644 index 0000000..695d12a --- /dev/null +++ b/src/v2/components/database/index.ts @@ -0,0 +1,260 @@ +import * as aws from '@pulumi/aws'; +import * as awsNative from '@pulumi/aws-native'; +import * as awsx from '@pulumi/awsx'; +import * as pulumi from '@pulumi/pulumi'; +import { Password } from '../../../components/password'; +import { commonTags } from '../../../constants'; + +export namespace Database { + export type Instance = { + dbName?: pulumi.Input; + engineVersion?: pulumi.Input; + multiAz?: pulumi.Input; + instanceClass?: pulumi.Input; + allowMajorVersionUpgrade?: pulumi.Input; + autoMinorVersionUpgrade?: pulumi.Input; + }; + + export type Credentials = { + username?: pulumi.Input; + password?: pulumi.Input; + }; + + export type Storage = { + allocatedStorage?: pulumi.Input; + maxAllocatedStorage?: pulumi.Input; + kmsKeyId?: pulumi.Input; + }; + + export type Args = Instance & + Credentials & + Storage & { + vpc: pulumi.Input; + enableMonitoring?: pulumi.Input; + applyImmediately?: pulumi.Input; + snapshotIdentifier?: pulumi.Input; + parameterGroupName?: pulumi.Input; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; + }; +} + +const defaults = { + multiAz: false, + applyImmediately: false, + allocatedStorage: 20, + maxAllocatedStorage: 100, + instanceClass: 'db.t4g.micro', + enableMonitoring: false, + allowMajorVersionUpgrade: false, + autoMinorVersionUpgrade: true, + engineVersion: '17.2', +}; + +export class Database extends pulumi.ComponentResource { + name: string; + instance: awsNative.rds.DbInstance; + vpc: pulumi.Output; + dbSubnetGroup: aws.rds.SubnetGroup; + dbSecurityGroup: aws.ec2.SecurityGroup; + password: Password; + kmsKeyId: pulumi.Output; + monitoringRole?: aws.iam.Role; + encryptedSnapshotCopy?: aws.rds.SnapshotCopy; + + constructor( + name: string, + args: Database.Args, + opts: pulumi.ComponentResourceOptions = {}, + ) { + super('studion:Database', name, {}, opts); + + this.name = name; + + const argsWithDefaults = Object.assign({}, defaults, args); + const { vpc, kmsKeyId, enableMonitoring, snapshotIdentifier } = + argsWithDefaults; + + this.vpc = pulumi.output(vpc); + this.dbSubnetGroup = this.createSubnetGroup(); + this.dbSecurityGroup = this.createSecurityGroup(); + + this.password = new Password( + `${this.name}-database-password`, + { value: args.password }, + { parent: this }, + ); + + this.kmsKeyId = kmsKeyId + ? pulumi.output(kmsKeyId) + : this.createEncryptionKey().arn; + + if (enableMonitoring) { + this.monitoringRole = this.createMonitoringRole(); + } + + if (snapshotIdentifier) { + this.encryptedSnapshotCopy = + this.createEncryptedSnapshotCopy(snapshotIdentifier); + } + + this.instance = this.createDatabaseInstance(argsWithDefaults); + + this.registerOutputs(); + } + + private createSubnetGroup() { + return new aws.rds.SubnetGroup( + `${this.name}-subnet-group`, + { + subnetIds: this.vpc.isolatedSubnetIds, + tags: commonTags, + }, + { parent: this }, + ); + } + + private createSecurityGroup() { + return new aws.ec2.SecurityGroup( + `${this.name}-security-group`, + { + vpcId: this.vpc.vpcId, + ingress: [ + { + protocol: 'tcp', + fromPort: 5432, + toPort: 5432, + cidrBlocks: [this.vpc.vpc.cidrBlock], + }, + ], + tags: commonTags, + }, + { parent: this }, + ); + } + + private createEncryptionKey() { + return new aws.kms.Key( + `${this.name}-rds-key`, + { + description: `${this.name} RDS encryption key`, + customerMasterKeySpec: 'SYMMETRIC_DEFAULT', + isEnabled: true, + keyUsage: 'ENCRYPT_DECRYPT', + multiRegion: false, + enableKeyRotation: true, + tags: commonTags, + }, + { parent: this }, + ); + } + + private createMonitoringRole() { + const monitoringRole = new aws.iam.Role( + `${this.name}-rds-monitoring`, + { + assumeRolePolicy: { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'monitoring.rds.amazonaws.com', + }, + }, + ], + }, + }, + { parent: this }, + ); + + new aws.iam.RolePolicyAttachment( + `${this.name}-rds-monitoring-role-attachment`, + { + role: monitoringRole.name, + policyArn: + 'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole', + }, + { parent: this }, + ); + + return monitoringRole; + } + + private createEncryptedSnapshotCopy( + snapshotIdentifier: Database.Args['snapshotIdentifier'], + ) { + const sourceDbSnapshotIdentifier = pulumi + .output(snapshotIdentifier) + .apply(snapshotIdentifier => + aws.rds.getSnapshot({ + dbSnapshotIdentifier: snapshotIdentifier, + }), + ).dbSnapshotArn; + + return new aws.rds.SnapshotCopy( + `${this.name}-encrypted-snapshot-copy`, + { + sourceDbSnapshotIdentifier, + targetDbSnapshotIdentifier: pulumi.interpolate`${snapshotIdentifier}-encrypted-copy`, + kmsKeyId: this.kmsKeyId, + }, + { parent: this }, + ); + } + + private createDatabaseInstance(args: Database.Args) { + const monitoringOptions = + args.enableMonitoring && this.monitoringRole + ? { + monitoringInterval: 60, + monitoringRoleArn: this.monitoringRole.arn, + enablePerformanceInsights: true, + performanceInsightsRetentionPeriod: 7, + } + : {}; + + const instance = new awsNative.rds.DbInstance( + `${this.name}-rds`, + { + dbInstanceIdentifier: `${this.name}-db-instance`, + engine: 'postgres', + engineVersion: args.engineVersion, + dbInstanceClass: args.instanceClass, + dbName: args.dbName, + masterUsername: args.username, + masterUserPassword: this.password.value, + dbSubnetGroupName: this.dbSubnetGroup.name, + vpcSecurityGroups: [this.dbSecurityGroup.id], + allocatedStorage: args.allocatedStorage?.toString(), + maxAllocatedStorage: args.maxAllocatedStorage, + multiAz: args.multiAz, + applyImmediately: args.applyImmediately, + allowMajorVersionUpgrade: args.allowMajorVersionUpgrade, + autoMinorVersionUpgrade: args.autoMinorVersionUpgrade, + kmsKeyId: this.kmsKeyId, + storageEncrypted: true, + publiclyAccessible: false, + preferredMaintenanceWindow: 'Mon:07:00-Mon:07:30', + preferredBackupWindow: '06:00-06:30', + backupRetentionPeriod: 14, + caCertificateIdentifier: 'rds-ca-rsa2048-g1', + dbParameterGroupName: args.parameterGroupName, + dbSnapshotIdentifier: + this.encryptedSnapshotCopy?.targetDbSnapshotIdentifier, + ...monitoringOptions, + tags: pulumi + .output(args.tags) + .apply(tags => [ + ...Object.entries({ ...commonTags, ...tags }).map( + ([key, value]) => ({ key, value }), + ), + ]), + }, + { parent: this, dependsOn: [this.password] }, + ); + return instance; + } +}