diff --git a/Pulumi.mjs b/Pulumi.mjs index 993ed503..6c5038ae 100644 --- a/Pulumi.mjs +++ b/Pulumi.mjs @@ -1,18 +1,20 @@ import { createUploads } from './pulumi/uploads.mjs'; import { createVpc } from './pulumi/vpc.mjs'; +import { createAurora } from './pulumi/aurora.mjs'; import { createThumbnails } from './pulumi/thumbnails.mjs'; import { createLambdaProcessUploads } from './pulumi/lambda-process-uploads.mjs'; import { updateEnv } from './pulumi/env.mjs'; import * as pulumi from "@pulumi/pulumi"; const stack = pulumi.getStack(); -const { uploadsBucket, uploadsAccessKey } = createUploads(); -const thumbnailsBucket = createThumbnails(); -const processUploadsFunctionUrl = createLambdaProcessUploads(thumbnailsBucket); +const { uploadsBucket, uploadsAccessKey } = await createUploads(); +const thumbnailsBucket = await createThumbnails(); +const processUploadsFunctionUrl = await createLambdaProcessUploads(thumbnailsBucket); if (stack != 'dev') { - const vpc = createVpc(); + const { vpc, subnets } = await createVpc(); + const { auroraCluster } = await createAurora(vpc, subnets) } -updateEnv({ +await updateEnv({ stack, uploadsBucket, uploadsAccessKey, diff --git a/pulumi/aurora.mjs b/pulumi/aurora.mjs new file mode 100644 index 00000000..701a5450 --- /dev/null +++ b/pulumi/aurora.mjs @@ -0,0 +1,93 @@ +import * as aws from "@pulumi/aws"; +import { prefix, safeName } from './utils.mjs'; + +export const createAurora = async (vpc, subnets) => { + + const clusterSecurityGroup = new aws.ec2.SecurityGroup(prefix("aurora-sg"), { + vpcId: vpc.id, + description: "Allow traffic to Aurora", + ingress: [ + { protocol: "tcp", fromPort: 3306, toPort: 3306, cidrBlocks: ["0.0.0.0/0"] }, + ], + egress: [ + { protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] }, + ], + }); + + const clusterParameterGroup = new aws.rds.ClusterParameterGroup(prefix("aurora-mysql80-param-group"), { + family: "aurora-mysql8.0", + parameters: [ + { + name: "character_set_server", + value: "utf8", + }, + { + name: "collation_server", + value: "utf8_unicode_ci", + }, + { + name: "innodb_ft_min_token_size", + value: 2, + applyMethod: "pending-reboot", // Apply this change during next reboot + }, + { + name: "sql_mode", + value: "NO_ENGINE_SUBSTITUTION" + }, + ], + }); + + const clusterSubnetGroup = new aws.rds.SubnetGroup(prefix("aurora-subnet-group"), { + subnetIds: [subnets[0].id, subnets[1].id], + }); + + const databaseName = safeName(); + const username = "root"; + const password = "changeMeAfterTheFact"; + + + const auroraCluster = new aws.rds.Cluster(prefix('aurora-cluster'), { + clusterIdentifier: prefix('aurora-cluster'), + engine: "aurora-mysql", + engineMode: "provisioned", + databaseName: databaseName, + masterUsername: username, + masterPassword: password, + serverlessv2ScalingConfiguration: { + maxCapacity: 1, + minCapacity: 0.5, + }, + storageEncrypted: true, + finalSnapshotIdentifier: prefix("final-snap-shot"), + backupRetentionPeriod: 7, + vpcSecurityGroupIds: [clusterSecurityGroup.id], + dbClusterParameterGroupName: clusterParameterGroup, + dbSubnetGroupName: clusterSubnetGroup.name, + }); + + const region = await aws.getRegion({}); + + // Create a read instance + const readInstance = new aws.rds.ClusterInstance(prefix("aurora-read"), { + clusterIdentifier: auroraCluster.id, + identifierPrefix: prefix("aurora-read"), + instanceClass: "db.serverless", + engine: auroraCluster.engine, + engineVersion: auroraCluster.engineVersion, + availabilityZone: region.id + "b", // Distributing instances across AZs + }); + + // Create a write instance + const writeInstance = new aws.rds.ClusterInstance(prefix("aurora-write"), { + clusterIdentifier: auroraCluster.id, + identifierPrefix: prefix("aurora-write"), + instanceClass: "db.serverless", + engine: auroraCluster.engine, + engineVersion: auroraCluster.engineVersion, + preferredMaintenanceWindow: "sun:23:45-mon:00:15", + availabilityZone: region.id + "a", // Distributing instances across AZs + }); + + + return { auroraCluster, clusterParameterGroup, clusterSecurityGroup }; +} \ No newline at end of file diff --git a/pulumi/env.mjs b/pulumi/env.mjs index 26894efd..bce3c207 100644 --- a/pulumi/env.mjs +++ b/pulumi/env.mjs @@ -4,7 +4,7 @@ import * as pulumi from "@pulumi/pulumi"; // this has to be its own file rather than each function updating itself, because of the paralleization of pulumi causing a race condition for the .env file -export const updateEnv = (obj) => { +export const updateEnv = async (obj) => { const env = new Env(obj.stack == 'dev' ? '.env' : '.env.' + obj.stack); aws.getRegion({}).then(region => env.set('AWS_REGION', region.id)); obj.uploadsAccessKey.id.apply(id => env.set("VING_AWS_UPLOADS_KEY", id)); diff --git a/pulumi/lambda-process-uploads.mjs b/pulumi/lambda-process-uploads.mjs index e2c8f8e7..2947f130 100644 --- a/pulumi/lambda-process-uploads.mjs +++ b/pulumi/lambda-process-uploads.mjs @@ -4,7 +4,7 @@ import * as pulumi from "@pulumi/pulumi"; import { execSync } from 'child_process'; -export const createLambdaProcessUploads = (thumbnailsBucket) => { +export const createLambdaProcessUploads = async (thumbnailsBucket) => { execSync('./create.nodemods.layer.sh', { cwd: './pulumi/aws/lambda/layer/nodemods' }); diff --git a/pulumi/thumbnails.mjs b/pulumi/thumbnails.mjs index 48cd661c..ba0fa975 100644 --- a/pulumi/thumbnails.mjs +++ b/pulumi/thumbnails.mjs @@ -2,7 +2,7 @@ import * as aws from "@pulumi/aws"; import { prefix } from './utils.mjs'; import * as pulumi from "@pulumi/pulumi"; -export const createThumbnails = () => { +export const createThumbnails = async () => { const thumbnailsBucket = new aws.s3.BucketV2(prefix('thumbnails'), {}); diff --git a/pulumi/uploads.mjs b/pulumi/uploads.mjs index 9a5f1d2e..9a5ef08a 100644 --- a/pulumi/uploads.mjs +++ b/pulumi/uploads.mjs @@ -2,7 +2,7 @@ import * as aws from "@pulumi/aws"; import { prefix } from './utils.mjs'; import * as pulumi from "@pulumi/pulumi"; -export const createUploads = () => { +export const createUploads = async () => { const uploadsBucket = new aws.s3.BucketV2(prefix('uploads'), {}); diff --git a/pulumi/utils.mjs b/pulumi/utils.mjs index 0a953536..2bb486f9 100644 --- a/pulumi/utils.mjs +++ b/pulumi/utils.mjs @@ -1,5 +1,11 @@ import * as pulumi from "@pulumi/pulumi"; +export const safeName = () => { + return `${pulumi.getProject()}${pulumi.getStack()}`.toLowerCase().replace(/\s+/g, ''); +} + export const prefix = (name) => { - return `${pulumi.getProject()}-${pulumi.getStack()}-${name}`; + if (name) + return `${pulumi.getProject()}-${pulumi.getStack()}-${name}`; + return `${pulumi.getProject()}-${pulumi.getStack()}`; } \ No newline at end of file diff --git a/pulumi/vpc.mjs b/pulumi/vpc.mjs index 1ef4c02d..5bc9f536 100644 --- a/pulumi/vpc.mjs +++ b/pulumi/vpc.mjs @@ -1,7 +1,7 @@ import * as aws from "@pulumi/aws"; import { prefix } from './utils.mjs'; -export const createVpc = () => { +export const createVpc = async () => { const vpc = new aws.ec2.Vpc(prefix('vpc'), { cidrBlock: "10.0.0.0/16", @@ -12,5 +12,19 @@ export const createVpc = () => { }, }); - return vpc; + const region = await aws.getRegion({}); + + const subnet1 = new aws.ec2.Subnet(prefix('subnet1'), { + vpcId: vpc.id, + cidrBlock: "10.0.1.0/24", + availabilityZone: region.id + "a", + }); + + const subnet2 = new aws.ec2.Subnet(prefix('subnet2'), { + vpcId: vpc.id, + cidrBlock: "10.0.2.0/24", + availabilityZone: region.id + "b", + }); + + return { vpc, subnets: [subnet1, subnet2] }; } \ No newline at end of file diff --git a/ving/docs/change-log.md b/ving/docs/change-log.md index 5f8ac716..e6cf7979 100644 --- a/ving/docs/change-log.md +++ b/ving/docs/change-log.md @@ -8,6 +8,7 @@ outline: deep ### 2024-08-02 * Added Pulumi prod. * Pulumi prod creates a VPC. +* Pulumi prod creates a a database cluster. ## July 2024 diff --git a/ving/docs/subsystems/pulumi.md b/ving/docs/subsystems/pulumi.md index ebd07c00..8cb22d67 100644 --- a/ving/docs/subsystems/pulumi.md +++ b/ving/docs/subsystems/pulumi.md @@ -46,4 +46,13 @@ Prod does the following things differently than dev: - stores its AWS generated variables in .env.prod - provisions a VPC - provisions an Aurora Serverless database -- provisions an EC2 instance \ No newline at end of file +- provisions an EC2 instance + +### Deploying Prod (or other stacks) +After deploying prod, or other stacks, there are a few things you'll need to do to set it up: + + +#### Database +First by default it will create a database with the root user of `root` and the password `changeMeAfterTheFact`. You'll need to change this password in the AWS console. You can do this by going to the RDS console, clicking on the cluster, and then clicking on the "Modify" button. Then you can change the password. + +Second, you may also want to change the minimum and maximum capacity units of the Aurora cluster. You can do this by going to the RDS console, clicking on the cluster, and then clicking on the "Modify" button. Then you can change the minimum and maximum capacity units. \ No newline at end of file