From 311529520d99edfd52621914f61399b6a795c465 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 2 Nov 2023 10:16:57 +0100 Subject: [PATCH 01/58] Added mongo server --- src/components/mongo-server.ts | 400 +++++++++++++++++++++++++++++++++ src/components/project.ts | 44 +++- src/index.ts | 1 + 3 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 src/components/mongo-server.ts diff --git a/src/components/mongo-server.ts b/src/components/mongo-server.ts new file mode 100644 index 0000000..9a3cb2b --- /dev/null +++ b/src/components/mongo-server.ts @@ -0,0 +1,400 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws'; +import * as awsx from '@pulumi/awsx'; +import { CustomSize, Size } from '../types/size'; +import { PredefinedSize, commonTags } from '../constants'; +import { ContainerDefinition } from '@pulumi/aws/ecs'; + +const config = new pulumi.Config('aws'); +const awsRegion = config.require('region'); + +const assumeRolePolicy: aws.iam.PolicyDocument = { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Principal: { + Service: 'ecs-tasks.amazonaws.com', + }, + Effect: 'Allow', + Sid: '', + }, + ], +}; + +type RoleInlinePolicy = { + /** + * Name of the role policy. + */ + name?: pulumi.Input; + /** + * Policy document as a JSON formatted string. + */ + policy?: pulumi.Input; +}; + +export type MongoServerArgs = { + /** + * The ECR image used to start a container. + */ + image: pulumi.Input; + /** + * Exposed service port. + */ + port: pulumi.Input; + /** + * The aws.ecs.Cluster resource. + */ + cluster: aws.ecs.Cluster; + /** + * The awsx.ec2.Vpc resource. + */ + vpc: awsx.ec2.Vpc; + /** + * Number of instances of the task definition to place and keep running. Defaults to 1. + */ + desiredCount?: pulumi.Input; + /** + * Min capacity of the scalable target. Defaults to 1. + */ + minCount?: pulumi.Input; + /** + * Max capacity of the scalable target. Defaults to 10. + */ + maxCount?: pulumi.Input; + /** + * CPU and memory size used for running the container. Defaults to "small". + * Available predefined options are: + * - small (0.25 vCPU, 0.5 GB memory) + * - medium (0.5 vCPU, 1 GB memory) + * - large (1 vCPU memory, 2 GB memory) + * - xlarge (2 vCPU, 4 GB memory) + */ + size?: pulumi.Input; + /** + * The environment variables to pass to a container. Don't use this field for + * sensitive information such as passwords, API keys, etc. For that purpose, + * please use the `secrets` property. + * Defaults to []. + */ + environment?: aws.ecs.KeyValuePair[]; + /** + * The secrets to pass to the container. Defaults to []. + */ + secrets?: aws.ecs.Secret[]; + /** + * Path for the health check request. Defaults to "/healtcheck". + */ + healtCheckPath?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + /** + * A map of tags to assign to the resource. + */ + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +}; + +const defaults = { + desiredCount: 1, + minCount: 1, + maxCount: 10, + size: 'small', + environment: [], + secrets: [], + healtCheckPath: '/healtcheck', + taskExecutionRoleInlinePolicies: [], + taskRoleInlinePolicies: [], +}; + +export class MongoServer extends pulumi.ComponentResource { + name: string; + logGroup: aws.cloudwatch.LogGroup; + taskDefinition: aws.ecs.TaskDefinition; + service: aws.ecs.Service; + + constructor( + name: string, + args: MongoServerArgs, + opts: pulumi.ComponentResourceOptions = {}, + ) { + super('studion:MongoServer', name, {}, opts); + + this.name = name; + this.logGroup = this.createLogGroup(); + + this.taskDefinition = this.createTaskDefinition(args); + this.service = this.createEcsService(args); + this.enableAutoscaling(args); + + this.registerOutputs(); + } + + private createLogGroup() { + const logGroup = new aws.cloudwatch.LogGroup( + `${this.name}-log-group`, + { + retentionInDays: 14, + namePrefix: `/ecs/${this.name}-`, + tags: commonTags, + }, + { parent: this }, + ); + return logGroup; + } + + private createTaskDefinition(args: MongoServerArgs) { + const argsWithDefaults = Object.assign({}, defaults, args); + const stack = pulumi.getStack(); + + const secretManagerSecretsInlinePolicy = { + name: `${this.name}-secret-manager-access`, + policy: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Sid: 'AllowContainerToGetSecretManagerSecrets', + Effect: 'Allow', + Action: ['secretsmanager:GetSecretValue'], + Resource: '*', + }, + ], + }), + }; + + const taskExecutionRole = new aws.iam.Role( + `${this.name}-ecs-task-exec-role`, + { + namePrefix: `${this.name}-ecs-task-exec-role-`, + assumeRolePolicy, + managedPolicyArns: [ + 'arn:aws:iam::aws:policy/CloudWatchFullAccess', + 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess', + ], + inlinePolicies: [ + secretManagerSecretsInlinePolicy, + ...argsWithDefaults.taskExecutionRoleInlinePolicies, + ], + tags: commonTags, + }, + { parent: this }, + ); + + const execCmdInlinePolicy = { + name: `${this.name}-ecs-exec`, + policy: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Sid: 'AllowContainerToCreateECSExecSSMChannel', + Effect: 'Allow', + Action: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + ], + Resource: '*', + }, + ], + }), + }; + + const taskRole = new aws.iam.Role( + `${this.name}-ecs-task-role`, + { + namePrefix: `${this.name}-ecs-task-role-`, + assumeRolePolicy, + inlinePolicies: [ + execCmdInlinePolicy, + ...argsWithDefaults.taskRoleInlinePolicies, + ], + tags: commonTags, + }, + { parent: this }, + ); + + const parsedSize = pulumi.all([argsWithDefaults.size]).apply(([size]) => { + const mapCapabilities = ({ cpu, memory }: CustomSize) => ({ + cpu: String(cpu), + memory: String(memory), + }); + if (typeof size === 'string') { + return mapCapabilities(PredefinedSize[size]); + } + if (typeof size === 'object') { + return mapCapabilities(size); + } + throw Error('Incorrect EcsService size argument'); + }); + + const taskDefinition = new aws.ecs.TaskDefinition( + `${this.name}-task-definition`, + { + family: `${this.name}-task-definition-${stack}`, + networkMode: 'awsvpc', + executionRoleArn: taskExecutionRole.arn, + taskRoleArn: taskRole.arn, + cpu: parsedSize.cpu, + memory: parsedSize.memory, + requiresCompatibilities: ['FARGATE'], + containerDefinitions: pulumi + .all([ + this.name, + argsWithDefaults.image, + argsWithDefaults.port, + argsWithDefaults.environment, + argsWithDefaults.secrets, + this.logGroup.name, + awsRegion, + ]) + .apply( + ([ + containerName, + image, + port, + environment, + secrets, + logGroup, + region, + ]) => { + return JSON.stringify([ + { + readonlyRootFilesystem: false, + name: containerName, + image, + essential: true, + portMappings: [ + { + containerPort: port, + protocol: 'tcp', + }, + ], + logConfiguration: { + logDriver: 'awslogs', + options: { + 'awslogs-group': logGroup, + 'awslogs-region': region, + 'awslogs-stream-prefix': 'ecs', + }, + }, + environment, + secrets, + }, + ] as ContainerDefinition[]); + }, + ), + tags: { ...commonTags, ...argsWithDefaults.tags }, + }, + { parent: this }, + ); + + return taskDefinition; + } + + private createEcsService(args: MongoServerArgs) { + const argsWithDefaults = Object.assign({}, defaults, args); + + const serviceSecurityGroup = new aws.ec2.SecurityGroup( + `${this.name}-security-group`, + { + vpcId: argsWithDefaults.vpc.vpcId, + ingress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + securityGroups: [this.lbSecurityGroup.id], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + }, + ], + tags: commonTags, + }, + { parent: this }, + ); + + const service = new aws.ecs.Service( + `${this.name}-service`, + { + name: this.name, + cluster: argsWithDefaults.cluster.id, + launchType: 'FARGATE', + desiredCount: argsWithDefaults.desiredCount, + taskDefinition: this.taskDefinition.arn, + enableExecuteCommand: true, + networkConfiguration: { + assignPublicIp: true, + subnets: argsWithDefaults.vpc.publicSubnetIds, + securityGroups: [serviceSecurityGroup.id], + }, + tags: { ...commonTags, ...argsWithDefaults.tags }, + }, + { + parent: this, + dependsOn: [], + }, + ); + return service; + } + + private enableAutoscaling(args: MongoServerArgs) { + const argsWithDefaults = Object.assign({}, defaults, args); + + const autoscalingTarget = new aws.appautoscaling.Target( + `${this.name}-autoscale-target`, + { + minCapacity: argsWithDefaults.minCount, + maxCapacity: argsWithDefaults.maxCount, + resourceId: pulumi.interpolate`service/${argsWithDefaults.cluster.name}/${this.service.name}`, + serviceNamespace: 'ecs', + scalableDimension: 'ecs:service:DesiredCount', + tags: commonTags, + }, + { parent: this }, + ); + + const memoryAutoscalingPolicy = new aws.appautoscaling.Policy( + `${this.name}-memory-autoscale-policy`, + { + policyType: 'TargetTrackingScaling', + resourceId: autoscalingTarget.resourceId, + scalableDimension: autoscalingTarget.scalableDimension, + serviceNamespace: autoscalingTarget.serviceNamespace, + targetTrackingScalingPolicyConfiguration: { + predefinedMetricSpecification: { + predefinedMetricType: 'ECSServiceAverageMemoryUtilization', + }, + targetValue: 80, + }, + }, + { parent: this }, + ); + + const cpuAutoscalingPolicy = new aws.appautoscaling.Policy( + `${this.name}-cpu-autoscale-policy`, + { + policyType: 'TargetTrackingScaling', + resourceId: autoscalingTarget.resourceId, + scalableDimension: autoscalingTarget.scalableDimension, + serviceNamespace: autoscalingTarget.serviceNamespace, + targetTrackingScalingPolicyConfiguration: { + predefinedMetricSpecification: { + predefinedMetricType: 'ECSServiceAverageCPUUtilization', + }, + targetValue: 60, + }, + }, + { parent: this }, + ); + } +} diff --git a/src/components/project.ts b/src/components/project.ts index fbfe736..6704090 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -4,12 +4,13 @@ import * as awsx from '@pulumi/awsx'; import * as upstash from '@upstash/pulumi'; import { Database, DatabaseArgs } from './database'; import { WebServer, WebServerArgs } from './web-server'; +import { MongoServer, MongoServerArgs } from './mongo-server'; import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; -export type Service = Database | Redis | StaticSite | WebServer; +export type Service = Database | Redis | StaticSite | WebServer | MongoServer; export type Services = Record; type ServiceArgs = { @@ -40,12 +41,22 @@ export type WebServerService = { 'cluster' | 'vpc' | 'hostedZoneId' | 'environment' | 'secrets' >; +export type MongoServerService = { + type: 'MONGO_SERVER'; + environment?: + | aws.ecs.KeyValuePair[] + | ((services: Services) => aws.ecs.KeyValuePair[]); + secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); +} & ServiceArgs & + Omit; + export type ProjectArgs = { services: ( | DatabaseService | RedisService | StaticSiteService | WebServerService + | MongoServerService )[]; hostedZoneId?: pulumi.Input; enableSSMConnect?: pulumi.Input; @@ -113,7 +124,9 @@ export class Project extends pulumi.ComponentResource { private createServices(services: ProjectArgs['services']) { const hasRedisService = services.some(it => it.type === 'REDIS'); - const hasWebServerService = services.some(it => it.type === 'WEB_SERVER'); + const hasWebServerService = services.some( + it => it.type === 'WEB_SERVER' || it.type === 'MONGO_SERVER', + ); if (hasRedisService) this.createRedisPrerequisites(); if (hasWebServerService) this.createWebServerPrerequisites(); services.forEach(it => { @@ -121,6 +134,7 @@ export class Project extends pulumi.ComponentResource { if (it.type === 'REDIS') this.createRedisService(it); if (it.type === 'STATIC_SITE') this.createStaticSiteService(it); if (it.type === 'WEB_SERVER') this.createWebServerService(it); + if (it.type === 'MONGO_SERVER') this.createMongoServerService(it); }); } @@ -209,4 +223,30 @@ export class Project extends pulumi.ComponentResource { ); this.services[options.serviceName] = service; } + + private createMongoServerService(options: MongoServerService) { + if (!this.cluster) return; + + const { serviceName, environment, secrets, ...ecsOptions } = options; + const parsedEnv = + typeof environment === 'function' + ? environment(this.services) + : environment; + + const parsedSecrets = + typeof secrets === 'function' ? secrets(this.services) : secrets; + + const service = new MongoServer( + serviceName, + { + ...ecsOptions, + cluster: this.cluster, + vpc: this.vpc, + environment: parsedEnv, + secrets: parsedSecrets, + }, + { parent: this }, + ); + this.services[options.serviceName] = service; + } } diff --git a/src/index.ts b/src/index.ts index 7764d9a..50cdfa6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './components/web-server'; +export * from './components/mongo-server'; export * from './components/static-site'; export * from './components/database'; export * from './components/redis'; From 5edf471ead1bd92e6d65a9e0071a8a9655d533c4 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 2 Nov 2023 16:35:14 +0100 Subject: [PATCH 02/58] Removed autoscaling --- src/components/mongo-server.ts | 54 +--------------------------------- 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/src/components/mongo-server.ts b/src/components/mongo-server.ts index 9a3cb2b..d356b95 100644 --- a/src/components/mongo-server.ts +++ b/src/components/mongo-server.ts @@ -128,7 +128,6 @@ export class MongoServer extends pulumi.ComponentResource { this.taskDefinition = this.createTaskDefinition(args); this.service = this.createEcsService(args); - this.enableAutoscaling(args); this.registerOutputs(); } @@ -307,7 +306,7 @@ export class MongoServer extends pulumi.ComponentResource { fromPort: 0, toPort: 0, protocol: '-1', - securityGroups: [this.lbSecurityGroup.id], + cidrBlocks: ['0.0.0.0/0'], }, ], egress: [ @@ -346,55 +345,4 @@ export class MongoServer extends pulumi.ComponentResource { ); return service; } - - private enableAutoscaling(args: MongoServerArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - - const autoscalingTarget = new aws.appautoscaling.Target( - `${this.name}-autoscale-target`, - { - minCapacity: argsWithDefaults.minCount, - maxCapacity: argsWithDefaults.maxCount, - resourceId: pulumi.interpolate`service/${argsWithDefaults.cluster.name}/${this.service.name}`, - serviceNamespace: 'ecs', - scalableDimension: 'ecs:service:DesiredCount', - tags: commonTags, - }, - { parent: this }, - ); - - const memoryAutoscalingPolicy = new aws.appautoscaling.Policy( - `${this.name}-memory-autoscale-policy`, - { - policyType: 'TargetTrackingScaling', - resourceId: autoscalingTarget.resourceId, - scalableDimension: autoscalingTarget.scalableDimension, - serviceNamespace: autoscalingTarget.serviceNamespace, - targetTrackingScalingPolicyConfiguration: { - predefinedMetricSpecification: { - predefinedMetricType: 'ECSServiceAverageMemoryUtilization', - }, - targetValue: 80, - }, - }, - { parent: this }, - ); - - const cpuAutoscalingPolicy = new aws.appautoscaling.Policy( - `${this.name}-cpu-autoscale-policy`, - { - policyType: 'TargetTrackingScaling', - resourceId: autoscalingTarget.resourceId, - scalableDimension: autoscalingTarget.scalableDimension, - serviceNamespace: autoscalingTarget.serviceNamespace, - targetTrackingScalingPolicyConfiguration: { - predefinedMetricSpecification: { - predefinedMetricType: 'ECSServiceAverageCPUUtilization', - }, - targetValue: 60, - }, - }, - { parent: this }, - ); - } } From 26dc46b6327e1de611ae9e17de6fa1f44934d091 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 2 Nov 2023 16:35:23 +0100 Subject: [PATCH 03/58] Changed service name --- src/components/project.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/project.ts b/src/components/project.ts index 6704090..3dead59 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -42,7 +42,7 @@ export type WebServerService = { >; export type MongoServerService = { - type: 'MONGO_SERVER'; + type: 'MONGO'; environment?: | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); @@ -125,7 +125,7 @@ export class Project extends pulumi.ComponentResource { private createServices(services: ProjectArgs['services']) { const hasRedisService = services.some(it => it.type === 'REDIS'); const hasWebServerService = services.some( - it => it.type === 'WEB_SERVER' || it.type === 'MONGO_SERVER', + it => it.type === 'WEB_SERVER' || it.type === 'MONGO', ); if (hasRedisService) this.createRedisPrerequisites(); if (hasWebServerService) this.createWebServerPrerequisites(); @@ -134,7 +134,7 @@ export class Project extends pulumi.ComponentResource { if (it.type === 'REDIS') this.createRedisService(it); if (it.type === 'STATIC_SITE') this.createStaticSiteService(it); if (it.type === 'WEB_SERVER') this.createWebServerService(it); - if (it.type === 'MONGO_SERVER') this.createMongoServerService(it); + if (it.type === 'MONGO') this.createMongoServerService(it); }); } From f744227d4945a218b198383a59e5f8e83d9a9e7b Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 2 Nov 2023 18:29:01 +0100 Subject: [PATCH 04/58] Added persistent storage --- src/components/mongo-server.ts | 88 ++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/src/components/mongo-server.ts b/src/components/mongo-server.ts index d356b95..3e0a53c 100644 --- a/src/components/mongo-server.ts +++ b/src/components/mongo-server.ts @@ -114,6 +114,8 @@ export class MongoServer extends pulumi.ComponentResource { name: string; logGroup: aws.cloudwatch.LogGroup; taskDefinition: aws.ecs.TaskDefinition; + serviceSecurityGroup: aws.ec2.SecurityGroup; + mountTarget: aws.efs.MountTarget; service: aws.ecs.Service; constructor( @@ -126,6 +128,8 @@ export class MongoServer extends pulumi.ComponentResource { this.name = name; this.logGroup = this.createLogGroup(); + this.serviceSecurityGroup = this.createSecurityGroup(args); + this.mountTarget = this.createMountTarget(args); this.taskDefinition = this.createTaskDefinition(args); this.service = this.createEcsService(args); @@ -145,6 +149,48 @@ export class MongoServer extends pulumi.ComponentResource { return logGroup; } + private createSecurityGroup(args: MongoServerArgs) { + const argsWithDefaults = Object.assign({}, defaults, args); + return new aws.ec2.SecurityGroup( + `${this.name}-security-group`, + { + vpcId: argsWithDefaults.vpc.vpcId, + ingress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + }, + ], + tags: commonTags, + }, + { parent: this }, + ); + } + + private createMountTarget(args: MongoServerArgs) { + const efs = new aws.efs.FileSystem(`${this.name}-efs`, { + tags: { + Name: `${this.name}-data`, + }, + }); + + return new aws.efs.MountTarget(`${this.name}-mount-target`, { + fileSystemId: efs.id, + subnetId: args.vpc.publicSubnetIds[0], + securityGroups: [this.serviceSecurityGroup.id], + }); + } + private createTaskDefinition(args: MongoServerArgs) { const argsWithDefaults = Object.assign({}, defaults, args); const stack = pulumi.getStack(); @@ -272,6 +318,12 @@ export class MongoServer extends pulumi.ComponentResource { protocol: 'tcp', }, ], + mountPoints: [ + { + containerPath: '/data/db', + sourceVolume: `${this.name}-volume`, + }, + ], logConfiguration: { logDriver: 'awslogs', options: { @@ -286,6 +338,15 @@ export class MongoServer extends pulumi.ComponentResource { ] as ContainerDefinition[]); }, ), + volumes: [ + { + name: `${this.name}-volume`, + efsVolumeConfiguration: { + fileSystemId: this.mountTarget.fileSystemId, + transitEncryption: 'ENABLED', + }, + }, + ], tags: { ...commonTags, ...argsWithDefaults.tags }, }, { parent: this }, @@ -297,31 +358,6 @@ export class MongoServer extends pulumi.ComponentResource { private createEcsService(args: MongoServerArgs) { const argsWithDefaults = Object.assign({}, defaults, args); - const serviceSecurityGroup = new aws.ec2.SecurityGroup( - `${this.name}-security-group`, - { - vpcId: argsWithDefaults.vpc.vpcId, - ingress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - cidrBlocks: ['0.0.0.0/0'], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - cidrBlocks: ['0.0.0.0/0'], - }, - ], - tags: commonTags, - }, - { parent: this }, - ); - const service = new aws.ecs.Service( `${this.name}-service`, { @@ -334,7 +370,7 @@ export class MongoServer extends pulumi.ComponentResource { networkConfiguration: { assignPublicIp: true, subnets: argsWithDefaults.vpc.publicSubnetIds, - securityGroups: [serviceSecurityGroup.id], + securityGroups: [this.serviceSecurityGroup.id], }, tags: { ...commonTags, ...argsWithDefaults.tags }, }, From cab63df37fbe2d24fd9631eb99c2ef2064f9e01e Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 2 Nov 2023 18:36:56 +0100 Subject: [PATCH 05/58] Renamed Mongo service --- src/components/{mongo-server.ts => mongo.ts} | 16 ++++++------ src/components/project.ts | 26 ++++++++++---------- src/index.ts | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) rename src/components/{mongo-server.ts => mongo.ts} (96%) diff --git a/src/components/mongo-server.ts b/src/components/mongo.ts similarity index 96% rename from src/components/mongo-server.ts rename to src/components/mongo.ts index 3e0a53c..af68773 100644 --- a/src/components/mongo-server.ts +++ b/src/components/mongo.ts @@ -33,7 +33,7 @@ type RoleInlinePolicy = { policy?: pulumi.Input; }; -export type MongoServerArgs = { +export type MongoArgs = { /** * The ECR image used to start a container. */ @@ -110,7 +110,7 @@ const defaults = { taskRoleInlinePolicies: [], }; -export class MongoServer extends pulumi.ComponentResource { +export class Mongo extends pulumi.ComponentResource { name: string; logGroup: aws.cloudwatch.LogGroup; taskDefinition: aws.ecs.TaskDefinition; @@ -120,10 +120,10 @@ export class MongoServer extends pulumi.ComponentResource { constructor( name: string, - args: MongoServerArgs, + args: MongoArgs, opts: pulumi.ComponentResourceOptions = {}, ) { - super('studion:MongoServer', name, {}, opts); + super('studion:Mongo', name, {}, opts); this.name = name; this.logGroup = this.createLogGroup(); @@ -149,7 +149,7 @@ export class MongoServer extends pulumi.ComponentResource { return logGroup; } - private createSecurityGroup(args: MongoServerArgs) { + private createSecurityGroup(args: MongoArgs) { const argsWithDefaults = Object.assign({}, defaults, args); return new aws.ec2.SecurityGroup( `${this.name}-security-group`, @@ -177,7 +177,7 @@ export class MongoServer extends pulumi.ComponentResource { ); } - private createMountTarget(args: MongoServerArgs) { + private createMountTarget(args: MongoArgs) { const efs = new aws.efs.FileSystem(`${this.name}-efs`, { tags: { Name: `${this.name}-data`, @@ -191,7 +191,7 @@ export class MongoServer extends pulumi.ComponentResource { }); } - private createTaskDefinition(args: MongoServerArgs) { + private createTaskDefinition(args: MongoArgs) { const argsWithDefaults = Object.assign({}, defaults, args); const stack = pulumi.getStack(); @@ -355,7 +355,7 @@ export class MongoServer extends pulumi.ComponentResource { return taskDefinition; } - private createEcsService(args: MongoServerArgs) { + private createEcsService(args: MongoArgs) { const argsWithDefaults = Object.assign({}, defaults, args); const service = new aws.ecs.Service( diff --git a/src/components/project.ts b/src/components/project.ts index 3dead59..2a2c48b 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -4,13 +4,13 @@ import * as awsx from '@pulumi/awsx'; import * as upstash from '@upstash/pulumi'; import { Database, DatabaseArgs } from './database'; import { WebServer, WebServerArgs } from './web-server'; -import { MongoServer, MongoServerArgs } from './mongo-server'; +import { Mongo, MongoArgs } from './mongo'; import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; -export type Service = Database | Redis | StaticSite | WebServer | MongoServer; +export type Service = Database | Redis | StaticSite | WebServer | Mongo; export type Services = Record; type ServiceArgs = { @@ -41,14 +41,14 @@ export type WebServerService = { 'cluster' | 'vpc' | 'hostedZoneId' | 'environment' | 'secrets' >; -export type MongoServerService = { +export type MongoService = { type: 'MONGO'; environment?: | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); } & ServiceArgs & - Omit; + Omit; export type ProjectArgs = { services: ( @@ -56,7 +56,7 @@ export type ProjectArgs = { | RedisService | StaticSiteService | WebServerService - | MongoServerService + | MongoService )[]; hostedZoneId?: pulumi.Input; enableSSMConnect?: pulumi.Input; @@ -124,17 +124,17 @@ export class Project extends pulumi.ComponentResource { private createServices(services: ProjectArgs['services']) { const hasRedisService = services.some(it => it.type === 'REDIS'); - const hasWebServerService = services.some( - it => it.type === 'WEB_SERVER' || it.type === 'MONGO', - ); + const shouldCreateCluster = + services.some(it => it.type === 'WEB_SERVER' || it.type === 'MONGO') && + !this.cluster; if (hasRedisService) this.createRedisPrerequisites(); - if (hasWebServerService) this.createWebServerPrerequisites(); + if (shouldCreateCluster) this.createCluster(); services.forEach(it => { if (it.type === 'DATABASE') this.createDatabaseService(it); if (it.type === 'REDIS') this.createRedisService(it); if (it.type === 'STATIC_SITE') this.createStaticSiteService(it); if (it.type === 'WEB_SERVER') this.createWebServerService(it); - if (it.type === 'MONGO') this.createMongoServerService(it); + if (it.type === 'MONGO') this.createMongoService(it); }); } @@ -147,7 +147,7 @@ export class Project extends pulumi.ComponentResource { }); } - private createWebServerPrerequisites() { + private createCluster() { const stack = pulumi.getStack(); this.cluster = new aws.ecs.Cluster( `${this.name}-cluster`, @@ -224,7 +224,7 @@ export class Project extends pulumi.ComponentResource { this.services[options.serviceName] = service; } - private createMongoServerService(options: MongoServerService) { + private createMongoService(options: MongoService) { if (!this.cluster) return; const { serviceName, environment, secrets, ...ecsOptions } = options; @@ -236,7 +236,7 @@ export class Project extends pulumi.ComponentResource { const parsedSecrets = typeof secrets === 'function' ? secrets(this.services) : secrets; - const service = new MongoServer( + const service = new Mongo( serviceName, { ...ecsOptions, diff --git a/src/index.ts b/src/index.ts index 50cdfa6..d83bb87 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './components/web-server'; -export * from './components/mongo-server'; +export * from './components/mongo'; export * from './components/static-site'; export * from './components/database'; export * from './components/redis'; From d583221c73d4ca48636c17f51750d2362981ce86 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Fri, 3 Nov 2023 10:08:50 +0100 Subject: [PATCH 06/58] Added autoscaling --- src/components/mongo.ts | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index af68773..96854b9 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -50,6 +50,10 @@ export type MongoArgs = { * The awsx.ec2.Vpc resource. */ vpc: awsx.ec2.Vpc; + /** + * Enables auto scaling. + */ + enableAutoScaling: boolean; /** * Number of instances of the task definition to place and keep running. Defaults to 1. */ @@ -133,6 +137,8 @@ export class Mongo extends pulumi.ComponentResource { this.taskDefinition = this.createTaskDefinition(args); this.service = this.createEcsService(args); + if (args.enableAutoScaling) this.enableAutoscaling(args); + this.registerOutputs(); } @@ -381,4 +387,55 @@ export class Mongo extends pulumi.ComponentResource { ); return service; } + + private enableAutoscaling(args: MongoArgs) { + const argsWithDefaults = Object.assign({}, defaults, args); + + const autoscalingTarget = new aws.appautoscaling.Target( + `${this.name}-autoscale-target`, + { + minCapacity: argsWithDefaults.minCount, + maxCapacity: argsWithDefaults.maxCount, + resourceId: pulumi.interpolate`service/${argsWithDefaults.cluster.name}/${this.service.name}`, + serviceNamespace: 'ecs', + scalableDimension: 'ecs:service:DesiredCount', + tags: commonTags, + }, + { parent: this }, + ); + + const memoryAutoscalingPolicy = new aws.appautoscaling.Policy( + `${this.name}-memory-autoscale-policy`, + { + policyType: 'TargetTrackingScaling', + resourceId: autoscalingTarget.resourceId, + scalableDimension: autoscalingTarget.scalableDimension, + serviceNamespace: autoscalingTarget.serviceNamespace, + targetTrackingScalingPolicyConfiguration: { + predefinedMetricSpecification: { + predefinedMetricType: 'ECSServiceAverageMemoryUtilization', + }, + targetValue: 80, + }, + }, + { parent: this }, + ); + + const cpuAutoscalingPolicy = new aws.appautoscaling.Policy( + `${this.name}-cpu-autoscale-policy`, + { + policyType: 'TargetTrackingScaling', + resourceId: autoscalingTarget.resourceId, + scalableDimension: autoscalingTarget.scalableDimension, + serviceNamespace: autoscalingTarget.serviceNamespace, + targetTrackingScalingPolicyConfiguration: { + predefinedMetricSpecification: { + predefinedMetricType: 'ECSServiceAverageCPUUtilization', + }, + targetValue: 60, + }, + }, + { parent: this }, + ); + } } From 18f36de2520073e0532d05d040080832e21834fe Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Fri, 3 Nov 2023 11:10:27 +0100 Subject: [PATCH 07/58] Added vpc ingress rule --- src/components/mongo.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 96854b9..72591a0 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -163,10 +163,10 @@ export class Mongo extends pulumi.ComponentResource { vpcId: argsWithDefaults.vpc.vpcId, ingress: [ { - fromPort: 0, - toPort: 0, - protocol: '-1', - cidrBlocks: ['0.0.0.0/0'], + fromPort: 2049, + toPort: 2049, + protocol: 'tcp', + cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], }, ], egress: [ From c9edc5cb121f53022e26912fabf7ed4346a20e72 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Mon, 6 Nov 2023 10:33:28 +0100 Subject: [PATCH 08/58] Removed unused arguments --- src/components/mongo.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 72591a0..3023dfb 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -86,10 +86,6 @@ export type MongoArgs = { * The secrets to pass to the container. Defaults to []. */ secrets?: aws.ecs.Secret[]; - /** - * Path for the health check request. Defaults to "/healtcheck". - */ - healtCheckPath?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< pulumi.Input[] >; @@ -163,9 +159,9 @@ export class Mongo extends pulumi.ComponentResource { vpcId: argsWithDefaults.vpc.vpcId, ingress: [ { - fromPort: 2049, - toPort: 2049, - protocol: 'tcp', + fromPort: 0, + toPort: 0, + protocol: '-1', cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], }, ], From d79a9b85f744eb66821d6e0b2ef88618da4ab7f5 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Mon, 6 Nov 2023 10:59:49 +0100 Subject: [PATCH 09/58] Fixed tcp range --- src/components/mongo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 3023dfb..a3fb44c 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -160,8 +160,8 @@ export class Mongo extends pulumi.ComponentResource { ingress: [ { fromPort: 0, - toPort: 0, - protocol: '-1', + toPort: 65535, + protocol: 'tcp', cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], }, ], From 608952e1ad71ce71202789e0afbf08e771161f51 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Mon, 6 Nov 2023 14:29:03 +0100 Subject: [PATCH 10/58] Added mongo to private subnet --- src/components/mongo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index a3fb44c..207e7ef 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -188,7 +188,7 @@ export class Mongo extends pulumi.ComponentResource { return new aws.efs.MountTarget(`${this.name}-mount-target`, { fileSystemId: efs.id, - subnetId: args.vpc.publicSubnetIds[0], + subnetId: args.vpc.privateSubnetIds[0], securityGroups: [this.serviceSecurityGroup.id], }); } @@ -371,7 +371,7 @@ export class Mongo extends pulumi.ComponentResource { enableExecuteCommand: true, networkConfiguration: { assignPublicIp: true, - subnets: argsWithDefaults.vpc.publicSubnetIds, + subnets: [argsWithDefaults.vpc.privateSubnetIds[0]], securityGroups: [this.serviceSecurityGroup.id], }, tags: { ...commonTags, ...argsWithDefaults.tags }, From 2bbba8ca41850678f1bcf9c1f87c4bcb0c298f34 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Mon, 6 Nov 2023 15:55:12 +0100 Subject: [PATCH 11/58] Removed autoscaling --- src/components/mongo.ts | 57 ----------------------------------------- 1 file changed, 57 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 207e7ef..a922620 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -50,10 +50,6 @@ export type MongoArgs = { * The awsx.ec2.Vpc resource. */ vpc: awsx.ec2.Vpc; - /** - * Enables auto scaling. - */ - enableAutoScaling: boolean; /** * Number of instances of the task definition to place and keep running. Defaults to 1. */ @@ -133,8 +129,6 @@ export class Mongo extends pulumi.ComponentResource { this.taskDefinition = this.createTaskDefinition(args); this.service = this.createEcsService(args); - if (args.enableAutoScaling) this.enableAutoscaling(args); - this.registerOutputs(); } @@ -383,55 +377,4 @@ export class Mongo extends pulumi.ComponentResource { ); return service; } - - private enableAutoscaling(args: MongoArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - - const autoscalingTarget = new aws.appautoscaling.Target( - `${this.name}-autoscale-target`, - { - minCapacity: argsWithDefaults.minCount, - maxCapacity: argsWithDefaults.maxCount, - resourceId: pulumi.interpolate`service/${argsWithDefaults.cluster.name}/${this.service.name}`, - serviceNamespace: 'ecs', - scalableDimension: 'ecs:service:DesiredCount', - tags: commonTags, - }, - { parent: this }, - ); - - const memoryAutoscalingPolicy = new aws.appautoscaling.Policy( - `${this.name}-memory-autoscale-policy`, - { - policyType: 'TargetTrackingScaling', - resourceId: autoscalingTarget.resourceId, - scalableDimension: autoscalingTarget.scalableDimension, - serviceNamespace: autoscalingTarget.serviceNamespace, - targetTrackingScalingPolicyConfiguration: { - predefinedMetricSpecification: { - predefinedMetricType: 'ECSServiceAverageMemoryUtilization', - }, - targetValue: 80, - }, - }, - { parent: this }, - ); - - const cpuAutoscalingPolicy = new aws.appautoscaling.Policy( - `${this.name}-cpu-autoscale-policy`, - { - policyType: 'TargetTrackingScaling', - resourceId: autoscalingTarget.resourceId, - scalableDimension: autoscalingTarget.scalableDimension, - serviceNamespace: autoscalingTarget.serviceNamespace, - targetTrackingScalingPolicyConfiguration: { - predefinedMetricSpecification: { - predefinedMetricType: 'ECSServiceAverageCPUUtilization', - }, - targetValue: 60, - }, - }, - { parent: this }, - ); - } } From c01bef91ae808be8fe14c86f60cb4f14b5f6a681 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Mon, 6 Nov 2023 16:52:13 +0100 Subject: [PATCH 12/58] Added service discovery --- src/components/mongo.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index a922620..c9bdd4b 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -112,6 +112,7 @@ export class Mongo extends pulumi.ComponentResource { taskDefinition: aws.ecs.TaskDefinition; serviceSecurityGroup: aws.ec2.SecurityGroup; mountTarget: aws.efs.MountTarget; + serviceDiscovery: aws.servicediscovery.Service; service: aws.ecs.Service; constructor( @@ -127,6 +128,7 @@ export class Mongo extends pulumi.ComponentResource { this.serviceSecurityGroup = this.createSecurityGroup(args); this.mountTarget = this.createMountTarget(args); this.taskDefinition = this.createTaskDefinition(args); + this.serviceDiscovery = this.createServiceDiscovery(args); this.service = this.createEcsService(args); this.registerOutputs(); @@ -351,6 +353,31 @@ export class Mongo extends pulumi.ComponentResource { return taskDefinition; } + private createServiceDiscovery(args: MongoArgs) { + const privateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace( + `${this.name}-private-dns-namespace`, + { + vpc: args.vpc.vpcId, + }, + ); + + return new aws.servicediscovery.Service(`${this.name}-service-discovery`, { + dnsConfig: { + namespaceId: privateDnsNamespace.id, + dnsRecords: [ + { + ttl: 10, + type: 'A', + }, + ], + routingPolicy: 'MULTIVALUE', + }, + healthCheckCustomConfig: { + failureThreshold: 1, + }, + }); + } + private createEcsService(args: MongoArgs) { const argsWithDefaults = Object.assign({}, defaults, args); @@ -368,6 +395,9 @@ export class Mongo extends pulumi.ComponentResource { subnets: [argsWithDefaults.vpc.privateSubnetIds[0]], securityGroups: [this.serviceSecurityGroup.id], }, + serviceRegistries: { + registryArn: this.serviceDiscovery.arn, + }, tags: { ...commonTags, ...argsWithDefaults.tags }, }, { From 08fd849e1125bd39daafa6c4fbbde1e51effef59 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 7 Nov 2023 14:15:47 +0100 Subject: [PATCH 13/58] Removed unused variable --- src/components/mongo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index c9bdd4b..eba6ad9 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -101,7 +101,6 @@ const defaults = { size: 'small', environment: [], secrets: [], - healtCheckPath: '/healtcheck', taskExecutionRoleInlinePolicies: [], taskRoleInlinePolicies: [], }; From 6dd4ecb8033c6a4db4c121a58c2da6b8effa5e66 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 8 Nov 2023 13:23:25 +0100 Subject: [PATCH 14/58] Removed public IP --- src/components/mongo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index eba6ad9..3bb62a8 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -390,7 +390,6 @@ export class Mongo extends pulumi.ComponentResource { taskDefinition: this.taskDefinition.arn, enableExecuteCommand: true, networkConfiguration: { - assignPublicIp: true, subnets: [argsWithDefaults.vpc.privateSubnetIds[0]], securityGroups: [this.serviceSecurityGroup.id], }, From c701e4859c44181060c1af62f28ba33bac12483c Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 8 Nov 2023 13:24:37 +0100 Subject: [PATCH 15/58] Removed unused args --- src/components/mongo.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 3bb62a8..ef36289 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -54,14 +54,6 @@ export type MongoArgs = { * Number of instances of the task definition to place and keep running. Defaults to 1. */ desiredCount?: pulumi.Input; - /** - * Min capacity of the scalable target. Defaults to 1. - */ - minCount?: pulumi.Input; - /** - * Max capacity of the scalable target. Defaults to 10. - */ - maxCount?: pulumi.Input; /** * CPU and memory size used for running the container. Defaults to "small". * Available predefined options are: @@ -96,8 +88,6 @@ export type MongoArgs = { const defaults = { desiredCount: 1, - minCount: 1, - maxCount: 10, size: 'small', environment: [], secrets: [], From 1521662e578455e213371bad3da3b28218e3349a Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 8 Nov 2023 13:41:58 +0100 Subject: [PATCH 16/58] Renamed variable --- src/components/mongo.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index ef36289..de245e5 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -100,7 +100,7 @@ export class Mongo extends pulumi.ComponentResource { logGroup: aws.cloudwatch.LogGroup; taskDefinition: aws.ecs.TaskDefinition; serviceSecurityGroup: aws.ec2.SecurityGroup; - mountTarget: aws.efs.MountTarget; + persistentStorage: aws.efs.MountTarget; serviceDiscovery: aws.servicediscovery.Service; service: aws.ecs.Service; @@ -115,7 +115,7 @@ export class Mongo extends pulumi.ComponentResource { this.logGroup = this.createLogGroup(); this.serviceSecurityGroup = this.createSecurityGroup(args); - this.mountTarget = this.createMountTarget(args); + this.persistentStorage = this.createPersistentStorage(args); this.taskDefinition = this.createTaskDefinition(args); this.serviceDiscovery = this.createServiceDiscovery(args); this.service = this.createEcsService(args); @@ -164,7 +164,7 @@ export class Mongo extends pulumi.ComponentResource { ); } - private createMountTarget(args: MongoArgs) { + private createPersistentStorage(args: MongoArgs) { const efs = new aws.efs.FileSystem(`${this.name}-efs`, { tags: { Name: `${this.name}-data`, @@ -329,7 +329,7 @@ export class Mongo extends pulumi.ComponentResource { { name: `${this.name}-volume`, efsVolumeConfiguration: { - fileSystemId: this.mountTarget.fileSystemId, + fileSystemId: this.persistentStorage.fileSystemId, transitEncryption: 'ENABLED', }, }, From 014e2bffd26bf7aae9267dadd5b5c9497712c793 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 8 Nov 2023 13:45:00 +0100 Subject: [PATCH 17/58] Fixes --- src/components/mongo.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index de245e5..1c929d0 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -50,10 +50,6 @@ export type MongoArgs = { * The awsx.ec2.Vpc resource. */ vpc: awsx.ec2.Vpc; - /** - * Number of instances of the task definition to place and keep running. Defaults to 1. - */ - desiredCount?: pulumi.Input; /** * CPU and memory size used for running the container. Defaults to "small". * Available predefined options are: @@ -87,7 +83,6 @@ export type MongoArgs = { }; const defaults = { - desiredCount: 1, size: 'small', environment: [], secrets: [], @@ -113,7 +108,6 @@ export class Mongo extends pulumi.ComponentResource { this.name = name; this.logGroup = this.createLogGroup(); - this.serviceSecurityGroup = this.createSecurityGroup(args); this.persistentStorage = this.createPersistentStorage(args); this.taskDefinition = this.createTaskDefinition(args); @@ -295,7 +289,7 @@ export class Mongo extends pulumi.ComponentResource { ]) => { return JSON.stringify([ { - readonlyRootFilesystem: false, + readonlyRootFilesystem: true, name: containerName, image, essential: true, @@ -376,9 +370,8 @@ export class Mongo extends pulumi.ComponentResource { name: this.name, cluster: argsWithDefaults.cluster.id, launchType: 'FARGATE', - desiredCount: argsWithDefaults.desiredCount, + desiredCount: 1, taskDefinition: this.taskDefinition.arn, - enableExecuteCommand: true, networkConfiguration: { subnets: [argsWithDefaults.vpc.privateSubnetIds[0]], securityGroups: [this.serviceSecurityGroup.id], From 9df6ee53f412a5558cb9afd8a91556d0dd8073e8 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 8 Nov 2023 13:45:56 +0100 Subject: [PATCH 18/58] Renamed variable --- src/components/project.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/project.ts b/src/components/project.ts index 2a2c48b..ce37537 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -124,11 +124,11 @@ export class Project extends pulumi.ComponentResource { private createServices(services: ProjectArgs['services']) { const hasRedisService = services.some(it => it.type === 'REDIS'); - const shouldCreateCluster = + const shouldCreateEcsCluster = services.some(it => it.type === 'WEB_SERVER' || it.type === 'MONGO') && !this.cluster; if (hasRedisService) this.createRedisPrerequisites(); - if (shouldCreateCluster) this.createCluster(); + if (shouldCreateEcsCluster) this.createCluster(); services.forEach(it => { if (it.type === 'DATABASE') this.createDatabaseService(it); if (it.type === 'REDIS') this.createRedisService(it); From d32105bf9d4c32163976c6a253ad2863cf90bd2e Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 8 Nov 2023 14:04:26 +0100 Subject: [PATCH 19/58] Added missing cluster error --- src/components/project.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/project.ts b/src/components/project.ts index ce37537..7d45b8b 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -72,6 +72,13 @@ export class MissingHostedZoneId extends Error { } } +export class MissingCluster extends Error { + constructor() { + super('Cluster does not exist'); + this.name = this.constructor.name; + } +} + export class Project extends pulumi.ComponentResource { name: string; vpc: awsx.ec2.Vpc; @@ -197,7 +204,7 @@ export class Project extends pulumi.ComponentResource { } private createWebServerService(options: WebServerService) { - if (!this.cluster) return; + if (!this.cluster) throw new MissingCluster(); if (!this.hostedZoneId) throw new MissingHostedZoneId(options.type); const { serviceName, environment, secrets, ...ecsOptions } = options; @@ -225,7 +232,7 @@ export class Project extends pulumi.ComponentResource { } private createMongoService(options: MongoService) { - if (!this.cluster) return; + if (!this.cluster) throw new MissingCluster(); const { serviceName, environment, secrets, ...ecsOptions } = options; const parsedEnv = From 06270d7f17e561d5c2ea0eeb63c0acbb00b76b4f Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 00:18:08 +0100 Subject: [PATCH 20/58] Fixed system access --- src/components/mongo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 1c929d0..22cb5cb 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -289,7 +289,7 @@ export class Mongo extends pulumi.ComponentResource { ]) => { return JSON.stringify([ { - readonlyRootFilesystem: true, + readonlyRootFilesystem: false, name: containerName, image, essential: true, From 2269eac609c1275c8e728b9fe1b1f1c7bde89824 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 00:31:29 +0100 Subject: [PATCH 21/58] Added correct ports to inbound rules --- src/components/mongo.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 22cb5cb..e1e26ad 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -138,8 +138,14 @@ export class Mongo extends pulumi.ComponentResource { vpcId: argsWithDefaults.vpc.vpcId, ingress: [ { - fromPort: 0, - toPort: 65535, + fromPort: 27017, + toPort: 27017, + protocol: 'tcp', + cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], + }, + { + fromPort: 2049, + toPort: 2049, protocol: 'tcp', cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], }, From 662a210ca5040f1fea239d2f42eb29409004bb9e Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 01:12:41 +0100 Subject: [PATCH 22/58] Renamed service discovery and improved inbound rules --- src/components/mongo.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index e1e26ad..006a92f 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -138,8 +138,8 @@ export class Mongo extends pulumi.ComponentResource { vpcId: argsWithDefaults.vpc.vpcId, ingress: [ { - fromPort: 27017, - toPort: 27017, + fromPort: argsWithDefaults.port, + toPort: argsWithDefaults.port, protocol: 'tcp', cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], }, @@ -347,10 +347,11 @@ export class Mongo extends pulumi.ComponentResource { `${this.name}-private-dns-namespace`, { vpc: args.vpc.vpcId, + name: this.name, }, ); - return new aws.servicediscovery.Service(`${this.name}-service-discovery`, { + return new aws.servicediscovery.Service(`mongo-service`, { dnsConfig: { namespaceId: privateDnsNamespace.id, dnsRecords: [ From 5fc03675781aa9d58e7c06c18587da38c1bf1804 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 01:52:42 +0100 Subject: [PATCH 23/58] Extracted common arguments --- src/common/ecs.ts | 81 +++++++++++++++++++++++++++++++ src/components/mongo.ts | 93 ++++-------------------------------- src/components/project.ts | 5 +- src/components/web-server.ts | 80 ++----------------------------- 4 files changed, 95 insertions(+), 164 deletions(-) create mode 100644 src/common/ecs.ts diff --git a/src/common/ecs.ts b/src/common/ecs.ts new file mode 100644 index 0000000..16c2b8b --- /dev/null +++ b/src/common/ecs.ts @@ -0,0 +1,81 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws'; +import * as awsx from '@pulumi/awsx'; +import { Size } from '../types/size'; + +const config = new pulumi.Config('aws'); +export const awsRegion = config.require('region'); + +export const assumeRolePolicy: aws.iam.PolicyDocument = { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Principal: { + Service: 'ecs-tasks.amazonaws.com', + }, + Effect: 'Allow', + Sid: '', + }, + ], +}; + +export type RoleInlinePolicy = { + /** + * Name of the role policy. + */ + name?: pulumi.Input; + /** + * Policy document as a JSON formatted string. + */ + policy?: pulumi.Input; +}; + +export type EcsArgs = { + /** + * The ECR image used to start a container. + */ + image: pulumi.Input; + /** + * Exposed service port. + */ + port: pulumi.Input; + /** + * The aws.ecs.Cluster resource. + */ + cluster: aws.ecs.Cluster; + /** + * The awsx.ec2.Vpc resource. + */ + vpc: awsx.ec2.Vpc; + /** + * CPU and memory size used for running the container. Defaults to "small". + * Available predefined options are: + * - small (0.25 vCPU, 0.5 GB memory) + * - medium (0.5 vCPU, 1 GB memory) + * - large (1 vCPU memory, 2 GB memory) + * - xlarge (2 vCPU, 4 GB memory) + */ + size?: pulumi.Input; + /** + * The environment variables to pass to a container. Don't use this field for + * sensitive information such as passwords, API keys, etc. For that purpose, + * please use the `secrets` property. + * Defaults to []. + */ + environment?: aws.ecs.KeyValuePair[]; + /** + * The secrets to pass to the container. Defaults to []. + */ + secrets?: aws.ecs.Secret[]; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + /** + * A map of tags to assign to the resource. + */ + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +}; diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 006a92f..80bcd61 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,86 +1,9 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; -import * as awsx from '@pulumi/awsx'; -import { CustomSize, Size } from '../types/size'; +import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; - -const config = new pulumi.Config('aws'); -const awsRegion = config.require('region'); - -const assumeRolePolicy: aws.iam.PolicyDocument = { - Version: '2012-10-17', - Statement: [ - { - Action: 'sts:AssumeRole', - Principal: { - Service: 'ecs-tasks.amazonaws.com', - }, - Effect: 'Allow', - Sid: '', - }, - ], -}; - -type RoleInlinePolicy = { - /** - * Name of the role policy. - */ - name?: pulumi.Input; - /** - * Policy document as a JSON formatted string. - */ - policy?: pulumi.Input; -}; - -export type MongoArgs = { - /** - * The ECR image used to start a container. - */ - image: pulumi.Input; - /** - * Exposed service port. - */ - port: pulumi.Input; - /** - * The aws.ecs.Cluster resource. - */ - cluster: aws.ecs.Cluster; - /** - * The awsx.ec2.Vpc resource. - */ - vpc: awsx.ec2.Vpc; - /** - * CPU and memory size used for running the container. Defaults to "small". - * Available predefined options are: - * - small (0.25 vCPU, 0.5 GB memory) - * - medium (0.5 vCPU, 1 GB memory) - * - large (1 vCPU memory, 2 GB memory) - * - xlarge (2 vCPU, 4 GB memory) - */ - size?: pulumi.Input; - /** - * The environment variables to pass to a container. Don't use this field for - * sensitive information such as passwords, API keys, etc. For that purpose, - * please use the `secrets` property. - * Defaults to []. - */ - environment?: aws.ecs.KeyValuePair[]; - /** - * The secrets to pass to the container. Defaults to []. - */ - secrets?: aws.ecs.Secret[]; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; - /** - * A map of tags to assign to the resource. - */ - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; -}; +import { EcsArgs, assumeRolePolicy, awsRegion } from '../common/ecs'; const defaults = { size: 'small', @@ -101,7 +24,7 @@ export class Mongo extends pulumi.ComponentResource { constructor( name: string, - args: MongoArgs, + args: EcsArgs, opts: pulumi.ComponentResourceOptions = {}, ) { super('studion:Mongo', name, {}, opts); @@ -130,7 +53,7 @@ export class Mongo extends pulumi.ComponentResource { return logGroup; } - private createSecurityGroup(args: MongoArgs) { + private createSecurityGroup(args: EcsArgs) { const argsWithDefaults = Object.assign({}, defaults, args); return new aws.ec2.SecurityGroup( `${this.name}-security-group`, @@ -164,7 +87,7 @@ export class Mongo extends pulumi.ComponentResource { ); } - private createPersistentStorage(args: MongoArgs) { + private createPersistentStorage(args: EcsArgs) { const efs = new aws.efs.FileSystem(`${this.name}-efs`, { tags: { Name: `${this.name}-data`, @@ -178,7 +101,7 @@ export class Mongo extends pulumi.ComponentResource { }); } - private createTaskDefinition(args: MongoArgs) { + private createTaskDefinition(args: EcsArgs) { const argsWithDefaults = Object.assign({}, defaults, args); const stack = pulumi.getStack(); @@ -342,7 +265,7 @@ export class Mongo extends pulumi.ComponentResource { return taskDefinition; } - private createServiceDiscovery(args: MongoArgs) { + private createServiceDiscovery(args: EcsArgs) { const privateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace( `${this.name}-private-dns-namespace`, { @@ -368,7 +291,7 @@ export class Mongo extends pulumi.ComponentResource { }); } - private createEcsService(args: MongoArgs) { + private createEcsService(args: EcsArgs) { const argsWithDefaults = Object.assign({}, defaults, args); const service = new aws.ecs.Service( diff --git a/src/components/project.ts b/src/components/project.ts index 7d45b8b..fea8b3b 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -4,11 +4,12 @@ import * as awsx from '@pulumi/awsx'; import * as upstash from '@upstash/pulumi'; import { Database, DatabaseArgs } from './database'; import { WebServer, WebServerArgs } from './web-server'; -import { Mongo, MongoArgs } from './mongo'; +import { Mongo } from './mongo'; import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; +import { EcsArgs } from '../common/ecs'; export type Service = Database | Redis | StaticSite | WebServer | Mongo; export type Services = Record; @@ -48,7 +49,7 @@ export type MongoService = { | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); } & ServiceArgs & - Omit; + Omit; export type ProjectArgs = { services: ( diff --git a/src/components/web-server.ts b/src/components/web-server.ts index 9610148..e307677 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -1,65 +1,21 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; -import * as awsx from '@pulumi/awsx'; -import { CustomSize, Size } from '../types/size'; +import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; import { AcmCertificate } from './acm-certificate'; +import { EcsArgs, assumeRolePolicy, awsRegion } from '../common/ecs'; -const config = new pulumi.Config('aws'); -const awsRegion = config.require('region'); - -const assumeRolePolicy: aws.iam.PolicyDocument = { - Version: '2012-10-17', - Statement: [ - { - Action: 'sts:AssumeRole', - Principal: { - Service: 'ecs-tasks.amazonaws.com', - }, - Effect: 'Allow', - Sid: '', - }, - ], -}; - -export type RoleInlinePolicy = { - /** - * Name of the role policy. - */ - name?: pulumi.Input; - /** - * Policy document as a JSON formatted string. - */ - policy?: pulumi.Input; -}; - -export type WebServerArgs = { - /** - * The ECR image used to start a container. - */ - image: pulumi.Input; - /** - * Exposed service port. - */ - port: pulumi.Input; +export type WebServerArgs = EcsArgs & { /** * The domain which will be used to access the service. * The domain or subdomain must belong to the provided hostedZone. */ domain: pulumi.Input; - /** - * The aws.ecs.Cluster resource. - */ - cluster: aws.ecs.Cluster; /** * The ID of the hosted zone. */ hostedZoneId: pulumi.Input; - /** - * The awsx.ec2.Vpc resource. - */ - vpc: awsx.ec2.Vpc; /** * Number of instances of the task definition to place and keep running. Defaults to 1. */ @@ -72,40 +28,10 @@ export type WebServerArgs = { * Max capacity of the scalable target. Defaults to 10. */ maxCount?: pulumi.Input; - /** - * CPU and memory size used for running the container. Defaults to "small". - * Available predefined options are: - * - small (0.25 vCPU, 0.5 GB memory) - * - medium (0.5 vCPU, 1 GB memory) - * - large (1 vCPU memory, 2 GB memory) - * - xlarge (2 vCPU, 4 GB memory) - */ - size?: pulumi.Input; - /** - * The environment variables to pass to a container. Don't use this field for - * sensitive information such as passwords, API keys, etc. For that purpose, - * please use the `secrets` property. - * Defaults to []. - */ - environment?: aws.ecs.KeyValuePair[]; - /** - * The secrets to pass to the container. Defaults to []. - */ - secrets?: aws.ecs.Secret[]; /** * Path for the health check request. Defaults to "/healtcheck". */ healtCheckPath?: pulumi.Input; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; - /** - * A map of tags to assign to the resource. - */ - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; }; const defaults = { From 3e09d723aaa5dc03e6d3b0aab964465cb5a9f96a Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 08:59:06 +0100 Subject: [PATCH 24/58] Updated documentation --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index 99e817d..f77add2 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ type ProjectArgs = { | RedisService | StaticSiteService | WebServerService + | MongoService )[]; hostedZoneId?: pulumi.Input; enableSSMConnect?: pulumi.Input; @@ -116,6 +117,27 @@ type DatabaseService = { }; ``` +```ts +type MongoService = { + type: 'MONGO'; + serviceName: string; + environment?: + | aws.ecs.KeyValuePair[] + | ((services: Services) => aws.ecs.KeyValuePair[]); + secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); + image: pulumi.Input; + port: pulumi.Input; + size?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +}; +``` + ```ts export type RedisService = { type: 'REDIS'; @@ -410,6 +432,49 @@ export type WebServerArgs = { }; ``` +### Monogo + +AWS ECS Fargate mongo service. + +Features: + +- has persistent storage +- comes with service discovery namespace +- creates CloudWatch log group +- comes with predefined cpu and memory options: `small`, `medium`, `large`, `xlarge` + +
+ +```ts +new Mongo(name: string, args: EcsArgs, opts?: pulumi.ComponentResourceOptions ); +``` + +| Argument | Description | +| :------- | :--------------------------------------------: | +| name \* | The unique name of the resource. | +| args \* | The arguments to resource properties. | +| opts | Bag of options to control resource's behavior. | + +```ts + export type EcsArgs = { + type: 'MONGO'; + serviceName: string; + environment?: + | aws.ecs.KeyValuePair[] + | ((services: Services) => aws.ecs.KeyValuePair[]); + secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); + image: pulumi.Input; + port: pulumi.Input; + size?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +``` + #### Exec into running ECS task **Prerequisites** From e361a0b935dff0fb9eb734130df4711b26982b57 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 09:11:24 +0100 Subject: [PATCH 25/58] Added lifecycle policy to efs --- src/components/mongo.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 80bcd61..3cb6ac2 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -89,6 +89,11 @@ export class Mongo extends pulumi.ComponentResource { private createPersistentStorage(args: EcsArgs) { const efs = new aws.efs.FileSystem(`${this.name}-efs`, { + lifecyclePolicies: [ + { + transitionToPrimaryStorageClass: 'AFTER_1_ACCESS', + }, + ], tags: { Name: `${this.name}-data`, }, From a7eae291fc3d5363a61475cd1cf9447e1e0ad7bd Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 09:48:00 +0100 Subject: [PATCH 26/58] Added encryption to efs --- src/components/mongo.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 3cb6ac2..5ce17f8 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -88,10 +88,17 @@ export class Mongo extends pulumi.ComponentResource { } private createPersistentStorage(args: EcsArgs) { + const key = new aws.kms.Key(`${this.name}-kms-key`, { + deletionWindowInDays: 10, + }); + const efs = new aws.efs.FileSystem(`${this.name}-efs`, { + encrypted: true, + kmsKeyId: key.arn, lifecyclePolicies: [ { transitionToPrimaryStorageClass: 'AFTER_1_ACCESS', + transitionToIa: 'AFTER_30_DAYS', }, ], tags: { From 214659ce63970de18a6fa3ecf6ec33b80bcbf29d Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 09:51:38 +0100 Subject: [PATCH 27/58] Fixes --- src/components/mongo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 5ce17f8..434368e 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -98,7 +98,6 @@ export class Mongo extends pulumi.ComponentResource { lifecyclePolicies: [ { transitionToPrimaryStorageClass: 'AFTER_1_ACCESS', - transitionToIa: 'AFTER_30_DAYS', }, ], tags: { From dc6444aa8f9d45e4a49da8932a51887e21710561 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 10:06:33 +0100 Subject: [PATCH 28/58] Fixed mongo port mapping --- src/components/mongo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 434368e..096ae11 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -253,6 +253,7 @@ export class Mongo extends pulumi.ComponentResource { 'awslogs-stream-prefix': 'ecs', }, }, + command: ['mongod', '--port', port.toString()], environment, secrets, }, From 0c6eaeae46973ddb0f3a198ef2eeaed37e08e218 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 17:30:36 +0100 Subject: [PATCH 29/58] Moved ecs service --- src/{common => components}/ecs.ts | 0 src/components/mongo.ts | 2 +- src/components/project.ts | 2 +- src/components/web-server.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{common => components}/ecs.ts (100%) diff --git a/src/common/ecs.ts b/src/components/ecs.ts similarity index 100% rename from src/common/ecs.ts rename to src/components/ecs.ts diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 096ae11..3faf421 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -3,7 +3,7 @@ import * as aws from '@pulumi/aws'; import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; -import { EcsArgs, assumeRolePolicy, awsRegion } from '../common/ecs'; +import { EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; const defaults = { size: 'small', diff --git a/src/components/project.ts b/src/components/project.ts index fea8b3b..97f528c 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -9,7 +9,7 @@ import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; -import { EcsArgs } from '../common/ecs'; +import { EcsArgs } from './ecs'; export type Service = Database | Redis | StaticSite | WebServer | Mongo; export type Services = Record; diff --git a/src/components/web-server.ts b/src/components/web-server.ts index e307677..ad9396f 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -4,7 +4,7 @@ import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; import { AcmCertificate } from './acm-certificate'; -import { EcsArgs, assumeRolePolicy, awsRegion } from '../common/ecs'; +import { EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; export type WebServerArgs = EcsArgs & { /** From e5b1f92fa5ab6042bb87ec2c05aa64e8ddf16205 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 17:37:58 +0100 Subject: [PATCH 30/58] Added ecs service class --- src/components/ecs.ts | 33 +++++++++++++++++++++++++++++++++ src/components/mongo.ts | 23 +++-------------------- src/components/web-server.ts | 23 +++-------------------- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/src/components/ecs.ts b/src/components/ecs.ts index 16c2b8b..5b4ec3c 100644 --- a/src/components/ecs.ts +++ b/src/components/ecs.ts @@ -2,6 +2,7 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; import * as awsx from '@pulumi/awsx'; import { Size } from '../types/size'; +import { commonTags } from '../constants'; const config = new pulumi.Config('aws'); export const awsRegion = config.require('region'); @@ -79,3 +80,35 @@ export type EcsArgs = { [key: string]: pulumi.Input; }>; }; + +export class Ecs extends pulumi.ComponentResource { + name: string; + logGroup: aws.cloudwatch.LogGroup; + + constructor( + type: string, + name: string, + args: EcsArgs, + opts: pulumi.ComponentResourceOptions = {}, + ) { + super(type, name, {}, opts); + + this.name = name; + this.logGroup = this.createLogGroup(); + + this.registerOutputs(); + } + + private createLogGroup() { + const logGroup = new aws.cloudwatch.LogGroup( + `${this.name}-log-group`, + { + retentionInDays: 14, + namePrefix: `/ecs/${this.name}-`, + tags: commonTags, + }, + { parent: this }, + ); + return logGroup; + } +} diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 3faf421..688766b 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -3,7 +3,7 @@ import * as aws from '@pulumi/aws'; import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; -import { EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; +import { Ecs, EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; const defaults = { size: 'small', @@ -13,9 +13,7 @@ const defaults = { taskRoleInlinePolicies: [], }; -export class Mongo extends pulumi.ComponentResource { - name: string; - logGroup: aws.cloudwatch.LogGroup; +export class Mongo extends Ecs { taskDefinition: aws.ecs.TaskDefinition; serviceSecurityGroup: aws.ec2.SecurityGroup; persistentStorage: aws.efs.MountTarget; @@ -27,10 +25,8 @@ export class Mongo extends pulumi.ComponentResource { args: EcsArgs, opts: pulumi.ComponentResourceOptions = {}, ) { - super('studion:Mongo', name, {}, opts); + super('studion:Mongo', name, args, opts); - this.name = name; - this.logGroup = this.createLogGroup(); this.serviceSecurityGroup = this.createSecurityGroup(args); this.persistentStorage = this.createPersistentStorage(args); this.taskDefinition = this.createTaskDefinition(args); @@ -40,19 +36,6 @@ export class Mongo extends pulumi.ComponentResource { this.registerOutputs(); } - private createLogGroup() { - const logGroup = new aws.cloudwatch.LogGroup( - `${this.name}-log-group`, - { - retentionInDays: 14, - namePrefix: `/ecs/${this.name}-`, - tags: commonTags, - }, - { parent: this }, - ); - return logGroup; - } - private createSecurityGroup(args: EcsArgs) { const argsWithDefaults = Object.assign({}, defaults, args); return new aws.ec2.SecurityGroup( diff --git a/src/components/web-server.ts b/src/components/web-server.ts index ad9396f..4f2be42 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -4,7 +4,7 @@ import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; import { AcmCertificate } from './acm-certificate'; -import { EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; +import { Ecs, EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; export type WebServerArgs = EcsArgs & { /** @@ -46,10 +46,8 @@ const defaults = { taskRoleInlinePolicies: [], }; -export class WebServer extends pulumi.ComponentResource { - name: string; +export class WebServer extends Ecs { certificate: AcmCertificate; - logGroup: aws.cloudwatch.LogGroup; lbSecurityGroup: aws.ec2.SecurityGroup; lb: aws.lb.LoadBalancer; lbTargetGroup: aws.lb.TargetGroup; @@ -63,12 +61,10 @@ export class WebServer extends pulumi.ComponentResource { args: WebServerArgs, opts: pulumi.ComponentResourceOptions = {}, ) { - super('studion:WebServer', name, {}, opts); + super('studion:WebServer', name, args, opts); - this.name = name; const { domain, hostedZoneId, vpc, port, healtCheckPath } = args; this.certificate = this.createTlsCertificate({ domain, hostedZoneId }); - this.logGroup = this.createLogGroup(); const { lb, lbTargetGroup, @@ -104,19 +100,6 @@ export class WebServer extends pulumi.ComponentResource { return certificate; } - private createLogGroup() { - const logGroup = new aws.cloudwatch.LogGroup( - `${this.name}-log-group`, - { - retentionInDays: 14, - namePrefix: `/ecs/${this.name}-`, - tags: commonTags, - }, - { parent: this }, - ); - return logGroup; - } - private createLoadBalancer({ vpc, port, From c2d67027e20400e5107bc789eb8c8c36493ba93f Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 17:58:00 +0100 Subject: [PATCH 31/58] Extracted defaults --- src/components/ecs.ts | 12 ++++++++++++ src/components/mongo.ts | 10 +--------- src/components/web-server.ts | 14 +------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/components/ecs.ts b/src/components/ecs.ts index 5b4ec3c..db31b5e 100644 --- a/src/components/ecs.ts +++ b/src/components/ecs.ts @@ -81,6 +81,18 @@ export type EcsArgs = { }>; }; +export const defaults = { + desiredCount: 1, + minCount: 1, + maxCount: 10, + size: 'small', + environment: [], + secrets: [], + healtCheckPath: '/healtcheck', + taskExecutionRoleInlinePolicies: [], + taskRoleInlinePolicies: [], +}; + export class Ecs extends pulumi.ComponentResource { name: string; logGroup: aws.cloudwatch.LogGroup; diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 688766b..bbf5fe0 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -3,15 +3,7 @@ import * as aws from '@pulumi/aws'; import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; -import { Ecs, EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; - -const defaults = { - size: 'small', - environment: [], - secrets: [], - taskExecutionRoleInlinePolicies: [], - taskRoleInlinePolicies: [], -}; +import { Ecs, EcsArgs, assumeRolePolicy, awsRegion, defaults } from './ecs'; export class Mongo extends Ecs { taskDefinition: aws.ecs.TaskDefinition; diff --git a/src/components/web-server.ts b/src/components/web-server.ts index 4f2be42..a1faf39 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -4,7 +4,7 @@ import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; import { AcmCertificate } from './acm-certificate'; -import { Ecs, EcsArgs, assumeRolePolicy, awsRegion } from './ecs'; +import { Ecs, EcsArgs, assumeRolePolicy, awsRegion, defaults } from './ecs'; export type WebServerArgs = EcsArgs & { /** @@ -34,18 +34,6 @@ export type WebServerArgs = EcsArgs & { healtCheckPath?: pulumi.Input; }; -const defaults = { - desiredCount: 1, - minCount: 1, - maxCount: 10, - size: 'small', - environment: [], - secrets: [], - healtCheckPath: '/healtcheck', - taskExecutionRoleInlinePolicies: [], - taskRoleInlinePolicies: [], -}; - export class WebServer extends Ecs { certificate: AcmCertificate; lbSecurityGroup: aws.ec2.SecurityGroup; From d6338ba7a2e78e304825f499f277edca2a684519 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 9 Nov 2023 18:13:01 +0100 Subject: [PATCH 32/58] Renamed property --- src/components/mongo.ts | 8 ++++---- src/index.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index bbf5fe0..f7a1739 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -7,7 +7,7 @@ import { Ecs, EcsArgs, assumeRolePolicy, awsRegion, defaults } from './ecs'; export class Mongo extends Ecs { taskDefinition: aws.ecs.TaskDefinition; - serviceSecurityGroup: aws.ec2.SecurityGroup; + securityGroup: aws.ec2.SecurityGroup; persistentStorage: aws.efs.MountTarget; serviceDiscovery: aws.servicediscovery.Service; service: aws.ecs.Service; @@ -19,7 +19,7 @@ export class Mongo extends Ecs { ) { super('studion:Mongo', name, args, opts); - this.serviceSecurityGroup = this.createSecurityGroup(args); + this.securityGroup = this.createSecurityGroup(args); this.persistentStorage = this.createPersistentStorage(args); this.taskDefinition = this.createTaskDefinition(args); this.serviceDiscovery = this.createServiceDiscovery(args); @@ -83,7 +83,7 @@ export class Mongo extends Ecs { return new aws.efs.MountTarget(`${this.name}-mount-target`, { fileSystemId: efs.id, subnetId: args.vpc.privateSubnetIds[0], - securityGroups: [this.serviceSecurityGroup.id], + securityGroups: [this.securityGroup.id], }); } @@ -291,7 +291,7 @@ export class Mongo extends Ecs { taskDefinition: this.taskDefinition.arn, networkConfiguration: { subnets: [argsWithDefaults.vpc.privateSubnetIds[0]], - securityGroups: [this.serviceSecurityGroup.id], + securityGroups: [this.securityGroup.id], }, serviceRegistries: { registryArn: this.serviceDiscovery.arn, diff --git a/src/index.ts b/src/index.ts index d83bb87..eed27db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from './components/database'; export * from './components/redis'; export * from './components/project'; export * from './components/ec2-ssm-connect'; +export * from './components/ecs'; From e0bf42d96db8231b20f762ceafa1615e47cacfdf Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Mon, 13 Nov 2023 14:42:19 +0100 Subject: [PATCH 33/58] Added common elements to mongo and webserver --- src/components/mongo.ts | 20 +++++++++++++++++++- src/components/web-server.ts | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index f7a1739..7ef3927 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -5,7 +5,9 @@ import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; import { Ecs, EcsArgs, assumeRolePolicy, awsRegion, defaults } from './ecs'; -export class Mongo extends Ecs { +export class Mongo extends pulumi.ComponentResource { + name: string; + logGroup: aws.cloudwatch.LogGroup; taskDefinition: aws.ecs.TaskDefinition; securityGroup: aws.ec2.SecurityGroup; persistentStorage: aws.efs.MountTarget; @@ -19,6 +21,9 @@ export class Mongo extends Ecs { ) { super('studion:Mongo', name, args, opts); + this.name = name; + this.logGroup = this.createLogGroup(); + this.securityGroup = this.createSecurityGroup(args); this.persistentStorage = this.createPersistentStorage(args); this.taskDefinition = this.createTaskDefinition(args); @@ -28,6 +33,19 @@ export class Mongo extends Ecs { this.registerOutputs(); } + private createLogGroup() { + const logGroup = new aws.cloudwatch.LogGroup( + `${this.name}-log-group`, + { + retentionInDays: 14, + namePrefix: `/ecs/${this.name}-`, + tags: commonTags, + }, + { parent: this }, + ); + return logGroup; + } + private createSecurityGroup(args: EcsArgs) { const argsWithDefaults = Object.assign({}, defaults, args); return new aws.ec2.SecurityGroup( diff --git a/src/components/web-server.ts b/src/components/web-server.ts index a1faf39..54b8f41 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -34,7 +34,9 @@ export type WebServerArgs = EcsArgs & { healtCheckPath?: pulumi.Input; }; -export class WebServer extends Ecs { +export class WebServer extends pulumi.ComponentResource { + name: string; + logGroup: aws.cloudwatch.LogGroup; certificate: AcmCertificate; lbSecurityGroup: aws.ec2.SecurityGroup; lb: aws.lb.LoadBalancer; @@ -51,6 +53,9 @@ export class WebServer extends Ecs { ) { super('studion:WebServer', name, args, opts); + this.name = name; + this.logGroup = this.createLogGroup(); + const { domain, hostedZoneId, vpc, port, healtCheckPath } = args; this.certificate = this.createTlsCertificate({ domain, hostedZoneId }); const { @@ -73,6 +78,19 @@ export class WebServer extends Ecs { this.registerOutputs(); } + private createLogGroup() { + const logGroup = new aws.cloudwatch.LogGroup( + `${this.name}-log-group`, + { + retentionInDays: 14, + namePrefix: `/ecs/${this.name}-`, + tags: commonTags, + }, + { parent: this }, + ); + return logGroup; + } + private createTlsCertificate({ domain, hostedZoneId, From bde349181f48161587a9d2a2a655d8458592f11c Mon Sep 17 00:00:00 2001 From: Ivo Kovacevic Date: Mon, 13 Nov 2023 17:01:58 +0100 Subject: [PATCH 34/58] Extract common ecs logic to ecs service --- src/components/ecs-service.ts | 541 ++++++++++++++++++++++++++++++++++ src/components/ecs.ts | 126 -------- src/components/mongo.ts | 331 ++------------------- src/components/project.ts | 4 +- src/components/web-server.ts | 10 +- src/index.ts | 2 +- 6 files changed, 574 insertions(+), 440 deletions(-) create mode 100644 src/components/ecs-service.ts delete mode 100644 src/components/ecs.ts diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts new file mode 100644 index 0000000..345cc12 --- /dev/null +++ b/src/components/ecs-service.ts @@ -0,0 +1,541 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws'; +import * as awsx from '@pulumi/awsx'; +import { CustomSize, Size } from '../types/size'; +import { PredefinedSize, commonTags } from '../constants'; +import { ContainerDefinition } from '@pulumi/aws/ecs'; + +const config = new pulumi.Config('aws'); +export const awsRegion = config.require('region'); + +export const assumeRolePolicy: aws.iam.PolicyDocument = { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Principal: { + Service: 'ecs-tasks.amazonaws.com', + }, + Effect: 'Allow', + Sid: '', + }, + ], +}; + +export type RoleInlinePolicy = { + /** + * Name of the role policy. + */ + name?: pulumi.Input; + /** + * Policy document as a JSON formatted string. + */ + policy?: pulumi.Input; +}; + +export type EcsServiceArgs = { + /** + * The ECR image used to start a container. + */ + image: pulumi.Input; + /** + * Exposed service port. + */ + port: pulumi.Input; + /** + * The aws.ecs.Cluster resource. + */ + cluster: aws.ecs.Cluster; + /** + * The awsx.ec2.Vpc resource. + */ + vpc: awsx.ec2.Vpc; + /** + * Number of instances of the task definition to place and keep running. Defaults to 1. + */ + desiredCount?: pulumi.Input; + /** + * Min capacity of the scalable target. Defaults to 1. + */ + minCount?: pulumi.Input; + /** + * Max capacity of the scalable target. Defaults to 10. + */ + maxCount?: pulumi.Input; + /** + * CPU and memory size used for running the container. Defaults to "small". + * Available predefined options are: + * - small (0.25 vCPU, 0.5 GB memory) + * - medium (0.5 vCPU, 1 GB memory) + * - large (1 vCPU memory, 2 GB memory) + * - xlarge (2 vCPU, 4 GB memory) + */ + size?: pulumi.Input; + /** + * The environment variables to pass to a container. Don't use this field for + * sensitive information such as passwords, API keys, etc. For that purpose, + * please use the `secrets` property. + * Defaults to []. + */ + environment?: aws.ecs.KeyValuePair[]; + /** + * The secrets to pass to the container. Defaults to []. + */ + secrets?: aws.ecs.Secret[]; + enableServiceAutoDiscovery: pulumi.Input; + persistentStorageVolumePath?: pulumi.Input; + dockerCommand?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + lbTargetGroupArn?: aws.lb.TargetGroup['arn']; + securityGroup?: aws.ec2.SecurityGroup; + assignPublicIp?: pulumi.Input; + taskRoleInlinePolicies?: pulumi.Input[]>; + /** + * A map of tags to assign to the resource. + */ + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +}; + +export const defaults = { + desiredCount: 1, + minCount: 1, + maxCount: 1, + size: 'small', + environment: [], + secrets: [], + enableServiceAutoDiscovery: false, + assignPublicIp: false, + taskExecutionRoleInlinePolicies: [], + taskRoleInlinePolicies: [], +}; + +export class EcsService extends pulumi.ComponentResource { + name: string; + logGroup: aws.cloudwatch.LogGroup; + taskDefinition: aws.ecs.TaskDefinition; + serviceDiscoveryService?: aws.servicediscovery.Service; + service: aws.ecs.Service; + + constructor( + name: string, + args: EcsServiceArgs, + opts: pulumi.ComponentResourceOptions = {}, + ) { + super('studion:ecs:Service', name, {}, opts); + const argsWithDefaults = Object.assign({}, defaults, args); + + this.name = name; + this.logGroup = this.createLogGroup(); + this.taskDefinition = this.createTaskDefinition(args); + if (argsWithDefaults.enableServiceAutoDiscovery) { + this.serviceDiscoveryService = this.createServiceDiscovery( + argsWithDefaults.vpc, + ); + } + this.service = this.createEcsService(args, opts); + this.enableAutoscaling(args); + + this.registerOutputs(); + } + + private createLogGroup() { + const logGroup = new aws.cloudwatch.LogGroup( + `${this.name}-log-group`, + { + retentionInDays: 14, + namePrefix: `/ecs/${this.name}-`, + tags: commonTags, + }, + { parent: this }, + ); + return logGroup; + } + + private createPersistentStorage(vpc: awsx.ec2.Vpc) { + const efs = new aws.efs.FileSystem( + `${this.name}-efs`, + { + encrypted: true, + lifecyclePolicies: [ + { + transitionToPrimaryStorageClass: 'AFTER_1_ACCESS', + transitionToIa: 'AFTER_7_DAYS', + }, + ], + performanceMode: 'generalPurpose', + throughputMode: 'bursting', + tags: { + ...commonTags, + Name: `${this.name}-data`, + }, + }, + { parent: this }, + ); + + const securityGroup = new aws.ec2.SecurityGroup( + `${this.name}-persistent-storage-security-group`, + { + vpcId: vpc.vpcId, + ingress: [ + { + fromPort: 2049, + toPort: 2049, + protocol: 'tcp', + cidrBlocks: [vpc.vpc.cidrBlock], + }, + ], + tags: commonTags, + }, + { parent: this }, + ); + + const mountTarget = new aws.efs.MountTarget( + `${this.name}-mount-target`, + { + fileSystemId: efs.id, + subnetId: vpc.privateSubnetIds[0], + securityGroups: [securityGroup.id], + }, + { parent: this }, + ); + + return efs; + } + + private createTaskDefinition(args: EcsServiceArgs) { + const argsWithDefaults = Object.assign({}, defaults, args); + const stack = pulumi.getStack(); + + const secretManagerSecretsInlinePolicy = { + name: `${this.name}-secret-manager-access`, + policy: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Sid: 'AllowContainerToGetSecretManagerSecrets', + Effect: 'Allow', + Action: ['secretsmanager:GetSecretValue'], + Resource: '*', + }, + ], + }), + }; + + const taskExecutionRole = new aws.iam.Role( + `${this.name}-ecs-task-exec-role`, + { + namePrefix: `${this.name}-ecs-task-exec-role-`, + assumeRolePolicy, + managedPolicyArns: [ + 'arn:aws:iam::aws:policy/CloudWatchFullAccess', + 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess', + ], + inlinePolicies: [ + secretManagerSecretsInlinePolicy, + ...argsWithDefaults.taskExecutionRoleInlinePolicies, + ], + tags: commonTags, + }, + { parent: this }, + ); + + const execCmdInlinePolicy = { + name: `${this.name}-ecs-exec`, + policy: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Sid: 'AllowContainerToCreateECSExecSSMChannel', + Effect: 'Allow', + Action: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + ], + Resource: '*', + }, + ], + }), + }; + + const taskRole = new aws.iam.Role( + `${this.name}-ecs-task-role`, + { + namePrefix: `${this.name}-ecs-task-role-`, + assumeRolePolicy, + inlinePolicies: [ + execCmdInlinePolicy, + ...argsWithDefaults.taskRoleInlinePolicies, + ], + tags: commonTags, + }, + { parent: this }, + ); + + const parsedSize = pulumi.all([argsWithDefaults.size]).apply(([size]) => { + const mapCapabilities = ({ cpu, memory }: CustomSize) => ({ + cpu: String(cpu), + memory: String(memory), + }); + if (typeof size === 'string') { + return mapCapabilities(PredefinedSize[size]); + } + if (typeof size === 'object') { + return mapCapabilities(size); + } + throw Error('Incorrect EcsService size argument'); + }); + + const taskDefinition = new aws.ecs.TaskDefinition( + `${this.name}-task-definition`, + { + family: `${this.name}-task-definition-${stack}`, + networkMode: 'awsvpc', + executionRoleArn: taskExecutionRole.arn, + taskRoleArn: taskRole.arn, + cpu: parsedSize.cpu, + memory: parsedSize.memory, + requiresCompatibilities: ['FARGATE'], + containerDefinitions: pulumi + .all([ + this.name, + argsWithDefaults.image, + argsWithDefaults.port, + argsWithDefaults.environment, + argsWithDefaults.secrets, + argsWithDefaults.persistentStorageVolumePath, + argsWithDefaults.dockerCommand, + this.logGroup.name, + awsRegion, + ]) + .apply( + ([ + containerName, + image, + port, + environment, + secrets, + persistentStorageVolumePath, + command, + logGroup, + region, + ]) => { + return JSON.stringify([ + { + readonlyRootFilesystem: false, + name: containerName, + image, + essential: true, + portMappings: [ + { + containerPort: port, + protocol: 'tcp', + }, + ], + ...(persistentStorageVolumePath && { + mountPoints: [ + { + containerPath: persistentStorageVolumePath, + sourceVolume: `${this.name}-volume`, + }, + ], + }), + logConfiguration: { + logDriver: 'awslogs', + options: { + 'awslogs-group': logGroup, + 'awslogs-region': region, + 'awslogs-stream-prefix': 'ecs', + }, + }, + command, + environment, + secrets, + }, + ] as ContainerDefinition[]); + }, + ), + ...(argsWithDefaults.persistentStorageVolumePath && { + volumes: [ + { + name: `${this.name}-volume`, + efsVolumeConfiguration: { + fileSystemId: this.createPersistentStorage(argsWithDefaults.vpc) + .id, + transitEncryption: 'ENABLED', + }, + }, + ], + }), + tags: { ...commonTags, ...argsWithDefaults.tags }, + }, + { parent: this }, + ); + + return taskDefinition; + } + + private createServiceDiscovery(vpc: awsx.ec2.Vpc) { + const privateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace( + `${this.name}-private-dns-namespace`, + { + vpc: vpc.vpcId, + name: this.name, + tags: commonTags, + }, + { parent: this }, + ); + + return new aws.servicediscovery.Service( + `mongo-service`, + { + name: this.name, + dnsConfig: { + namespaceId: privateDnsNamespace.id, + dnsRecords: [ + { + ttl: 10, + type: 'A', + }, + ], + routingPolicy: 'MULTIVALUE', + }, + healthCheckCustomConfig: { + failureThreshold: 1, + }, + tags: commonTags, + }, + { parent: this }, + ); + } + + private createEcsService( + args: EcsServiceArgs, + opts: pulumi.ComponentResourceOptions, + ) { + const argsWithDefaults = Object.assign({}, defaults, args); + + const securityGroup = + argsWithDefaults.securityGroup || + new aws.ec2.SecurityGroup( + `${this.name}-service-security-group`, + { + vpcId: argsWithDefaults.vpc.vpcId, + ingress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + securityGroups: [argsWithDefaults.vpc.vpc.cidrBlock], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + }, + ], + tags: commonTags, + }, + { parent: this }, + ); + + const service = new aws.ecs.Service( + `${this.name}-service`, + { + name: this.name, + cluster: argsWithDefaults.cluster.id, + launchType: 'FARGATE', + desiredCount: argsWithDefaults.desiredCount, + taskDefinition: this.taskDefinition.arn, + enableExecuteCommand: true, + ...(argsWithDefaults.lbTargetGroupArn && { + loadBalancers: [ + { + containerName: this.name, + containerPort: argsWithDefaults.port, + targetGroupArn: argsWithDefaults.lbTargetGroupArn, + }, + ], + }), + networkConfiguration: { + assignPublicIp: argsWithDefaults.assignPublicIp, + subnets: argsWithDefaults.assignPublicIp + ? argsWithDefaults.vpc.publicSubnetIds + : argsWithDefaults.vpc.privateSubnetIds, + securityGroups: [securityGroup.id], + }, + ...(argsWithDefaults.enableServiceAutoDiscovery && + this.serviceDiscoveryService && { + serviceRegistries: { + registryArn: this.serviceDiscoveryService.arn, + }, + }), + tags: { ...commonTags, ...argsWithDefaults.tags }, + }, + { + parent: this, + dependsOn: opts.dependsOn, + }, + ); + return service; + } + + private enableAutoscaling(args: EcsServiceArgs) { + const argsWithDefaults = Object.assign({}, defaults, args); + + const autoscalingTarget = new aws.appautoscaling.Target( + `${this.name}-autoscale-target`, + { + minCapacity: argsWithDefaults.minCount, + maxCapacity: argsWithDefaults.maxCount, + resourceId: pulumi.interpolate`service/${argsWithDefaults.cluster.name}/${this.service.name}`, + serviceNamespace: 'ecs', + scalableDimension: 'ecs:service:DesiredCount', + tags: commonTags, + }, + { parent: this }, + ); + + const memoryAutoscalingPolicy = new aws.appautoscaling.Policy( + `${this.name}-memory-autoscale-policy`, + { + policyType: 'TargetTrackingScaling', + resourceId: autoscalingTarget.resourceId, + scalableDimension: autoscalingTarget.scalableDimension, + serviceNamespace: autoscalingTarget.serviceNamespace, + targetTrackingScalingPolicyConfiguration: { + predefinedMetricSpecification: { + predefinedMetricType: 'ECSServiceAverageMemoryUtilization', + }, + targetValue: 70, + }, + }, + { parent: this }, + ); + + const cpuAutoscalingPolicy = new aws.appautoscaling.Policy( + `${this.name}-cpu-autoscale-policy`, + { + policyType: 'TargetTrackingScaling', + resourceId: autoscalingTarget.resourceId, + scalableDimension: autoscalingTarget.scalableDimension, + serviceNamespace: autoscalingTarget.serviceNamespace, + targetTrackingScalingPolicyConfiguration: { + predefinedMetricSpecification: { + predefinedMetricType: 'ECSServiceAverageCPUUtilization', + }, + targetValue: 70, + }, + }, + { parent: this }, + ); + } +} diff --git a/src/components/ecs.ts b/src/components/ecs.ts deleted file mode 100644 index db31b5e..0000000 --- a/src/components/ecs.ts +++ /dev/null @@ -1,126 +0,0 @@ -import * as pulumi from '@pulumi/pulumi'; -import * as aws from '@pulumi/aws'; -import * as awsx from '@pulumi/awsx'; -import { Size } from '../types/size'; -import { commonTags } from '../constants'; - -const config = new pulumi.Config('aws'); -export const awsRegion = config.require('region'); - -export const assumeRolePolicy: aws.iam.PolicyDocument = { - Version: '2012-10-17', - Statement: [ - { - Action: 'sts:AssumeRole', - Principal: { - Service: 'ecs-tasks.amazonaws.com', - }, - Effect: 'Allow', - Sid: '', - }, - ], -}; - -export type RoleInlinePolicy = { - /** - * Name of the role policy. - */ - name?: pulumi.Input; - /** - * Policy document as a JSON formatted string. - */ - policy?: pulumi.Input; -}; - -export type EcsArgs = { - /** - * The ECR image used to start a container. - */ - image: pulumi.Input; - /** - * Exposed service port. - */ - port: pulumi.Input; - /** - * The aws.ecs.Cluster resource. - */ - cluster: aws.ecs.Cluster; - /** - * The awsx.ec2.Vpc resource. - */ - vpc: awsx.ec2.Vpc; - /** - * CPU and memory size used for running the container. Defaults to "small". - * Available predefined options are: - * - small (0.25 vCPU, 0.5 GB memory) - * - medium (0.5 vCPU, 1 GB memory) - * - large (1 vCPU memory, 2 GB memory) - * - xlarge (2 vCPU, 4 GB memory) - */ - size?: pulumi.Input; - /** - * The environment variables to pass to a container. Don't use this field for - * sensitive information such as passwords, API keys, etc. For that purpose, - * please use the `secrets` property. - * Defaults to []. - */ - environment?: aws.ecs.KeyValuePair[]; - /** - * The secrets to pass to the container. Defaults to []. - */ - secrets?: aws.ecs.Secret[]; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; - /** - * A map of tags to assign to the resource. - */ - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; -}; - -export const defaults = { - desiredCount: 1, - minCount: 1, - maxCount: 10, - size: 'small', - environment: [], - secrets: [], - healtCheckPath: '/healtcheck', - taskExecutionRoleInlinePolicies: [], - taskRoleInlinePolicies: [], -}; - -export class Ecs extends pulumi.ComponentResource { - name: string; - logGroup: aws.cloudwatch.LogGroup; - - constructor( - type: string, - name: string, - args: EcsArgs, - opts: pulumi.ComponentResourceOptions = {}, - ) { - super(type, name, {}, opts); - - this.name = name; - this.logGroup = this.createLogGroup(); - - this.registerOutputs(); - } - - private createLogGroup() { - const logGroup = new aws.cloudwatch.LogGroup( - `${this.name}-log-group`, - { - retentionInDays: 14, - namePrefix: `/ecs/${this.name}-`, - tags: commonTags, - }, - { parent: this }, - ); - return logGroup; - } -} diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 7ef3927..711e1df 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,326 +1,39 @@ import * as pulumi from '@pulumi/pulumi'; -import * as aws from '@pulumi/aws'; -import { CustomSize } from '../types/size'; -import { PredefinedSize, commonTags } from '../constants'; -import { ContainerDefinition } from '@pulumi/aws/ecs'; -import { Ecs, EcsArgs, assumeRolePolicy, awsRegion, defaults } from './ecs'; +import { EcsService } from './ecs-service'; export class Mongo extends pulumi.ComponentResource { name: string; - logGroup: aws.cloudwatch.LogGroup; - taskDefinition: aws.ecs.TaskDefinition; - securityGroup: aws.ec2.SecurityGroup; - persistentStorage: aws.efs.MountTarget; - serviceDiscovery: aws.servicediscovery.Service; - service: aws.ecs.Service; + service: EcsService; constructor( name: string, - args: EcsArgs, + args: MongoArgs, opts: pulumi.ComponentResourceOptions = {}, ) { super('studion:Mongo', name, args, opts); this.name = name; - this.logGroup = this.createLogGroup(); - - this.securityGroup = this.createSecurityGroup(args); - this.persistentStorage = this.createPersistentStorage(args); - this.taskDefinition = this.createTaskDefinition(args); - this.serviceDiscovery = this.createServiceDiscovery(args); - this.service = this.createEcsService(args); - - this.registerOutputs(); - } - - private createLogGroup() { - const logGroup = new aws.cloudwatch.LogGroup( - `${this.name}-log-group`, - { - retentionInDays: 14, - namePrefix: `/ecs/${this.name}-`, - tags: commonTags, - }, - { parent: this }, - ); - return logGroup; - } - - private createSecurityGroup(args: EcsArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - return new aws.ec2.SecurityGroup( - `${this.name}-security-group`, - { - vpcId: argsWithDefaults.vpc.vpcId, - ingress: [ - { - fromPort: argsWithDefaults.port, - toPort: argsWithDefaults.port, - protocol: 'tcp', - cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], - }, - { - fromPort: 2049, - toPort: 2049, - protocol: 'tcp', - cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - cidrBlocks: ['0.0.0.0/0'], - }, - ], - tags: commonTags, - }, - { parent: this }, - ); - } - - private createPersistentStorage(args: EcsArgs) { - const key = new aws.kms.Key(`${this.name}-kms-key`, { - deletionWindowInDays: 10, - }); - - const efs = new aws.efs.FileSystem(`${this.name}-efs`, { - encrypted: true, - kmsKeyId: key.arn, - lifecyclePolicies: [ - { - transitionToPrimaryStorageClass: 'AFTER_1_ACCESS', - }, - ], - tags: { - Name: `${this.name}-data`, - }, - }); - - return new aws.efs.MountTarget(`${this.name}-mount-target`, { - fileSystemId: efs.id, - subnetId: args.vpc.privateSubnetIds[0], - securityGroups: [this.securityGroup.id], - }); - } - - private createTaskDefinition(args: EcsArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - const stack = pulumi.getStack(); - - const secretManagerSecretsInlinePolicy = { - name: `${this.name}-secret-manager-access`, - policy: JSON.stringify({ - Version: '2012-10-17', - Statement: [ - { - Sid: 'AllowContainerToGetSecretManagerSecrets', - Effect: 'Allow', - Action: ['secretsmanager:GetSecretValue'], - Resource: '*', - }, - ], - }), - }; - - const taskExecutionRole = new aws.iam.Role( - `${this.name}-ecs-task-exec-role`, - { - namePrefix: `${this.name}-ecs-task-exec-role-`, - assumeRolePolicy, - managedPolicyArns: [ - 'arn:aws:iam::aws:policy/CloudWatchFullAccess', - 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess', - ], - inlinePolicies: [ - secretManagerSecretsInlinePolicy, - ...argsWithDefaults.taskExecutionRoleInlinePolicies, - ], - tags: commonTags, - }, - { parent: this }, - ); - - const execCmdInlinePolicy = { - name: `${this.name}-ecs-exec`, - policy: JSON.stringify({ - Version: '2012-10-17', - Statement: [ - { - Sid: 'AllowContainerToCreateECSExecSSMChannel', - Effect: 'Allow', - Action: [ - 'ssmmessages:CreateControlChannel', - 'ssmmessages:CreateDataChannel', - 'ssmmessages:OpenControlChannel', - 'ssmmessages:OpenDataChannel', - ], - Resource: '*', - }, - ], - }), - }; - - const taskRole = new aws.iam.Role( - `${this.name}-ecs-task-role`, - { - namePrefix: `${this.name}-ecs-task-role-`, - assumeRolePolicy, - inlinePolicies: [ - execCmdInlinePolicy, - ...argsWithDefaults.taskRoleInlinePolicies, - ], - tags: commonTags, - }, - { parent: this }, - ); - - const parsedSize = pulumi.all([argsWithDefaults.size]).apply(([size]) => { - const mapCapabilities = ({ cpu, memory }: CustomSize) => ({ - cpu: String(cpu), - memory: String(memory), - }); - if (typeof size === 'string') { - return mapCapabilities(PredefinedSize[size]); - } - if (typeof size === 'object') { - return mapCapabilities(size); - } - throw Error('Incorrect EcsService size argument'); - }); - - const taskDefinition = new aws.ecs.TaskDefinition( - `${this.name}-task-definition`, - { - family: `${this.name}-task-definition-${stack}`, - networkMode: 'awsvpc', - executionRoleArn: taskExecutionRole.arn, - taskRoleArn: taskRole.arn, - cpu: parsedSize.cpu, - memory: parsedSize.memory, - requiresCompatibilities: ['FARGATE'], - containerDefinitions: pulumi - .all([ - this.name, - argsWithDefaults.image, - argsWithDefaults.port, - argsWithDefaults.environment, - argsWithDefaults.secrets, - this.logGroup.name, - awsRegion, - ]) - .apply( - ([ - containerName, - image, - port, - environment, - secrets, - logGroup, - region, - ]) => { - return JSON.stringify([ - { - readonlyRootFilesystem: false, - name: containerName, - image, - essential: true, - portMappings: [ - { - containerPort: port, - protocol: 'tcp', - }, - ], - mountPoints: [ - { - containerPath: '/data/db', - sourceVolume: `${this.name}-volume`, - }, - ], - logConfiguration: { - logDriver: 'awslogs', - options: { - 'awslogs-group': logGroup, - 'awslogs-region': region, - 'awslogs-stream-prefix': 'ecs', - }, - }, - command: ['mongod', '--port', port.toString()], - environment, - secrets, - }, - ] as ContainerDefinition[]); - }, - ), - volumes: [ - { - name: `${this.name}-volume`, - efsVolumeConfiguration: { - fileSystemId: this.persistentStorage.fileSystemId, - transitEncryption: 'ENABLED', - }, - }, - ], - tags: { ...commonTags, ...argsWithDefaults.tags }, - }, - { parent: this }, - ); - - return taskDefinition; - } - - private createServiceDiscovery(args: EcsArgs) { - const privateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace( - `${this.name}-private-dns-namespace`, - { - vpc: args.vpc.vpcId, - name: this.name, - }, - ); - - return new aws.servicediscovery.Service(`mongo-service`, { - dnsConfig: { - namespaceId: privateDnsNamespace.id, - dnsRecords: [ - { - ttl: 10, - type: 'A', - }, - ], - routingPolicy: 'MULTIVALUE', - }, - healthCheckCustomConfig: { - failureThreshold: 1, - }, - }); - } - - private createEcsService(args: EcsArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - - const service = new aws.ecs.Service( - `${this.name}-service`, + this.service = new EcsService( + name, { - name: this.name, - cluster: argsWithDefaults.cluster.id, - launchType: 'FARGATE', + image: 'mongo', + dockerCommand: ['mongod', '--port', port.toString()], desiredCount: 1, - taskDefinition: this.taskDefinition.arn, - networkConfiguration: { - subnets: [argsWithDefaults.vpc.privateSubnetIds[0]], - securityGroups: [this.securityGroup.id], - }, - serviceRegistries: { - registryArn: this.serviceDiscovery.arn, - }, - tags: { ...commonTags, ...argsWithDefaults.tags }, - }, - { - parent: this, - dependsOn: [], - }, + minCount: 1, + maxCount: 1, + enableServiceAutoDiscovery: true, + persistentStorageVolumePath: '/data/db', + assignPublicIp: false + port, + cluster, + vpc, + size, + environment, + secrets, + }, + { ...opts, parent: this }, ); - return service; + + this.registerOutputs(); } } diff --git a/src/components/project.ts b/src/components/project.ts index 97f528c..4bd21dd 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -9,7 +9,7 @@ import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; -import { EcsArgs } from './ecs'; +import { EcsServiceArgs } from './ecs-service'; export type Service = Database | Redis | StaticSite | WebServer | Mongo; export type Services = Record; @@ -49,7 +49,7 @@ export type MongoService = { | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); } & ServiceArgs & - Omit; + Omit; export type ProjectArgs = { services: ( diff --git a/src/components/web-server.ts b/src/components/web-server.ts index 54b8f41..6c55e6c 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -4,9 +4,15 @@ import { CustomSize } from '../types/size'; import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; import { AcmCertificate } from './acm-certificate'; -import { Ecs, EcsArgs, assumeRolePolicy, awsRegion, defaults } from './ecs'; +import { + EcsService, + EcsServiceArgs, + assumeRolePolicy, + awsRegion, + defaults, +} from './ecs-service'; -export type WebServerArgs = EcsArgs & { +export type WebServerArgs = EcsServiceArgs & { /** * The domain which will be used to access the service. * The domain or subdomain must belong to the provided hostedZone. diff --git a/src/index.ts b/src/index.ts index eed27db..59e1a48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,4 @@ export * from './components/database'; export * from './components/redis'; export * from './components/project'; export * from './components/ec2-ssm-connect'; -export * from './components/ecs'; +export * from './components/ecs-service'; From b2a76c98647fc5560ef850cd9fabf1d9706e3747 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 16:02:56 +0100 Subject: [PATCH 35/58] Fixes --- src/components/ecs-service.ts | 25 ++++++++--- src/components/mongo.ts | 83 +++++++++++++++++++++++++++++++---- src/components/project.ts | 5 +-- src/components/web-server.ts | 1 - 4 files changed, 95 insertions(+), 19 deletions(-) diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index 345cc12..a31365e 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -85,6 +85,7 @@ export type EcsServiceArgs = { enableServiceAutoDiscovery: pulumi.Input; persistentStorageVolumePath?: pulumi.Input; dockerCommand?: pulumi.Input; + enableAutoScaling?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< pulumi.Input[] >; @@ -111,6 +112,8 @@ export const defaults = { assignPublicIp: false, taskExecutionRoleInlinePolicies: [], taskRoleInlinePolicies: [], + healtCheckPath: '/healtcheck', + enableAutoScaling: false, }; export class EcsService extends pulumi.ComponentResource { @@ -137,7 +140,9 @@ export class EcsService extends pulumi.ComponentResource { ); } this.service = this.createEcsService(args, opts); - this.enableAutoscaling(args); + if (argsWithDefaults.enableAutoScaling) { + this.enableAutoscaling(args); + } this.registerOutputs(); } @@ -155,7 +160,7 @@ export class EcsService extends pulumi.ComponentResource { return logGroup; } - private createPersistentStorage(vpc: awsx.ec2.Vpc) { + private createPersistentStorage(vpc: awsx.ec2.Vpc, assignPublicIp: boolean) { const efs = new aws.efs.FileSystem( `${this.name}-efs`, { @@ -163,6 +168,8 @@ export class EcsService extends pulumi.ComponentResource { lifecyclePolicies: [ { transitionToPrimaryStorageClass: 'AFTER_1_ACCESS', + }, + { transitionToIa: 'AFTER_7_DAYS', }, ], @@ -197,7 +204,9 @@ export class EcsService extends pulumi.ComponentResource { `${this.name}-mount-target`, { fileSystemId: efs.id, - subnetId: vpc.privateSubnetIds[0], + subnetId: assignPublicIp + ? vpc.publicSubnetIds[0] + : vpc.privateSubnetIds[0], securityGroups: [securityGroup.id], }, { parent: this }, @@ -365,8 +374,10 @@ export class EcsService extends pulumi.ComponentResource { { name: `${this.name}-volume`, efsVolumeConfiguration: { - fileSystemId: this.createPersistentStorage(argsWithDefaults.vpc) - .id, + fileSystemId: this.createPersistentStorage( + argsWithDefaults.vpc, + argsWithDefaults.assignPublicIp, + ).id, transitEncryption: 'ENABLED', }, }, @@ -468,8 +479,8 @@ export class EcsService extends pulumi.ComponentResource { networkConfiguration: { assignPublicIp: argsWithDefaults.assignPublicIp, subnets: argsWithDefaults.assignPublicIp - ? argsWithDefaults.vpc.publicSubnetIds - : argsWithDefaults.vpc.privateSubnetIds, + ? [argsWithDefaults.vpc.publicSubnetIds[0]] + : [argsWithDefaults.vpc.privateSubnetIds[0]], securityGroups: [securityGroup.id], }, ...(argsWithDefaults.enableServiceAutoDiscovery && diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 711e1df..cafa5f2 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,5 +1,44 @@ import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws'; +import * as awsx from '@pulumi/awsx'; import { EcsService } from './ecs-service'; +import { Size } from '../types/size'; +import { commonTags } from '../constants'; + +export type MongoArgs = { + /** + * Exposed service port. + */ + port: pulumi.Input; + /** + * CPU and memory size used for running the container. Defaults to "small". + * Available predefined options are: + * - small (0.25 vCPU, 0.5 GB memory) + * - medium (0.5 vCPU, 1 GB memory) + * - large (1 vCPU memory, 2 GB memory) + * - xlarge (2 vCPU, 4 GB memory) + */ + size?: pulumi.Input; + /** + * The aws.ecs.Cluster resource. + */ + cluster: aws.ecs.Cluster; + /** + * The awsx.ec2.Vpc resource. + */ + vpc: awsx.ec2.Vpc; + /** + * The environment variables to pass to a container. Don't use this field for + * sensitive information such as passwords, API keys, etc. For that purpose, + * please use the `secrets` property. + * Defaults to []. + */ + environment?: aws.ecs.KeyValuePair[]; + /** + * The secrets to pass to the container. Defaults to []. + */ + secrets?: aws.ecs.Secret[]; +}; export class Mongo extends pulumi.ComponentResource { name: string; @@ -12,24 +51,52 @@ export class Mongo extends pulumi.ComponentResource { ) { super('studion:Mongo', name, args, opts); + const { port, size, cluster, vpc, environment, secrets } = args; + + const securityGroup = new aws.ec2.SecurityGroup( + `${name}-service-security-group`, + { + vpcId: vpc.vpcId, + ingress: [ + { + fromPort: port, + toPort: port, + protocol: 'tcp', + cidrBlocks: [vpc.vpc.cidrBlock], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + }, + ], + tags: commonTags, + }, + { parent: this }, + ); + this.name = name; this.service = new EcsService( name, { - image: 'mongo', - dockerCommand: ['mongod', '--port', port.toString()], + image: 'mongo:latest', + port, + cluster, desiredCount: 1, minCount: 1, maxCount: 1, + ...(size && { size }), + environment, + secrets, enableServiceAutoDiscovery: true, persistentStorageVolumePath: '/data/db', - assignPublicIp: false - port, - cluster, + dockerCommand: ['mongod', '--port', port.toString()], + assignPublicIp: false, vpc, - size, - environment, - secrets, + securityGroup, }, { ...opts, parent: this }, ); diff --git a/src/components/project.ts b/src/components/project.ts index 4bd21dd..7d45b8b 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -4,12 +4,11 @@ import * as awsx from '@pulumi/awsx'; import * as upstash from '@upstash/pulumi'; import { Database, DatabaseArgs } from './database'; import { WebServer, WebServerArgs } from './web-server'; -import { Mongo } from './mongo'; +import { Mongo, MongoArgs } from './mongo'; import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; -import { EcsServiceArgs } from './ecs-service'; export type Service = Database | Redis | StaticSite | WebServer | Mongo; export type Services = Record; @@ -49,7 +48,7 @@ export type MongoService = { | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); } & ServiceArgs & - Omit; + Omit; export type ProjectArgs = { services: ( diff --git a/src/components/web-server.ts b/src/components/web-server.ts index 6c55e6c..f4bb5ad 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -5,7 +5,6 @@ import { PredefinedSize, commonTags } from '../constants'; import { ContainerDefinition } from '@pulumi/aws/ecs'; import { AcmCertificate } from './acm-certificate'; import { - EcsService, EcsServiceArgs, assumeRolePolicy, awsRegion, From ed33d4227140771df03abf880abb3e5285ccbd4c Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 16:12:49 +0100 Subject: [PATCH 36/58] Fixed mount targets --- src/components/ecs-service.ts | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index a31365e..2b5ae9a 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -200,17 +200,23 @@ export class EcsService extends pulumi.ComponentResource { { parent: this }, ); - const mountTarget = new aws.efs.MountTarget( - `${this.name}-mount-target`, - { - fileSystemId: efs.id, - subnetId: assignPublicIp - ? vpc.publicSubnetIds[0] - : vpc.privateSubnetIds[0], - securityGroups: [securityGroup.id], - }, - { parent: this }, - ); + const subnetIds = assignPublicIp + ? vpc.publicSubnetIds + : vpc.privateSubnetIds; + + subnetIds.apply(privateSubnets => { + privateSubnets.forEach(it => { + const mountTarget = new aws.efs.MountTarget( + `${this.name}-mount-target-${it}`, + { + fileSystemId: efs.id, + subnetId: it, + securityGroups: [securityGroup.id], + }, + { parent: this }, + ); + }); + }); return efs; } @@ -479,8 +485,8 @@ export class EcsService extends pulumi.ComponentResource { networkConfiguration: { assignPublicIp: argsWithDefaults.assignPublicIp, subnets: argsWithDefaults.assignPublicIp - ? [argsWithDefaults.vpc.publicSubnetIds[0]] - : [argsWithDefaults.vpc.privateSubnetIds[0]], + ? argsWithDefaults.vpc.publicSubnetIds + : argsWithDefaults.vpc.privateSubnetIds, securityGroups: [securityGroup.id], }, ...(argsWithDefaults.enableServiceAutoDiscovery && From a0056e95007b295cc9495dda42f85c90c9c2448c Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 16:45:28 +0100 Subject: [PATCH 37/58] Extracted web server common logic --- src/components/web-server.ts | 400 +++++++++-------------------------- 1 file changed, 105 insertions(+), 295 deletions(-) diff --git a/src/components/web-server.ts b/src/components/web-server.ts index f4bb5ad..b67a8bf 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -1,26 +1,34 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; -import { CustomSize } from '../types/size'; -import { PredefinedSize, commonTags } from '../constants'; -import { ContainerDefinition } from '@pulumi/aws/ecs'; +import * as awsx from '@pulumi/awsx'; +import { commonTags } from '../constants'; import { AcmCertificate } from './acm-certificate'; -import { - EcsServiceArgs, - assumeRolePolicy, - awsRegion, - defaults, -} from './ecs-service'; +import { EcsService, defaults } from './ecs-service'; +import { Size } from '../types/size'; -export type WebServerArgs = EcsServiceArgs & { +export type WebServerArgs = { + /** + * The ECR image used to start a container. + */ + image: pulumi.Input; + /* + * Exposed service port. + */ + port: pulumi.Input; /** * The domain which will be used to access the service. * The domain or subdomain must belong to the provided hostedZone. */ domain: pulumi.Input; /** - * The ID of the hosted zone. + * CPU and memory size used for running the container. Defaults to "small". + * Available predefined options are: + * - small (0.25 vCPU, 0.5 GB memory) + * - medium (0.5 vCPU, 1 GB memory) + * - large (1 vCPU memory, 2 GB memory) + * - xlarge (2 vCPU, 4 GB memory) */ - hostedZoneId: pulumi.Input; + size?: pulumi.Input; /** * Number of instances of the task definition to place and keep running. Defaults to 1. */ @@ -33,6 +41,29 @@ export type WebServerArgs = EcsServiceArgs & { * Max capacity of the scalable target. Defaults to 10. */ maxCount?: pulumi.Input; + /** + * The aws.ecs.Cluster resource. + */ + cluster: aws.ecs.Cluster; + /** + * The awsx.ec2.Vpc resource. + */ + vpc: awsx.ec2.Vpc; + /** + * The environment variables to pass to a container. Don't use this field for + * sensitive information such as passwords, API keys, etc. For that purpose, + * please use the `secrets` property. + * Defaults to []. + */ + environment?: aws.ecs.KeyValuePair[]; + /** + * The secrets to pass to the container. Defaults to []. + */ + secrets?: aws.ecs.Secret[]; + /** + * The ID of the hosted zone. + */ + hostedZoneId: pulumi.Input; /** * Path for the health check request. Defaults to "/healtcheck". */ @@ -41,15 +72,13 @@ export type WebServerArgs = EcsServiceArgs & { export class WebServer extends pulumi.ComponentResource { name: string; - logGroup: aws.cloudwatch.LogGroup; + service: EcsService; certificate: AcmCertificate; lbSecurityGroup: aws.ec2.SecurityGroup; lb: aws.lb.LoadBalancer; lbTargetGroup: aws.lb.TargetGroup; lbHttpListener: aws.lb.Listener; lbTlsListener: aws.lb.Listener; - taskDefinition: aws.ecs.TaskDefinition; - service: aws.ecs.Service; constructor( name: string, @@ -58,10 +87,23 @@ export class WebServer extends pulumi.ComponentResource { ) { super('studion:WebServer', name, args, opts); - this.name = name; - this.logGroup = this.createLogGroup(); + const { + image, + port, + size, + cluster, + vpc, + environment, + secrets, + healtCheckPath, + domain, + hostedZoneId, + desiredCount, + minCount, + maxCount, + } = args; - const { domain, hostedZoneId, vpc, port, healtCheckPath } = args; + this.name = name; this.certificate = this.createTlsCertificate({ domain, hostedZoneId }); const { lb, @@ -75,25 +117,57 @@ export class WebServer extends pulumi.ComponentResource { this.lbHttpListener = lbHttpListener; this.lbTlsListener = lbTlsListener; this.lbSecurityGroup = lbSecurityGroup; - this.taskDefinition = this.createTaskDefinition(args); - this.service = this.createEcsService(args); - this.createDnsRecord({ domain, hostedZoneId }); - this.enableAutoscaling(args); - this.registerOutputs(); - } - - private createLogGroup() { - const logGroup = new aws.cloudwatch.LogGroup( - `${this.name}-log-group`, + const securityGroup = new aws.ec2.SecurityGroup( + `${name}-security-group`, { - retentionInDays: 14, - namePrefix: `/ecs/${this.name}-`, + vpcId: vpc.vpcId, + ingress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + securityGroups: [this.lbSecurityGroup.id], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + }, + ], tags: commonTags, }, { parent: this }, ); - return logGroup; + + this.service = new EcsService( + name, + { + image, + port, + cluster, + ...(desiredCount && { desiredCount }), + ...(minCount && { minCount }), + ...(maxCount && { maxCount }), + ...(size && { size }), + environment, + secrets, + enableServiceAutoDiscovery: false, + enableAutoScaling: true, + lbTargetGroupArn: lbTargetGroup.arn, + assignPublicIp: true, + vpc, + securityGroup, + }, + { ...opts, parent: this }, + ); + + this.createDnsRecord({ domain, hostedZoneId }); + + this.registerOutputs(); } private createTlsCertificate({ @@ -229,219 +303,6 @@ export class WebServer extends pulumi.ComponentResource { }; } - private createTaskDefinition(args: WebServerArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - const stack = pulumi.getStack(); - - const secretManagerSecretsInlinePolicy = { - name: `${this.name}-secret-manager-access`, - policy: JSON.stringify({ - Version: '2012-10-17', - Statement: [ - { - Sid: 'AllowContainerToGetSecretManagerSecrets', - Effect: 'Allow', - Action: ['secretsmanager:GetSecretValue'], - Resource: '*', - }, - ], - }), - }; - - const taskExecutionRole = new aws.iam.Role( - `${this.name}-ecs-task-exec-role`, - { - namePrefix: `${this.name}-ecs-task-exec-role-`, - assumeRolePolicy, - managedPolicyArns: [ - 'arn:aws:iam::aws:policy/CloudWatchFullAccess', - 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess', - ], - inlinePolicies: [ - secretManagerSecretsInlinePolicy, - ...argsWithDefaults.taskExecutionRoleInlinePolicies, - ], - tags: commonTags, - }, - { parent: this }, - ); - - const execCmdInlinePolicy = { - name: `${this.name}-ecs-exec`, - policy: JSON.stringify({ - Version: '2012-10-17', - Statement: [ - { - Sid: 'AllowContainerToCreateECSExecSSMChannel', - Effect: 'Allow', - Action: [ - 'ssmmessages:CreateControlChannel', - 'ssmmessages:CreateDataChannel', - 'ssmmessages:OpenControlChannel', - 'ssmmessages:OpenDataChannel', - ], - Resource: '*', - }, - ], - }), - }; - - const taskRole = new aws.iam.Role( - `${this.name}-ecs-task-role`, - { - namePrefix: `${this.name}-ecs-task-role-`, - assumeRolePolicy, - inlinePolicies: [ - execCmdInlinePolicy, - ...argsWithDefaults.taskRoleInlinePolicies, - ], - tags: commonTags, - }, - { parent: this }, - ); - - const parsedSize = pulumi.all([argsWithDefaults.size]).apply(([size]) => { - const mapCapabilities = ({ cpu, memory }: CustomSize) => ({ - cpu: String(cpu), - memory: String(memory), - }); - if (typeof size === 'string') { - return mapCapabilities(PredefinedSize[size]); - } - if (typeof size === 'object') { - return mapCapabilities(size); - } - throw Error('Incorrect EcsService size argument'); - }); - - const taskDefinition = new aws.ecs.TaskDefinition( - `${this.name}-task-definition`, - { - family: `${this.name}-task-definition-${stack}`, - networkMode: 'awsvpc', - executionRoleArn: taskExecutionRole.arn, - taskRoleArn: taskRole.arn, - cpu: parsedSize.cpu, - memory: parsedSize.memory, - requiresCompatibilities: ['FARGATE'], - containerDefinitions: pulumi - .all([ - this.name, - argsWithDefaults.image, - argsWithDefaults.port, - argsWithDefaults.environment, - argsWithDefaults.secrets, - this.logGroup.name, - awsRegion, - ]) - .apply( - ([ - containerName, - image, - port, - environment, - secrets, - logGroup, - region, - ]) => { - return JSON.stringify([ - { - readonlyRootFilesystem: false, - name: containerName, - image, - essential: true, - portMappings: [ - { - containerPort: port, - protocol: 'tcp', - }, - ], - logConfiguration: { - logDriver: 'awslogs', - options: { - 'awslogs-group': logGroup, - 'awslogs-region': region, - 'awslogs-stream-prefix': 'ecs', - }, - }, - environment, - secrets, - }, - ] as ContainerDefinition[]); - }, - ), - tags: { ...commonTags, ...argsWithDefaults.tags }, - }, - { parent: this }, - ); - - return taskDefinition; - } - - private createEcsService(args: WebServerArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - - const serviceSecurityGroup = new aws.ec2.SecurityGroup( - `${this.name}-security-group`, - { - vpcId: argsWithDefaults.vpc.vpcId, - ingress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - securityGroups: [this.lbSecurityGroup.id], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - cidrBlocks: ['0.0.0.0/0'], - }, - ], - tags: commonTags, - }, - { parent: this }, - ); - - const service = new aws.ecs.Service( - `${this.name}-service`, - { - name: this.name, - cluster: argsWithDefaults.cluster.id, - launchType: 'FARGATE', - desiredCount: argsWithDefaults.desiredCount, - taskDefinition: this.taskDefinition.arn, - enableExecuteCommand: true, - loadBalancers: [ - { - containerName: this.name, - containerPort: argsWithDefaults.port, - targetGroupArn: this.lbTargetGroup.arn, - }, - ], - networkConfiguration: { - assignPublicIp: true, - subnets: argsWithDefaults.vpc.publicSubnetIds, - securityGroups: [serviceSecurityGroup.id], - }, - tags: { ...commonTags, ...argsWithDefaults.tags }, - }, - { - parent: this, - dependsOn: [ - this.lb, - this.lbTargetGroup, - this.lbHttpListener, - this.lbTlsListener, - ], - }, - ); - return service; - } - private createDnsRecord({ domain, hostedZoneId, @@ -463,55 +324,4 @@ export class WebServer extends pulumi.ComponentResource { { parent: this }, ); } - - private enableAutoscaling(args: WebServerArgs) { - const argsWithDefaults = Object.assign({}, defaults, args); - - const autoscalingTarget = new aws.appautoscaling.Target( - `${this.name}-autoscale-target`, - { - minCapacity: argsWithDefaults.minCount, - maxCapacity: argsWithDefaults.maxCount, - resourceId: pulumi.interpolate`service/${argsWithDefaults.cluster.name}/${this.service.name}`, - serviceNamespace: 'ecs', - scalableDimension: 'ecs:service:DesiredCount', - tags: commonTags, - }, - { parent: this }, - ); - - const memoryAutoscalingPolicy = new aws.appautoscaling.Policy( - `${this.name}-memory-autoscale-policy`, - { - policyType: 'TargetTrackingScaling', - resourceId: autoscalingTarget.resourceId, - scalableDimension: autoscalingTarget.scalableDimension, - serviceNamespace: autoscalingTarget.serviceNamespace, - targetTrackingScalingPolicyConfiguration: { - predefinedMetricSpecification: { - predefinedMetricType: 'ECSServiceAverageMemoryUtilization', - }, - targetValue: 80, - }, - }, - { parent: this }, - ); - - const cpuAutoscalingPolicy = new aws.appautoscaling.Policy( - `${this.name}-cpu-autoscale-policy`, - { - policyType: 'TargetTrackingScaling', - resourceId: autoscalingTarget.resourceId, - scalableDimension: autoscalingTarget.scalableDimension, - serviceNamespace: autoscalingTarget.serviceNamespace, - targetTrackingScalingPolicyConfiguration: { - predefinedMetricSpecification: { - predefinedMetricType: 'ECSServiceAverageCPUUtilization', - }, - targetValue: 60, - }, - }, - { parent: this }, - ); - } } From 0c134fbe156c2cf53e0c334ada7eced6e03f8f53 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 17:02:10 +0100 Subject: [PATCH 38/58] Updated property descriptions --- src/components/ecs-service.ts | 27 ++++++++++++++++++++++++--- src/components/web-server.ts | 8 ++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index 2b5ae9a..1fe66c5 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -82,16 +82,37 @@ export type EcsServiceArgs = { * The secrets to pass to the container. Defaults to []. */ secrets?: aws.ecs.Secret[]; + /** + * Enable service auto discovery and assign DNS record to service. + * Defaults to false. + */ enableServiceAutoDiscovery: pulumi.Input; + /** + * Location of persistent storage volume. + */ persistentStorageVolumePath?: pulumi.Input; + /** + * Alternate docker CMD instruction. + */ dockerCommand?: pulumi.Input; + /** + * Enable auto scaling for service. + * Defaults to false + */ enableAutoScaling?: pulumi.Input; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; lbTargetGroupArn?: aws.lb.TargetGroup['arn']; + /** + * Custom service security group + * In case no security group is provided, default security group will be used. + */ securityGroup?: aws.ec2.SecurityGroup; + /** + * Assign public IP address to service. + */ assignPublicIp?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; taskRoleInlinePolicies?: pulumi.Input[]>; /** * A map of tags to assign to the resource. diff --git a/src/components/web-server.ts b/src/components/web-server.ts index b67a8bf..de5e701 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -41,6 +41,10 @@ export type WebServerArgs = { * Max capacity of the scalable target. Defaults to 10. */ maxCount?: pulumi.Input; + /** + * Path for the health check request. Defaults to "/healtcheck". + */ + healtCheckPath?: pulumi.Input; /** * The aws.ecs.Cluster resource. */ @@ -64,10 +68,6 @@ export type WebServerArgs = { * The ID of the hosted zone. */ hostedZoneId: pulumi.Input; - /** - * Path for the health check request. Defaults to "/healtcheck". - */ - healtCheckPath?: pulumi.Input; }; export class WebServer extends pulumi.ComponentResource { From 97f8e2e256af83b6d50ad69e94886d5e537b53da Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 17:19:52 +0100 Subject: [PATCH 39/58] Updated service health check --- README.md | 4 ++-- src/components/ecs-service.ts | 11 +++++++---- src/components/web-server.ts | 14 +++++++------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f77add2..443fd56 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ export type WebServerService = { minCount?: pulumi.Input; maxCount?: pulumi.Input; size?: pulumi.Input; - healtCheckPath?: pulumi.Input; + healthCheckPath?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< pulumi.Input[] >; @@ -421,7 +421,7 @@ export type WebServerArgs = { size?: pulumi.Input; environment?: aws.ecs.KeyValuePair[]; secrets?: aws.ecs.Secret[]; - healtCheckPath?: pulumi.Input; + healthCheckPath?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< pulumi.Input[] >; diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index 1fe66c5..33890a0 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -133,7 +133,7 @@ export const defaults = { assignPublicIp: false, taskExecutionRoleInlinePolicies: [], taskRoleInlinePolicies: [], - healtCheckPath: '/healtcheck', + healthCheckPath: '/healthcheck', enableAutoScaling: false, }; @@ -158,6 +158,7 @@ export class EcsService extends pulumi.ComponentResource { if (argsWithDefaults.enableServiceAutoDiscovery) { this.serviceDiscoveryService = this.createServiceDiscovery( argsWithDefaults.vpc, + argsWithDefaults.healthCheckPath, ); } this.service = this.createEcsService(args, opts); @@ -418,7 +419,7 @@ export class EcsService extends pulumi.ComponentResource { return taskDefinition; } - private createServiceDiscovery(vpc: awsx.ec2.Vpc) { + private createServiceDiscovery(vpc: awsx.ec2.Vpc, healthCheckPath: string) { const privateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace( `${this.name}-private-dns-namespace`, { @@ -443,8 +444,10 @@ export class EcsService extends pulumi.ComponentResource { ], routingPolicy: 'MULTIVALUE', }, - healthCheckCustomConfig: { - failureThreshold: 1, + healthCheckConfig: { + type: 'HTTP', + resourcePath: healthCheckPath || defaults.healthCheckPath, + failureThreshold: 2, }, tags: commonTags, }, diff --git a/src/components/web-server.ts b/src/components/web-server.ts index de5e701..dd7420c 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -42,9 +42,9 @@ export type WebServerArgs = { */ maxCount?: pulumi.Input; /** - * Path for the health check request. Defaults to "/healtcheck". + * Path for the healthh check request. Defaults to "/healthcheck". */ - healtCheckPath?: pulumi.Input; + healthCheckPath?: pulumi.Input; /** * The aws.ecs.Cluster resource. */ @@ -95,7 +95,7 @@ export class WebServer extends pulumi.ComponentResource { vpc, environment, secrets, - healtCheckPath, + healthCheckPath, domain, hostedZoneId, desiredCount, @@ -111,7 +111,7 @@ export class WebServer extends pulumi.ComponentResource { lbHttpListener, lbTlsListener, lbSecurityGroup, - } = this.createLoadBalancer({ vpc, port, healtCheckPath }); + } = this.createLoadBalancer({ vpc, port, healthCheckPath }); this.lb = lb; this.lbTargetGroup = lbTargetGroup; this.lbHttpListener = lbHttpListener; @@ -188,8 +188,8 @@ export class WebServer extends pulumi.ComponentResource { private createLoadBalancer({ vpc, port, - healtCheckPath, - }: Pick) { + healthCheckPath, + }: Pick) { const lbSecurityGroup = new aws.ec2.SecurityGroup( `${this.name}-lb-security-group`, { @@ -248,7 +248,7 @@ export class WebServer extends pulumi.ComponentResource { unhealthyThreshold: 2, interval: 60, timeout: 5, - path: healtCheckPath || defaults.healtCheckPath, + path: healthCheckPath || defaults.healthCheckPath, }, tags: { ...commonTags, Name: `${this.name}-lb-target-group` }, }, From 73cfe27702e69f64a4ba17d0d498474d4cc0a06e Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 17:33:28 +0100 Subject: [PATCH 40/58] Updated healthcheck path --- src/components/ecs-service.ts | 4 ++++ src/components/mongo.ts | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index 33890a0..3393a36 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -71,6 +71,10 @@ export type EcsServiceArgs = { * - xlarge (2 vCPU, 4 GB memory) */ size?: pulumi.Input; + /** + * Path for the healthh check request. Defaults to "/healthcheck". + */ + healthCheckPath?: pulumi.Input; /** * The environment variables to pass to a container. Don't use this field for * sensitive information such as passwords, API keys, etc. For that purpose, diff --git a/src/components/mongo.ts b/src/components/mongo.ts index cafa5f2..1eab1c9 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -19,6 +19,10 @@ export type MongoArgs = { * - xlarge (2 vCPU, 4 GB memory) */ size?: pulumi.Input; + /** + * Path for the healthh check request. Defaults to "/healthcheck". + */ + healthCheckPath?: pulumi.Input; /** * The aws.ecs.Cluster resource. */ @@ -51,7 +55,8 @@ export class Mongo extends pulumi.ComponentResource { ) { super('studion:Mongo', name, args, opts); - const { port, size, cluster, vpc, environment, secrets } = args; + const { port, size, cluster, vpc, environment, secrets, healthCheckPath } = + args; const securityGroup = new aws.ec2.SecurityGroup( `${name}-service-security-group`, @@ -97,6 +102,7 @@ export class Mongo extends pulumi.ComponentResource { assignPublicIp: false, vpc, securityGroup, + ...(healthCheckPath && { healthCheckPath }), }, { ...opts, parent: this }, ); From bbf078b43da82b4e1cf0da52a8262e242e232c73 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 17:45:51 +0100 Subject: [PATCH 41/58] Updated mongo readme --- README.md | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 443fd56..96a7b2d 100644 --- a/README.md +++ b/README.md @@ -125,16 +125,9 @@ type MongoService = { | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); - image: pulumi.Input; port: pulumi.Input; size?: pulumi.Input; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; + healthCheckPath?: pulumi.Input; }; ``` @@ -446,7 +439,7 @@ Features:
```ts -new Mongo(name: string, args: EcsArgs, opts?: pulumi.ComponentResourceOptions ); +new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions ); ``` | Argument | Description | @@ -456,23 +449,16 @@ new Mongo(name: string, args: EcsArgs, opts?: pulumi.ComponentResourceOptions ); | opts | Bag of options to control resource's behavior. | ```ts - export type EcsArgs = { - type: 'MONGO'; - serviceName: string; + export type MongoArgs = { + port: pulumi.Input; + size?: pulumi.Input; + healthCheckPath?: pulumi.Input; + cluster: aws.ecs.Cluster; + vpc: awsx.ec2.Vpc; environment?: | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); - image: pulumi.Input; - port: pulumi.Input; - size?: pulumi.Input; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; ``` #### Exec into running ECS task From 3523a943ec8ce90e52fd9a5e1bd5bcd224ce7cc5 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 17:49:55 +0100 Subject: [PATCH 42/58] Fixes --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 96a7b2d..2ab2f11 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,13 @@ type MongoService = { port: pulumi.Input; size?: pulumi.Input; healthCheckPath?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; }; ``` @@ -459,6 +466,13 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; ``` #### Exec into running ECS task From 0b2e31a2194dacb33224a9e548a416c9ca3ffdd2 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 14 Nov 2023 17:51:43 +0100 Subject: [PATCH 43/58] Added remaining propterties --- src/components/mongo.ts | 29 ++++++++++++++++++++++++++--- src/components/web-server.ts | 18 +++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 1eab1c9..eb7b681 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,7 +1,7 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; import * as awsx from '@pulumi/awsx'; -import { EcsService } from './ecs-service'; +import { EcsService, RoleInlinePolicy } from './ecs-service'; import { Size } from '../types/size'; import { commonTags } from '../constants'; @@ -42,6 +42,16 @@ export type MongoArgs = { * The secrets to pass to the container. Defaults to []. */ secrets?: aws.ecs.Secret[]; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + /** + * A map of tags to assign to the resource. + */ + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; }; export class Mongo extends pulumi.ComponentResource { @@ -55,8 +65,18 @@ export class Mongo extends pulumi.ComponentResource { ) { super('studion:Mongo', name, args, opts); - const { port, size, cluster, vpc, environment, secrets, healthCheckPath } = - args; + const { + port, + size, + cluster, + vpc, + environment, + secrets, + healthCheckPath, + taskExecutionRoleInlinePolicies, + taskRoleInlinePolicies, + tags, + } = args; const securityGroup = new aws.ec2.SecurityGroup( `${name}-service-security-group`, @@ -103,6 +123,9 @@ export class Mongo extends pulumi.ComponentResource { vpc, securityGroup, ...(healthCheckPath && { healthCheckPath }), + taskExecutionRoleInlinePolicies, + taskRoleInlinePolicies, + tags, }, { ...opts, parent: this }, ); diff --git a/src/components/web-server.ts b/src/components/web-server.ts index dd7420c..fb75c78 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -3,7 +3,7 @@ import * as aws from '@pulumi/aws'; import * as awsx from '@pulumi/awsx'; import { commonTags } from '../constants'; import { AcmCertificate } from './acm-certificate'; -import { EcsService, defaults } from './ecs-service'; +import { EcsService, RoleInlinePolicy, defaults } from './ecs-service'; import { Size } from '../types/size'; export type WebServerArgs = { @@ -68,6 +68,16 @@ export type WebServerArgs = { * The ID of the hosted zone. */ hostedZoneId: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + /** + * A map of tags to assign to the resource. + */ + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; }; export class WebServer extends pulumi.ComponentResource { @@ -101,6 +111,9 @@ export class WebServer extends pulumi.ComponentResource { desiredCount, minCount, maxCount, + taskExecutionRoleInlinePolicies, + taskRoleInlinePolicies, + tags, } = args; this.name = name; @@ -161,6 +174,9 @@ export class WebServer extends pulumi.ComponentResource { assignPublicIp: true, vpc, securityGroup, + taskExecutionRoleInlinePolicies, + taskRoleInlinePolicies, + tags, }, { ...opts, parent: this }, ); From b42afdece1d1f60721f9ae649c8aea759519a92d Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 10:10:46 +0100 Subject: [PATCH 44/58] Fixes --- README.md | 2 -- src/components/ecs-service.ts | 8 +------- src/components/mongo.ts | 17 +++++++---------- src/components/web-server.ts | 8 +++++--- 4 files changed, 13 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2ab2f11..4692cc5 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,6 @@ type MongoService = { secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); port: pulumi.Input; size?: pulumi.Input; - healthCheckPath?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< pulumi.Input[] >; @@ -459,7 +458,6 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions export type MongoArgs = { port: pulumi.Input; size?: pulumi.Input; - healthCheckPath?: pulumi.Input; cluster: aws.ecs.Cluster; vpc: awsx.ec2.Vpc; environment?: diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index 3393a36..da8b91a 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -162,7 +162,6 @@ export class EcsService extends pulumi.ComponentResource { if (argsWithDefaults.enableServiceAutoDiscovery) { this.serviceDiscoveryService = this.createServiceDiscovery( argsWithDefaults.vpc, - argsWithDefaults.healthCheckPath, ); } this.service = this.createEcsService(args, opts); @@ -423,7 +422,7 @@ export class EcsService extends pulumi.ComponentResource { return taskDefinition; } - private createServiceDiscovery(vpc: awsx.ec2.Vpc, healthCheckPath: string) { + private createServiceDiscovery(vpc: awsx.ec2.Vpc) { const privateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace( `${this.name}-private-dns-namespace`, { @@ -448,11 +447,6 @@ export class EcsService extends pulumi.ComponentResource { ], routingPolicy: 'MULTIVALUE', }, - healthCheckConfig: { - type: 'HTTP', - resourcePath: healthCheckPath || defaults.healthCheckPath, - failureThreshold: 2, - }, tags: commonTags, }, { parent: this }, diff --git a/src/components/mongo.ts b/src/components/mongo.ts index eb7b681..0f81722 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -19,10 +19,6 @@ export type MongoArgs = { * - xlarge (2 vCPU, 4 GB memory) */ size?: pulumi.Input; - /** - * Path for the healthh check request. Defaults to "/healthcheck". - */ - healthCheckPath?: pulumi.Input; /** * The aws.ecs.Cluster resource. */ @@ -72,7 +68,6 @@ export class Mongo extends pulumi.ComponentResource { vpc, environment, secrets, - healthCheckPath, taskExecutionRoleInlinePolicies, taskRoleInlinePolicies, tags, @@ -107,7 +102,8 @@ export class Mongo extends pulumi.ComponentResource { this.service = new EcsService( name, { - image: 'mongo:latest', + image: + 'mongo@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', port, cluster, desiredCount: 1, @@ -122,10 +118,11 @@ export class Mongo extends pulumi.ComponentResource { assignPublicIp: false, vpc, securityGroup, - ...(healthCheckPath && { healthCheckPath }), - taskExecutionRoleInlinePolicies, - taskRoleInlinePolicies, - tags, + ...(taskExecutionRoleInlinePolicies && { + taskExecutionRoleInlinePolicies, + }), + ...(taskRoleInlinePolicies && { taskRoleInlinePolicies }), + ...(tags && { tags }), }, { ...opts, parent: this }, ); diff --git a/src/components/web-server.ts b/src/components/web-server.ts index fb75c78..28d5319 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -174,9 +174,11 @@ export class WebServer extends pulumi.ComponentResource { assignPublicIp: true, vpc, securityGroup, - taskExecutionRoleInlinePolicies, - taskRoleInlinePolicies, - tags, + ...(taskExecutionRoleInlinePolicies && { + taskExecutionRoleInlinePolicies, + }), + ...(taskRoleInlinePolicies && { taskRoleInlinePolicies }), + ...(tags && { tags }), }, { ...opts, parent: this }, ); From 921112e96f6717dde5e9a85a5dea19886397f8b0 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 11:11:47 +0100 Subject: [PATCH 45/58] Added ecs service --- src/components/ecs-service.ts | 4 +-- src/components/mongo.ts | 6 ++--- src/components/project.ts | 49 ++++++++++++++++++++++++++++++++--- src/components/web-server.ts | 6 ++--- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index da8b91a..040f626 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -141,7 +141,7 @@ export const defaults = { enableAutoScaling: false, }; -export class EcsService extends pulumi.ComponentResource { +export class Ecs extends pulumi.ComponentResource { name: string; logGroup: aws.cloudwatch.LogGroup; taskDefinition: aws.ecs.TaskDefinition; @@ -470,7 +470,7 @@ export class EcsService extends pulumi.ComponentResource { fromPort: 0, toPort: 0, protocol: '-1', - securityGroups: [argsWithDefaults.vpc.vpc.cidrBlock], + cidrBlocks: [argsWithDefaults.vpc.vpc.cidrBlock], }, ], egress: [ diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 0f81722..8567078 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,7 +1,7 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; import * as awsx from '@pulumi/awsx'; -import { EcsService, RoleInlinePolicy } from './ecs-service'; +import { Ecs, RoleInlinePolicy } from './ecs-service'; import { Size } from '../types/size'; import { commonTags } from '../constants'; @@ -52,7 +52,7 @@ export type MongoArgs = { export class Mongo extends pulumi.ComponentResource { name: string; - service: EcsService; + service: Ecs; constructor( name: string, @@ -99,7 +99,7 @@ export class Mongo extends pulumi.ComponentResource { ); this.name = name; - this.service = new EcsService( + this.service = new Ecs( name, { image: diff --git a/src/components/project.ts b/src/components/project.ts index 7d45b8b..da6083d 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -9,8 +9,9 @@ import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; +import { Ecs, EcsServiceArgs } from './ecs-service'; -export type Service = Database | Redis | StaticSite | WebServer | Mongo; +export type Service = Database | Redis | StaticSite | WebServer | Mongo | Ecs; export type Services = Record; type ServiceArgs = { @@ -50,6 +51,18 @@ export type MongoService = { } & ServiceArgs & Omit; +export type EcsService = { + type: 'ECS'; + environment?: + | aws.ecs.KeyValuePair[] + | ((services: Services) => aws.ecs.KeyValuePair[]); + secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); +} & ServiceArgs & + Omit< + EcsServiceArgs, + 'cluster' | 'vpc' | 'hostedZoneId' | 'environment' | 'secrets' + >; + export type ProjectArgs = { services: ( | DatabaseService @@ -57,6 +70,7 @@ export type ProjectArgs = { | StaticSiteService | WebServerService | MongoService + | EcsService )[]; hostedZoneId?: pulumi.Input; enableSSMConnect?: pulumi.Input; @@ -132,8 +146,10 @@ export class Project extends pulumi.ComponentResource { private createServices(services: ProjectArgs['services']) { const hasRedisService = services.some(it => it.type === 'REDIS'); const shouldCreateEcsCluster = - services.some(it => it.type === 'WEB_SERVER' || it.type === 'MONGO') && - !this.cluster; + services.some( + it => + it.type === 'WEB_SERVER' || it.type === 'MONGO' || it.type === 'ECS', + ) && !this.cluster; if (hasRedisService) this.createRedisPrerequisites(); if (shouldCreateEcsCluster) this.createCluster(); services.forEach(it => { @@ -142,6 +158,7 @@ export class Project extends pulumi.ComponentResource { if (it.type === 'STATIC_SITE') this.createStaticSiteService(it); if (it.type === 'WEB_SERVER') this.createWebServerService(it); if (it.type === 'MONGO') this.createMongoService(it); + if (it.type === 'ECS') this.createEcsService(it); }); } @@ -256,4 +273,30 @@ export class Project extends pulumi.ComponentResource { ); this.services[options.serviceName] = service; } + + private createEcsService(options: EcsService) { + if (!this.cluster) throw new MissingCluster(); + + const { serviceName, environment, secrets, ...ecsOptions } = options; + const parsedEnv = + typeof environment === 'function' + ? environment(this.services) + : environment; + + const parsedSecrets = + typeof secrets === 'function' ? secrets(this.services) : secrets; + + const service = new Ecs( + serviceName, + { + ...ecsOptions, + cluster: this.cluster, + vpc: this.vpc, + environment: parsedEnv, + secrets: parsedSecrets, + }, + { parent: this }, + ); + this.services[options.serviceName] = service; + } } diff --git a/src/components/web-server.ts b/src/components/web-server.ts index 28d5319..ed9c84a 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -3,7 +3,7 @@ import * as aws from '@pulumi/aws'; import * as awsx from '@pulumi/awsx'; import { commonTags } from '../constants'; import { AcmCertificate } from './acm-certificate'; -import { EcsService, RoleInlinePolicy, defaults } from './ecs-service'; +import { Ecs, RoleInlinePolicy, defaults } from './ecs-service'; import { Size } from '../types/size'; export type WebServerArgs = { @@ -82,7 +82,7 @@ export type WebServerArgs = { export class WebServer extends pulumi.ComponentResource { name: string; - service: EcsService; + service: Ecs; certificate: AcmCertificate; lbSecurityGroup: aws.ec2.SecurityGroup; lb: aws.lb.LoadBalancer; @@ -156,7 +156,7 @@ export class WebServer extends pulumi.ComponentResource { { parent: this }, ); - this.service = new EcsService( + this.service = new Ecs( name, { image, From 34ea6a15eac158cd825638cb6efc0b7e66d0c527 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 15:30:10 +0100 Subject: [PATCH 46/58] Mongo port is optional --- README.md | 4 ++-- src/components/mongo.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4692cc5..98d0be0 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ type MongoService = { | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); - port: pulumi.Input; + port?: pulumi.Input; size?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< pulumi.Input[] @@ -456,7 +456,7 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions ```ts export type MongoArgs = { - port: pulumi.Input; + port?: pulumi.Input; size?: pulumi.Input; cluster: aws.ecs.Cluster; vpc: awsx.ec2.Vpc; diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 8567078..c346d36 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -9,7 +9,7 @@ export type MongoArgs = { /** * Exposed service port. */ - port: pulumi.Input; + port?: pulumi.Input; /** * CPU and memory size used for running the container. Defaults to "small". * Available predefined options are: @@ -73,14 +73,16 @@ export class Mongo extends pulumi.ComponentResource { tags, } = args; + const servicePort = port || 27017; + const securityGroup = new aws.ec2.SecurityGroup( `${name}-service-security-group`, { vpcId: vpc.vpcId, ingress: [ { - fromPort: port, - toPort: port, + fromPort: servicePort, + toPort: servicePort, protocol: 'tcp', cidrBlocks: [vpc.vpc.cidrBlock], }, @@ -104,7 +106,7 @@ export class Mongo extends pulumi.ComponentResource { { image: 'mongo@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', - port, + port: servicePort, cluster, desiredCount: 1, minCount: 1, @@ -114,7 +116,7 @@ export class Mongo extends pulumi.ComponentResource { secrets, enableServiceAutoDiscovery: true, persistentStorageVolumePath: '/data/db', - dockerCommand: ['mongod', '--port', port.toString()], + dockerCommand: ['mongod', '--port', servicePort.toString()], assignPublicIp: false, vpc, securityGroup, From d1277c9b1fee0671e6e02fe268b0795544b57cfc Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 15:31:28 +0100 Subject: [PATCH 47/58] Removed not needed args --- README.md | 8 -------- src/components/mongo.ts | 20 +------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/README.md b/README.md index 98d0be0..95f61fc 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,6 @@ type MongoService = { secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); port?: pulumi.Input; size?: pulumi.Input; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; tags?: pulumi.Input<{ [key: string]: pulumi.Input; }>; @@ -464,10 +460,6 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; tags?: pulumi.Input<{ [key: string]: pulumi.Input; }>; diff --git a/src/components/mongo.ts b/src/components/mongo.ts index c346d36..f4ac74b 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -38,10 +38,6 @@ export type MongoArgs = { * The secrets to pass to the container. Defaults to []. */ secrets?: aws.ecs.Secret[]; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; /** * A map of tags to assign to the resource. */ @@ -61,17 +57,7 @@ export class Mongo extends pulumi.ComponentResource { ) { super('studion:Mongo', name, args, opts); - const { - port, - size, - cluster, - vpc, - environment, - secrets, - taskExecutionRoleInlinePolicies, - taskRoleInlinePolicies, - tags, - } = args; + const { port, size, cluster, vpc, environment, secrets, tags } = args; const servicePort = port || 27017; @@ -120,10 +106,6 @@ export class Mongo extends pulumi.ComponentResource { assignPublicIp: false, vpc, securityGroup, - ...(taskExecutionRoleInlinePolicies && { - taskExecutionRoleInlinePolicies, - }), - ...(taskRoleInlinePolicies && { taskRoleInlinePolicies }), ...(tags && { tags }), }, { ...opts, parent: this }, From ad04b754231a9653c562ab02b549680e7b3676e8 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 15:32:41 +0100 Subject: [PATCH 48/58] Simplified args --- src/components/project.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/project.ts b/src/components/project.ts index da6083d..c3abd9c 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -44,10 +44,8 @@ export type WebServerService = { export type MongoService = { type: 'MONGO'; - environment?: - | aws.ecs.KeyValuePair[] - | ((services: Services) => aws.ecs.KeyValuePair[]); - secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); + environment?: aws.ecs.KeyValuePair[]; + secrets?: aws.ecs.Secret[]; } & ServiceArgs & Omit; From 66c633d726e29b633ec8c6d96745814a063726ed Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 15:34:55 +0100 Subject: [PATCH 49/58] Formatting --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 95f61fc..2392e8d 100644 --- a/README.md +++ b/README.md @@ -157,13 +157,13 @@ export type StaticSiteService = { export type WebServerService = { type: 'WEB_SERVER'; serviceName: string; + image: pulumi.Input; + port: pulumi.Input; + domain: pulumi.Input; environment?: | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); - image: pulumi.Input; - port: pulumi.Input; - domain: pulumi.Input; desiredCount?: pulumi.Input; minCount?: pulumi.Input; maxCount?: pulumi.Input; @@ -452,10 +452,10 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions ```ts export type MongoArgs = { - port?: pulumi.Input; - size?: pulumi.Input; cluster: aws.ecs.Cluster; vpc: awsx.ec2.Vpc; + port?: pulumi.Input; + size?: pulumi.Input; environment?: | aws.ecs.KeyValuePair[] | ((services: Services) => aws.ecs.KeyValuePair[]); From b4681fef2516d70aa83a232f81f470e442ff3e59 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 15:41:08 +0100 Subject: [PATCH 50/58] Added mongo version --- src/components/mongo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index f4ac74b..2ce1a30 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -91,7 +91,7 @@ export class Mongo extends pulumi.ComponentResource { name, { image: - 'mongo@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', + 'mongo:jammy@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', port: servicePort, cluster, desiredCount: 1, From ddc40f8600ecde572e959390e635acc0ff8dd613 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 16:06:57 +0100 Subject: [PATCH 51/58] Updated mongo args --- src/components/mongo.ts | 41 ++++----------------------------------- src/components/project.ts | 11 ++--------- 2 files changed, 6 insertions(+), 46 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 2ce1a30..fa66b99 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -57,56 +57,23 @@ export class Mongo extends pulumi.ComponentResource { ) { super('studion:Mongo', name, args, opts); - const { port, size, cluster, vpc, environment, secrets, tags } = args; - - const servicePort = port || 27017; - - const securityGroup = new aws.ec2.SecurityGroup( - `${name}-service-security-group`, - { - vpcId: vpc.vpcId, - ingress: [ - { - fromPort: servicePort, - toPort: servicePort, - protocol: 'tcp', - cidrBlocks: [vpc.vpc.cidrBlock], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - cidrBlocks: ['0.0.0.0/0'], - }, - ], - tags: commonTags, - }, - { parent: this }, - ); + const port = args.port || 27017; this.name = name; this.service = new Ecs( name, { + ...args, + port, image: 'mongo:jammy@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', - port: servicePort, - cluster, desiredCount: 1, minCount: 1, maxCount: 1, - ...(size && { size }), - environment, - secrets, enableServiceAutoDiscovery: true, persistentStorageVolumePath: '/data/db', - dockerCommand: ['mongod', '--port', servicePort.toString()], + dockerCommand: ['mongod', '--port', port.toString()], assignPublicIp: false, - vpc, - securityGroup, - ...(tags && { tags }), }, { ...opts, parent: this }, ); diff --git a/src/components/project.ts b/src/components/project.ts index c3abd9c..9e91d77 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -250,13 +250,6 @@ export class Project extends pulumi.ComponentResource { if (!this.cluster) throw new MissingCluster(); const { serviceName, environment, secrets, ...ecsOptions } = options; - const parsedEnv = - typeof environment === 'function' - ? environment(this.services) - : environment; - - const parsedSecrets = - typeof secrets === 'function' ? secrets(this.services) : secrets; const service = new Mongo( serviceName, @@ -264,8 +257,8 @@ export class Project extends pulumi.ComponentResource { ...ecsOptions, cluster: this.cluster, vpc: this.vpc, - environment: parsedEnv, - secrets: parsedSecrets, + environment: environment, + secrets: secrets, }, { parent: this }, ); From 84c28523ae1a76e2fcffedd61294c87b8b7801d7 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 16:31:30 +0100 Subject: [PATCH 52/58] Updated readme --- README.md | 132 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2392e8d..e3f2212 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ $ pulumi up 3. [Redis](#redis) 4. [StaticSite](#static-site) 5. [WebServer](#web-server) +6. [Mongo](#mongo) +7. [Ecs](#ecs) ### Project @@ -87,6 +89,7 @@ type ProjectArgs = { | StaticSiteService | WebServerService | MongoService + | EcsService )[]; hostedZoneId?: pulumi.Input; enableSSMConnect?: pulumi.Input; @@ -117,22 +120,6 @@ type DatabaseService = { }; ``` -```ts -type MongoService = { - type: 'MONGO'; - serviceName: string; - environment?: - | aws.ecs.KeyValuePair[] - | ((services: Services) => aws.ecs.KeyValuePair[]); - secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); - port?: pulumi.Input; - size?: pulumi.Input; - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; -}; -``` - ```ts export type RedisService = { type: 'REDIS'; @@ -179,6 +166,53 @@ export type WebServerService = { }; ``` +```ts +type MongoService = { + type: 'MONGO'; + serviceName: string; + environment?: aws.ecs.KeyValuePair[]; + secrets?: aws.ecs.Secret[]; + port?: pulumi.Input; + size?: pulumi.Input; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +}; +``` + +```ts +type EcsService = { + type: 'ECS'; + serviceName: string; + image: pulumi.Input; + port: pulumi.Input; + domain: pulumi.Input; + enableServiceAutoDiscovery: pulumi.Input; + enableAutoScaling?: pulumi.Input; + lbTargetGroupArn?: aws.lb.TargetGroup['arn']; + persistentStorageVolumePath?: pulumi.Input; + securityGroup?: aws.ec2.SecurityGroup; + assignPublicIp?: pulumi.Input; + dockerCommand?: pulumi.Input; + environment?: + | aws.ecs.KeyValuePair[] + | ((services: Services) => aws.ecs.KeyValuePair[]); + secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); + desiredCount?: pulumi.Input; + minCount?: pulumi.Input; + maxCount?: pulumi.Input; + size?: pulumi.Input; + healthCheckPath?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +}; +``` + Often, web server depends on other services such as database, Redis, etc. For that purpose, environment factory can be used. The factory function recieves services bag as argument. @@ -384,7 +418,7 @@ AWS ECS Fargate web server. Features: -- Memory and CPU autoscaling enabled +- memory and CPU autoscaling enabled - creates TLS certificate for the specified domain - redirects HTTP traffic to HTTPS - creates CloudWatch log group @@ -427,7 +461,7 @@ export type WebServerArgs = { }; ``` -### Monogo +### Mongo AWS ECS Fargate mongo service. @@ -456,15 +490,69 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions vpc: awsx.ec2.Vpc; port?: pulumi.Input; size?: pulumi.Input; - environment?: - | aws.ecs.KeyValuePair[] - | ((services: Services) => aws.ecs.KeyValuePair[]); - secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); + environment?: aws.ecs.KeyValuePair[]; + secrets?: aws.ecs.Secret[]; tags?: pulumi.Input<{ [key: string]: pulumi.Input; }>; ``` +### Ecs + +AWS ECS Fargate service. + +Features: + +- memory and CPU autoscaling +- service auto discovery +- persistent storage +- supports load balancing +- reates CloudWatch log group +- public IP address +- comes with predefined cpu and memory options: `small`, `medium`, `large`, `xlarge` + +
+ +```ts +new Ecs(name: string, args: EcsServiceArgs, opts?: pulumi.ComponentResourceOptions ); +``` + +| Argument | Description | +| :------- | :--------------------------------------------: | +| name \* | The unique name of the resource. | +| args \* | The arguments to resource properties. | +| opts | Bag of options to control resource's behavior. | + +```ts +export type EcsServiceArgs = { + image: pulumi.Input; + port: pulumi.Input; + cluster: aws.ecs.Cluster; + vpc: awsx.ec2.Vpc; + desiredCount?: pulumi.Input; + minCount?: pulumi.Input; + maxCount?: pulumi.Input; + size?: pulumi.Input; + healthCheckPath?: pulumi.Input; + environment?: aws.ecs.KeyValuePair[]; + secrets?: aws.ecs.Secret[]; + enableServiceAutoDiscovery: pulumi.Input; + persistentStorageVolumePath?: pulumi.Input; + dockerCommand?: pulumi.Input; + enableAutoScaling?: pulumi.Input; + lbTargetGroupArn?: aws.lb.TargetGroup['arn']; + securityGroup?: aws.ec2.SecurityGroup; + assignPublicIp?: pulumi.Input; + taskExecutionRoleInlinePolicies?: pulumi.Input< + pulumi.Input[] + >; + taskRoleInlinePolicies?: pulumi.Input[]>; + tags?: pulumi.Input<{ + [key: string]: pulumi.Input; + }>; +}; +``` + #### Exec into running ECS task **Prerequisites** From 5ef8dbc5a818729cc26054f10e26cf8ff4e46c65 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Wed, 15 Nov 2023 16:52:16 +0100 Subject: [PATCH 53/58] Extracted common args --- src/components/mongo.ts | 45 +++------------------ src/components/web-server.ts | 77 ++++++------------------------------ 2 files changed, 16 insertions(+), 106 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index fa66b99..92f6ff1 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,49 +1,14 @@ import * as pulumi from '@pulumi/pulumi'; -import * as aws from '@pulumi/aws'; -import * as awsx from '@pulumi/awsx'; -import { Ecs, RoleInlinePolicy } from './ecs-service'; -import { Size } from '../types/size'; -import { commonTags } from '../constants'; +import { Ecs, EcsServiceArgs } from './ecs-service'; -export type MongoArgs = { +export type MongoArgs = Pick< + EcsServiceArgs, + 'size' | 'cluster' | 'vpc' | 'environment' | 'secrets' | 'tags' +> & { /** * Exposed service port. */ port?: pulumi.Input; - /** - * CPU and memory size used for running the container. Defaults to "small". - * Available predefined options are: - * - small (0.25 vCPU, 0.5 GB memory) - * - medium (0.5 vCPU, 1 GB memory) - * - large (1 vCPU memory, 2 GB memory) - * - xlarge (2 vCPU, 4 GB memory) - */ - size?: pulumi.Input; - /** - * The aws.ecs.Cluster resource. - */ - cluster: aws.ecs.Cluster; - /** - * The awsx.ec2.Vpc resource. - */ - vpc: awsx.ec2.Vpc; - /** - * The environment variables to pass to a container. Don't use this field for - * sensitive information such as passwords, API keys, etc. For that purpose, - * please use the `secrets` property. - * Defaults to []. - */ - environment?: aws.ecs.KeyValuePair[]; - /** - * The secrets to pass to the container. Defaults to []. - */ - secrets?: aws.ecs.Secret[]; - /** - * A map of tags to assign to the resource. - */ - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; }; export class Mongo extends pulumi.ComponentResource { diff --git a/src/components/web-server.ts b/src/components/web-server.ts index ed9c84a..7d8c255 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -1,83 +1,28 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; -import * as awsx from '@pulumi/awsx'; import { commonTags } from '../constants'; import { AcmCertificate } from './acm-certificate'; -import { Ecs, RoleInlinePolicy, defaults } from './ecs-service'; -import { Size } from '../types/size'; +import { Ecs, EcsServiceArgs, defaults } from './ecs-service'; -export type WebServerArgs = { - /** - * The ECR image used to start a container. - */ - image: pulumi.Input; - /* - * Exposed service port. - */ - port: pulumi.Input; +export type WebServerArgs = Omit< + EcsServiceArgs, + | 'enableServiceAutoDiscovery' + | 'persistentStorageVolumePath' + | 'dockerCommand' + | 'enableAutoScaling' + | 'lbTargetGroupArn' + | 'securityGroup' + | 'assignPublicIp' +> & { /** * The domain which will be used to access the service. * The domain or subdomain must belong to the provided hostedZone. */ domain: pulumi.Input; - /** - * CPU and memory size used for running the container. Defaults to "small". - * Available predefined options are: - * - small (0.25 vCPU, 0.5 GB memory) - * - medium (0.5 vCPU, 1 GB memory) - * - large (1 vCPU memory, 2 GB memory) - * - xlarge (2 vCPU, 4 GB memory) - */ - size?: pulumi.Input; - /** - * Number of instances of the task definition to place and keep running. Defaults to 1. - */ - desiredCount?: pulumi.Input; - /** - * Min capacity of the scalable target. Defaults to 1. - */ - minCount?: pulumi.Input; - /** - * Max capacity of the scalable target. Defaults to 10. - */ - maxCount?: pulumi.Input; - /** - * Path for the healthh check request. Defaults to "/healthcheck". - */ - healthCheckPath?: pulumi.Input; - /** - * The aws.ecs.Cluster resource. - */ - cluster: aws.ecs.Cluster; - /** - * The awsx.ec2.Vpc resource. - */ - vpc: awsx.ec2.Vpc; - /** - * The environment variables to pass to a container. Don't use this field for - * sensitive information such as passwords, API keys, etc. For that purpose, - * please use the `secrets` property. - * Defaults to []. - */ - environment?: aws.ecs.KeyValuePair[]; - /** - * The secrets to pass to the container. Defaults to []. - */ - secrets?: aws.ecs.Secret[]; /** * The ID of the hosted zone. */ hostedZoneId: pulumi.Input; - taskExecutionRoleInlinePolicies?: pulumi.Input< - pulumi.Input[] - >; - taskRoleInlinePolicies?: pulumi.Input[]>; - /** - * A map of tags to assign to the resource. - */ - tags?: pulumi.Input<{ - [key: string]: pulumi.Input; - }>; }; export class WebServer extends pulumi.ComponentResource { From 9d7fbe861f11a76e8107038a6a0955bca5948f04 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 16 Nov 2023 11:38:36 +0100 Subject: [PATCH 54/58] Fixes --- README.md | 13 +++---- src/components/ecs-service.ts | 2 +- src/components/mongo.ts | 6 ++-- src/components/project.ts | 66 +++++++++++++++++++---------------- src/components/web-server.ts | 6 ++-- 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index e3f2212..5baae12 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ $ pulumi up 4. [StaticSite](#static-site) 5. [WebServer](#web-server) 6. [Mongo](#mongo) -7. [Ecs](#ecs) +7. [EcsService](#ecs-service) ### Project @@ -467,8 +467,8 @@ AWS ECS Fargate mongo service. Features: -- has persistent storage -- comes with service discovery namespace +- persistent storage +- service auto-discovery - creates CloudWatch log group - comes with predefined cpu and memory options: `small`, `medium`, `large`, `xlarge` @@ -485,7 +485,7 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions | opts | Bag of options to control resource's behavior. | ```ts - export type MongoArgs = { +export type MongoArgs = { cluster: aws.ecs.Cluster; vpc: awsx.ec2.Vpc; port?: pulumi.Input; @@ -495,9 +495,10 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions tags?: pulumi.Input<{ [key: string]: pulumi.Input; }>; +}; ``` -### Ecs +### Ecs Service AWS ECS Fargate service. @@ -514,7 +515,7 @@ Features:
```ts -new Ecs(name: string, args: EcsServiceArgs, opts?: pulumi.ComponentResourceOptions ); +new EcsService(name: string, args: EcsServiceArgs, opts?: pulumi.ComponentResourceOptions ); ``` | Argument | Description | diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index 040f626..9e3b29d 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -141,7 +141,7 @@ export const defaults = { enableAutoScaling: false, }; -export class Ecs extends pulumi.ComponentResource { +export class EcsService extends pulumi.ComponentResource { name: string; logGroup: aws.cloudwatch.LogGroup; taskDefinition: aws.ecs.TaskDefinition; diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 92f6ff1..823f37f 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,5 +1,5 @@ import * as pulumi from '@pulumi/pulumi'; -import { Ecs, EcsServiceArgs } from './ecs-service'; +import { EcsService, EcsServiceArgs } from './ecs-service'; export type MongoArgs = Pick< EcsServiceArgs, @@ -13,7 +13,7 @@ export type MongoArgs = Pick< export class Mongo extends pulumi.ComponentResource { name: string; - service: Ecs; + service: EcsService; constructor( name: string, @@ -25,7 +25,7 @@ export class Mongo extends pulumi.ComponentResource { const port = args.port || 27017; this.name = name; - this.service = new Ecs( + this.service = new EcsService( name, { ...args, diff --git a/src/components/project.ts b/src/components/project.ts index 9e91d77..2dad54b 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -5,13 +5,19 @@ import * as upstash from '@upstash/pulumi'; import { Database, DatabaseArgs } from './database'; import { WebServer, WebServerArgs } from './web-server'; import { Mongo, MongoArgs } from './mongo'; -import { Redis, RedisArgs } from './redis'; +import { Redis, RedisArgs, RedisOptions } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; -import { Ecs, EcsServiceArgs } from './ecs-service'; - -export type Service = Database | Redis | StaticSite | WebServer | Mongo | Ecs; +import { EcsService, EcsServiceArgs } from './ecs-service'; + +export type Service = + | Database + | Redis + | StaticSite + | WebServer + | Mongo + | EcsService; export type Services = Record; type ServiceArgs = { @@ -21,16 +27,16 @@ type ServiceArgs = { serviceName: string; }; -export type DatabaseService = { type: 'DATABASE' } & ServiceArgs & +export type DatabaseServiceOptions = { type: 'DATABASE' } & ServiceArgs & Omit; -export type RedisService = { type: 'REDIS' } & ServiceArgs & +export type RedisServiceOptions = { type: 'REDIS' } & ServiceArgs & Pick; -export type StaticSiteService = { type: 'STATIC_SITE' } & ServiceArgs & +export type StaticSiteServiceOptions = { type: 'STATIC_SITE' } & ServiceArgs & Omit; -export type WebServerService = { +export type WebServerServiceOptions = { type: 'WEB_SERVER'; environment?: | aws.ecs.KeyValuePair[] @@ -42,14 +48,14 @@ export type WebServerService = { 'cluster' | 'vpc' | 'hostedZoneId' | 'environment' | 'secrets' >; -export type MongoService = { +export type MongoServiceOptions = { type: 'MONGO'; environment?: aws.ecs.KeyValuePair[]; secrets?: aws.ecs.Secret[]; } & ServiceArgs & Omit; -export type EcsService = { +export type EcsServiceOptions = { type: 'ECS'; environment?: | aws.ecs.KeyValuePair[] @@ -63,12 +69,12 @@ export type EcsService = { export type ProjectArgs = { services: ( - | DatabaseService - | RedisService - | StaticSiteService - | WebServerService - | MongoService - | EcsService + | DatabaseServiceOptions + | RedisServiceOptions + | StaticSiteServiceOptions + | WebServerServiceOptions + | MongoServiceOptions + | EcsServiceOptions )[]; hostedZoneId?: pulumi.Input; enableSSMConnect?: pulumi.Input; @@ -84,9 +90,9 @@ export class MissingHostedZoneId extends Error { } } -export class MissingCluster extends Error { +export class MissingEcsCluster extends Error { constructor() { - super('Cluster does not exist'); + super('Ecs Cluster does not exist'); this.name = this.constructor.name; } } @@ -149,7 +155,7 @@ export class Project extends pulumi.ComponentResource { it.type === 'WEB_SERVER' || it.type === 'MONGO' || it.type === 'ECS', ) && !this.cluster; if (hasRedisService) this.createRedisPrerequisites(); - if (shouldCreateEcsCluster) this.createCluster(); + if (shouldCreateEcsCluster) this.createEcsCluster(); services.forEach(it => { if (it.type === 'DATABASE') this.createDatabaseService(it); if (it.type === 'REDIS') this.createRedisService(it); @@ -169,7 +175,7 @@ export class Project extends pulumi.ComponentResource { }); } - private createCluster() { + private createEcsCluster() { const stack = pulumi.getStack(); this.cluster = new aws.ecs.Cluster( `${this.name}-cluster`, @@ -181,7 +187,7 @@ export class Project extends pulumi.ComponentResource { ); } - private createDatabaseService(options: DatabaseService) { + private createDatabaseService(options: DatabaseServiceOptions) { const { serviceName, type, ...databaseOptions } = options; const service = new Database( serviceName, @@ -194,7 +200,7 @@ export class Project extends pulumi.ComponentResource { this.services[serviceName] = service; } - private createRedisService(options: RedisService) { + private createRedisService(options: RedisServiceOptions) { if (!this.upstashProvider) return; const { serviceName, ...redisOptions } = options; const service = new Redis(serviceName, redisOptions, { @@ -204,7 +210,7 @@ export class Project extends pulumi.ComponentResource { this.services[options.serviceName] = service; } - private createStaticSiteService(options: StaticSiteService) { + private createStaticSiteService(options: StaticSiteServiceOptions) { const { serviceName, ...staticSiteOptions } = options; if (!this.hostedZoneId) throw new MissingHostedZoneId(options.type); const service = new StaticSite( @@ -218,8 +224,8 @@ export class Project extends pulumi.ComponentResource { this.services[serviceName] = service; } - private createWebServerService(options: WebServerService) { - if (!this.cluster) throw new MissingCluster(); + private createWebServerService(options: WebServerServiceOptions) { + if (!this.cluster) throw new MissingEcsCluster(); if (!this.hostedZoneId) throw new MissingHostedZoneId(options.type); const { serviceName, environment, secrets, ...ecsOptions } = options; @@ -246,8 +252,8 @@ export class Project extends pulumi.ComponentResource { this.services[options.serviceName] = service; } - private createMongoService(options: MongoService) { - if (!this.cluster) throw new MissingCluster(); + private createMongoService(options: MongoServiceOptions) { + if (!this.cluster) throw new MissingEcsCluster(); const { serviceName, environment, secrets, ...ecsOptions } = options; @@ -265,8 +271,8 @@ export class Project extends pulumi.ComponentResource { this.services[options.serviceName] = service; } - private createEcsService(options: EcsService) { - if (!this.cluster) throw new MissingCluster(); + private createEcsService(options: EcsServiceOptions) { + if (!this.cluster) throw new MissingEcsCluster(); const { serviceName, environment, secrets, ...ecsOptions } = options; const parsedEnv = @@ -277,7 +283,7 @@ export class Project extends pulumi.ComponentResource { const parsedSecrets = typeof secrets === 'function' ? secrets(this.services) : secrets; - const service = new Ecs( + const service = new EcsService( serviceName, { ...ecsOptions, diff --git a/src/components/web-server.ts b/src/components/web-server.ts index 7d8c255..55cfef9 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -2,7 +2,7 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; import { commonTags } from '../constants'; import { AcmCertificate } from './acm-certificate'; -import { Ecs, EcsServiceArgs, defaults } from './ecs-service'; +import { EcsService, EcsServiceArgs, defaults } from './ecs-service'; export type WebServerArgs = Omit< EcsServiceArgs, @@ -27,7 +27,7 @@ export type WebServerArgs = Omit< export class WebServer extends pulumi.ComponentResource { name: string; - service: Ecs; + service: EcsService; certificate: AcmCertificate; lbSecurityGroup: aws.ec2.SecurityGroup; lb: aws.lb.LoadBalancer; @@ -101,7 +101,7 @@ export class WebServer extends pulumi.ComponentResource { { parent: this }, ); - this.service = new Ecs( + this.service = new EcsService( name, { image, From fb57089e90dcefedd1419f8e2568aaf3e4806655 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 16 Nov 2023 14:27:12 +0100 Subject: [PATCH 55/58] Added db username and password secrets --- README.md | 8 ++--- src/components/mongo.ts | 76 +++++++++++++++++++++++++++++++++++++-- src/components/project.ts | 8 ++--- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5baae12..fbfcaf0 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,8 @@ export type WebServerService = { type MongoService = { type: 'MONGO'; serviceName: string; - environment?: aws.ecs.KeyValuePair[]; - secrets?: aws.ecs.Secret[]; + username: pulumi.Input; + password: pulumi.Input; port?: pulumi.Input; size?: pulumi.Input; tags?: pulumi.Input<{ @@ -488,10 +488,10 @@ new Mongo(name: string, args: MongoArgs, opts?: pulumi.ComponentResourceOptions export type MongoArgs = { cluster: aws.ecs.Cluster; vpc: awsx.ec2.Vpc; + username: pulumi.Input; + password: pulumi.Input; port?: pulumi.Input; size?: pulumi.Input; - environment?: aws.ecs.KeyValuePair[]; - secrets?: aws.ecs.Secret[]; tags?: pulumi.Input<{ [key: string]: pulumi.Input; }>; diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 823f37f..e30076b 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -1,19 +1,31 @@ import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws'; +import { commonTags } from '../constants'; import { EcsService, EcsServiceArgs } from './ecs-service'; export type MongoArgs = Pick< EcsServiceArgs, - 'size' | 'cluster' | 'vpc' | 'environment' | 'secrets' | 'tags' + 'size' | 'cluster' | 'vpc' | 'tags' > & { /** * Exposed service port. */ port?: pulumi.Input; + /** + * Username for the master DB user. + */ + username: pulumi.Input; + /** + * Password for the master DB user. + * The value will be stored as a secret in AWS Secret Manager. + */ + password: pulumi.Input; }; export class Mongo extends pulumi.ComponentResource { name: string; service: EcsService; + databaseSecrets: aws.ecs.Secret[]; constructor( name: string, @@ -24,11 +36,15 @@ export class Mongo extends pulumi.ComponentResource { const port = args.port || 27017; + const { username, password, ...ecsArgs } = args; + this.name = name; + this.databaseSecrets = this.createDatabaseSecrets({ username, password }); + this.service = new EcsService( name, { - ...args, + ...ecsArgs, port, image: 'mongo:jammy@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', @@ -39,10 +55,66 @@ export class Mongo extends pulumi.ComponentResource { persistentStorageVolumePath: '/data/db', dockerCommand: ['mongod', '--port', port.toString()], assignPublicIp: false, + secrets: this.databaseSecrets, }, { ...opts, parent: this }, ); this.registerOutputs(); } + + private createDatabaseSecrets({ + username, + password, + }: Pick): aws.ecs.Secret[] { + const project = pulumi.getProject(); + const stack = pulumi.getStack(); + + const usernameSecret = new aws.secretsmanager.Secret( + `${this.name}-username-secret`, + { + namePrefix: `${stack}/${project}/DatabaseUsername-`, + tags: commonTags, + }, + { parent: this }, + ); + + const usernameSecretValue = new aws.secretsmanager.SecretVersion( + `${this.name}-username-secret-value`, + { + secretId: usernameSecret.id, + secretString: username, + }, + { parent: this, dependsOn: [usernameSecret] }, + ); + + const passwordSecret = new aws.secretsmanager.Secret( + `${this.name}-password-secret`, + { + namePrefix: `${stack}/${project}/DatabasePassword-`, + tags: commonTags, + }, + { parent: this }, + ); + + const passwordSecretValue = new aws.secretsmanager.SecretVersion( + `${this.name}-password-secret-value`, + { + secretId: passwordSecret.id, + secretString: password, + }, + { parent: this, dependsOn: [passwordSecret] }, + ); + + return [ + { + name: 'MONGO_INITDB_ROOT_USERNAME', + valueFrom: usernameSecretValue.arn, + }, + { + name: 'MONGO_INITDB_ROOT_PASSWORD', + valueFrom: passwordSecretValue.arn, + }, + ]; + } } diff --git a/src/components/project.ts b/src/components/project.ts index 2dad54b..a4aa2d9 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -5,7 +5,7 @@ import * as upstash from '@upstash/pulumi'; import { Database, DatabaseArgs } from './database'; import { WebServer, WebServerArgs } from './web-server'; import { Mongo, MongoArgs } from './mongo'; -import { Redis, RedisArgs, RedisOptions } from './redis'; +import { Redis, RedisArgs } from './redis'; import { StaticSite, StaticSiteArgs } from './static-site'; import { Ec2SSMConnect } from './ec2-ssm-connect'; import { commonTags } from '../constants'; @@ -50,8 +50,6 @@ export type WebServerServiceOptions = { export type MongoServiceOptions = { type: 'MONGO'; - environment?: aws.ecs.KeyValuePair[]; - secrets?: aws.ecs.Secret[]; } & ServiceArgs & Omit; @@ -255,7 +253,7 @@ export class Project extends pulumi.ComponentResource { private createMongoService(options: MongoServiceOptions) { if (!this.cluster) throw new MissingEcsCluster(); - const { serviceName, environment, secrets, ...ecsOptions } = options; + const { serviceName, ...ecsOptions } = options; const service = new Mongo( serviceName, @@ -263,8 +261,6 @@ export class Project extends pulumi.ComponentResource { ...ecsOptions, cluster: this.cluster, vpc: this.vpc, - environment: environment, - secrets: secrets, }, { parent: this }, ); From febe757a3c7c2db0b48496549d915a7e5028998e Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 16 Nov 2023 14:40:03 +0100 Subject: [PATCH 56/58] Renamed secrets --- src/components/mongo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/mongo.ts b/src/components/mongo.ts index e30076b..8482a7c 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -73,7 +73,7 @@ export class Mongo extends pulumi.ComponentResource { const usernameSecret = new aws.secretsmanager.Secret( `${this.name}-username-secret`, { - namePrefix: `${stack}/${project}/DatabaseUsername-`, + namePrefix: `${stack}/${project}/MongoUsername-`, tags: commonTags, }, { parent: this }, @@ -91,7 +91,7 @@ export class Mongo extends pulumi.ComponentResource { const passwordSecret = new aws.secretsmanager.Secret( `${this.name}-password-secret`, { - namePrefix: `${stack}/${project}/DatabasePassword-`, + namePrefix: `${stack}/${project}/MongoPassword-`, tags: commonTags, }, { parent: this }, From e6242261ce26507b718a7fc91a4d3fe831a7cf16 Mon Sep 17 00:00:00 2001 From: Ivo Kovacevic Date: Thu, 16 Nov 2023 15:58:24 +0100 Subject: [PATCH 57/58] Refactor services --- README.md | 36 ++++---- src/components/ecs-service.ts | 48 ++++++----- src/components/mongo.ts | 64 +++++--------- src/components/project.ts | 4 +- src/components/web-server.ts | 156 +++++++++++++++++----------------- 5 files changed, 147 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index fbfcaf0..ed23552 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,11 @@ export type WebServerService = { | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); desiredCount?: pulumi.Input; - minCount?: pulumi.Input; - maxCount?: pulumi.Input; + autoscaling?: pulumi.Input<{ + enabled: pulumi.Input; + minCount?: pulumi.Input; + maxCount?: pulumi.Input; + }>; size?: pulumi.Input; healthCheckPath?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< @@ -186,9 +189,7 @@ type EcsService = { serviceName: string; image: pulumi.Input; port: pulumi.Input; - domain: pulumi.Input; enableServiceAutoDiscovery: pulumi.Input; - enableAutoScaling?: pulumi.Input; lbTargetGroupArn?: aws.lb.TargetGroup['arn']; persistentStorageVolumePath?: pulumi.Input; securityGroup?: aws.ec2.SecurityGroup; @@ -199,8 +200,11 @@ type EcsService = { | ((services: Services) => aws.ecs.KeyValuePair[]); secrets?: aws.ecs.Secret[] | ((services: Services) => aws.ecs.Secret[]); desiredCount?: pulumi.Input; - minCount?: pulumi.Input; - maxCount?: pulumi.Input; + autoscaling?: pulumi.Input<{ + enabled: pulumi.Input; + minCount?: pulumi.Input; + maxCount?: pulumi.Input; + }>; size?: pulumi.Input; healthCheckPath?: pulumi.Input; taskExecutionRoleInlinePolicies?: pulumi.Input< @@ -445,8 +449,11 @@ export type WebServerArgs = { hostedZoneId: pulumi.Input; vpc: awsx.ec2.Vpc; desiredCount?: pulumi.Input; - minCount?: pulumi.Input; - maxCount?: pulumi.Input; + autoscaling?: pulumi.Input<{ + enabled: pulumi.Input; + minCount?: pulumi.Input; + maxCount?: pulumi.Input; + }>; size?: pulumi.Input; environment?: aws.ecs.KeyValuePair[]; secrets?: aws.ecs.Secret[]; @@ -507,9 +514,7 @@ Features: - memory and CPU autoscaling - service auto discovery - persistent storage -- supports load balancing -- reates CloudWatch log group -- public IP address +- CloudWatch logs - comes with predefined cpu and memory options: `small`, `medium`, `large`, `xlarge`
@@ -531,16 +536,17 @@ export type EcsServiceArgs = { cluster: aws.ecs.Cluster; vpc: awsx.ec2.Vpc; desiredCount?: pulumi.Input; - minCount?: pulumi.Input; - maxCount?: pulumi.Input; + autoscaling?: pulumi.Input<{ + enabled: pulumi.Input; + minCount?: pulumi.Input; + maxCount?: pulumi.Input; + }>; size?: pulumi.Input; - healthCheckPath?: pulumi.Input; environment?: aws.ecs.KeyValuePair[]; secrets?: aws.ecs.Secret[]; enableServiceAutoDiscovery: pulumi.Input; persistentStorageVolumePath?: pulumi.Input; dockerCommand?: pulumi.Input; - enableAutoScaling?: pulumi.Input; lbTargetGroupArn?: aws.lb.TargetGroup['arn']; securityGroup?: aws.ec2.SecurityGroup; assignPublicIp?: pulumi.Input; diff --git a/src/components/ecs-service.ts b/src/components/ecs-service.ts index 9e3b29d..9154d0c 100644 --- a/src/components/ecs-service.ts +++ b/src/components/ecs-service.ts @@ -54,14 +54,6 @@ export type EcsServiceArgs = { * Number of instances of the task definition to place and keep running. Defaults to 1. */ desiredCount?: pulumi.Input; - /** - * Min capacity of the scalable target. Defaults to 1. - */ - minCount?: pulumi.Input; - /** - * Max capacity of the scalable target. Defaults to 10. - */ - maxCount?: pulumi.Input; /** * CPU and memory size used for running the container. Defaults to "small". * Available predefined options are: @@ -71,10 +63,7 @@ export type EcsServiceArgs = { * - xlarge (2 vCPU, 4 GB memory) */ size?: pulumi.Input; - /** - * Path for the healthh check request. Defaults to "/healthcheck". - */ - healthCheckPath?: pulumi.Input; + /** * The environment variables to pass to a container. Don't use this field for * sensitive information such as passwords, API keys, etc. For that purpose, @@ -100,10 +89,22 @@ export type EcsServiceArgs = { */ dockerCommand?: pulumi.Input; /** - * Enable auto scaling for service. - * Defaults to false + * Autoscaling options for ecs service. */ - enableAutoScaling?: pulumi.Input; + autoscaling?: pulumi.Input<{ + /** + * Is autoscaling enabled or disabled. Defaults to false. + */ + enabled: pulumi.Input; + /** + * Min capacity of the scalable target. Defaults to 1. + */ + minCount?: pulumi.Input; + /** + * Max capacity of the scalable target. Defaults to 1. + */ + maxCount?: pulumi.Input; + }>; lbTargetGroupArn?: aws.lb.TargetGroup['arn']; /** * Custom service security group @@ -126,10 +127,8 @@ export type EcsServiceArgs = { }>; }; -export const defaults = { +const defaults = { desiredCount: 1, - minCount: 1, - maxCount: 1, size: 'small', environment: [], secrets: [], @@ -137,8 +136,11 @@ export const defaults = { assignPublicIp: false, taskExecutionRoleInlinePolicies: [], taskRoleInlinePolicies: [], - healthCheckPath: '/healthcheck', - enableAutoScaling: false, + autoscaling: { + enabled: false, + minCount: 1, + maxCount: 1, + }, }; export class EcsService extends pulumi.ComponentResource { @@ -165,7 +167,7 @@ export class EcsService extends pulumi.ComponentResource { ); } this.service = this.createEcsService(args, opts); - if (argsWithDefaults.enableAutoScaling) { + if (argsWithDefaults.autoscaling.enabled) { this.enableAutoscaling(args); } @@ -533,8 +535,8 @@ export class EcsService extends pulumi.ComponentResource { const autoscalingTarget = new aws.appautoscaling.Target( `${this.name}-autoscale-target`, { - minCapacity: argsWithDefaults.minCount, - maxCapacity: argsWithDefaults.maxCount, + minCapacity: argsWithDefaults.autoscaling.minCount, + maxCapacity: argsWithDefaults.autoscaling.maxCount, resourceId: pulumi.interpolate`service/${argsWithDefaults.cluster.name}/${this.service.name}`, serviceNamespace: 'ecs', scalableDimension: 'ecs:service:DesiredCount', diff --git a/src/components/mongo.ts b/src/components/mongo.ts index 8482a7c..1967d42 100644 --- a/src/components/mongo.ts +++ b/src/components/mongo.ts @@ -7,10 +7,6 @@ export type MongoArgs = Pick< EcsServiceArgs, 'size' | 'cluster' | 'vpc' | 'tags' > & { - /** - * Exposed service port. - */ - port?: pulumi.Input; /** * Username for the master DB user. */ @@ -20,12 +16,16 @@ export type MongoArgs = Pick< * The value will be stored as a secret in AWS Secret Manager. */ password: pulumi.Input; + /** + * Exposed service port. Defaults to 27017. + */ + port?: pulumi.Input; }; export class Mongo extends pulumi.ComponentResource { name: string; service: EcsService; - databaseSecrets: aws.ecs.Secret[]; + passwordSecret: aws.secretsmanager.Secret; constructor( name: string, @@ -39,7 +39,7 @@ export class Mongo extends pulumi.ComponentResource { const { username, password, ...ecsArgs } = args; this.name = name; - this.databaseSecrets = this.createDatabaseSecrets({ username, password }); + this.passwordSecret = this.createPasswordSecret(password); this.service = new EcsService( name, @@ -47,15 +47,25 @@ export class Mongo extends pulumi.ComponentResource { ...ecsArgs, port, image: - 'mongo:jammy@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', + 'mongo:7.0.3@sha256:238b1636bdd7820c752b91bec8a669f92568eb313ad89a1fc4a92903c1b40489', desiredCount: 1, - minCount: 1, - maxCount: 1, + autoscaling: { enabled: false }, enableServiceAutoDiscovery: true, persistentStorageVolumePath: '/data/db', dockerCommand: ['mongod', '--port', port.toString()], assignPublicIp: false, - secrets: this.databaseSecrets, + environment: [ + { + name: 'MONGO_INITDB_ROOT_USERNAME', + value: username, + }, + ], + secrets: [ + { + name: 'MONGO_INITDB_ROOT_PASSWORD', + valueFrom: this.passwordSecret.arn, + }, + ], }, { ...opts, parent: this }, ); @@ -63,31 +73,10 @@ export class Mongo extends pulumi.ComponentResource { this.registerOutputs(); } - private createDatabaseSecrets({ - username, - password, - }: Pick): aws.ecs.Secret[] { + private createPasswordSecret(password: MongoArgs['password']) { const project = pulumi.getProject(); const stack = pulumi.getStack(); - const usernameSecret = new aws.secretsmanager.Secret( - `${this.name}-username-secret`, - { - namePrefix: `${stack}/${project}/MongoUsername-`, - tags: commonTags, - }, - { parent: this }, - ); - - const usernameSecretValue = new aws.secretsmanager.SecretVersion( - `${this.name}-username-secret-value`, - { - secretId: usernameSecret.id, - secretString: username, - }, - { parent: this, dependsOn: [usernameSecret] }, - ); - const passwordSecret = new aws.secretsmanager.Secret( `${this.name}-password-secret`, { @@ -106,15 +95,6 @@ export class Mongo extends pulumi.ComponentResource { { parent: this, dependsOn: [passwordSecret] }, ); - return [ - { - name: 'MONGO_INITDB_ROOT_USERNAME', - valueFrom: usernameSecretValue.arn, - }, - { - name: 'MONGO_INITDB_ROOT_PASSWORD', - valueFrom: passwordSecretValue.arn, - }, - ]; + return passwordSecret; } } diff --git a/src/components/project.ts b/src/components/project.ts index a4aa2d9..5a5bd84 100644 --- a/src/components/project.ts +++ b/src/components/project.ts @@ -253,12 +253,12 @@ export class Project extends pulumi.ComponentResource { private createMongoService(options: MongoServiceOptions) { if (!this.cluster) throw new MissingEcsCluster(); - const { serviceName, ...ecsOptions } = options; + const { serviceName, ...mongoOptions } = options; const service = new Mongo( serviceName, { - ...ecsOptions, + ...mongoOptions, cluster: this.cluster, vpc: this.vpc, }, diff --git a/src/components/web-server.ts b/src/components/web-server.ts index 55cfef9..728e3b5 100644 --- a/src/components/web-server.ts +++ b/src/components/web-server.ts @@ -2,17 +2,22 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; import { commonTags } from '../constants'; import { AcmCertificate } from './acm-certificate'; -import { EcsService, EcsServiceArgs, defaults } from './ecs-service'; +import { EcsService, EcsServiceArgs } from './ecs-service'; -export type WebServerArgs = Omit< +export type WebServerArgs = Pick< EcsServiceArgs, - | 'enableServiceAutoDiscovery' - | 'persistentStorageVolumePath' - | 'dockerCommand' - | 'enableAutoScaling' - | 'lbTargetGroupArn' - | 'securityGroup' - | 'assignPublicIp' + | 'image' + | 'port' + | 'cluster' + | 'vpc' + | 'desiredCount' + | 'autoscaling' + | 'size' + | 'environment' + | 'secrets' + | 'taskExecutionRoleInlinePolicies' + | 'taskRoleInlinePolicies' + | 'tags' > & { /** * The domain which will be used to access the service. @@ -23,6 +28,14 @@ export type WebServerArgs = Omit< * The ID of the hosted zone. */ hostedZoneId: pulumi.Input; + /** + * Path for the health check request. Defaults to "/healthcheck". + */ + healthCheckPath?: pulumi.Input; +}; + +const defaults = { + healthCheckPath: '/healthcheck', }; export class WebServer extends pulumi.ComponentResource { @@ -30,6 +43,7 @@ export class WebServer extends pulumi.ComponentResource { service: EcsService; certificate: AcmCertificate; lbSecurityGroup: aws.ec2.SecurityGroup; + serviceSecurityGroup: aws.ec2.SecurityGroup; lb: aws.lb.LoadBalancer; lbTargetGroup: aws.lb.TargetGroup; lbHttpListener: aws.lb.Listener; @@ -42,24 +56,7 @@ export class WebServer extends pulumi.ComponentResource { ) { super('studion:WebServer', name, args, opts); - const { - image, - port, - size, - cluster, - vpc, - environment, - secrets, - healthCheckPath, - domain, - hostedZoneId, - desiredCount, - minCount, - maxCount, - taskExecutionRoleInlinePolicies, - taskRoleInlinePolicies, - tags, - } = args; + const { vpc, port, healthCheckPath, domain, hostedZoneId } = args; this.name = name; this.certificate = this.createTlsCertificate({ domain, hostedZoneId }); @@ -75,58 +72,8 @@ export class WebServer extends pulumi.ComponentResource { this.lbHttpListener = lbHttpListener; this.lbTlsListener = lbTlsListener; this.lbSecurityGroup = lbSecurityGroup; - - const securityGroup = new aws.ec2.SecurityGroup( - `${name}-security-group`, - { - vpcId: vpc.vpcId, - ingress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - securityGroups: [this.lbSecurityGroup.id], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 0, - protocol: '-1', - cidrBlocks: ['0.0.0.0/0'], - }, - ], - tags: commonTags, - }, - { parent: this }, - ); - - this.service = new EcsService( - name, - { - image, - port, - cluster, - ...(desiredCount && { desiredCount }), - ...(minCount && { minCount }), - ...(maxCount && { maxCount }), - ...(size && { size }), - environment, - secrets, - enableServiceAutoDiscovery: false, - enableAutoScaling: true, - lbTargetGroupArn: lbTargetGroup.arn, - assignPublicIp: true, - vpc, - securityGroup, - ...(taskExecutionRoleInlinePolicies && { - taskExecutionRoleInlinePolicies, - }), - ...(taskRoleInlinePolicies && { taskRoleInlinePolicies }), - ...(tags && { tags }), - }, - { ...opts, parent: this }, - ); + this.serviceSecurityGroup = this.createSecurityGroup({ vpc }); + this.service = this.createEcsService(args); this.createDnsRecord({ domain, hostedZoneId }); @@ -266,6 +213,57 @@ export class WebServer extends pulumi.ComponentResource { }; } + private createSecurityGroup({ vpc }: Pick) { + const securityGroup = new aws.ec2.SecurityGroup( + `${this.name}-security-group`, + { + vpcId: vpc.vpcId, + ingress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + securityGroups: [this.lbSecurityGroup.id], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: '-1', + cidrBlocks: ['0.0.0.0/0'], + }, + ], + tags: commonTags, + }, + { parent: this }, + ); + return securityGroup; + } + + private createEcsService(args: WebServerArgs) { + const service = new EcsService( + this.name, + { + ...args, + enableServiceAutoDiscovery: false, + lbTargetGroupArn: this.lbTargetGroup.arn, + assignPublicIp: true, + securityGroup: this.serviceSecurityGroup, + }, + { + parent: this, + dependsOn: [ + this.lb, + this.lbTargetGroup, + this.lbHttpListener, + this.lbTlsListener, + ], + }, + ); + return service; + } + private createDnsRecord({ domain, hostedZoneId, From d6e9613fdea9ad586ebdb92155ea99db0eb8fd76 Mon Sep 17 00:00:00 2001 From: Ivo Kovacevic Date: Thu, 16 Nov 2023 16:11:38 +0100 Subject: [PATCH 58/58] Remove mongo service from todos --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ed23552..f25be40 100644 --- a/README.md +++ b/README.md @@ -661,5 +661,4 @@ const project = new studion.Project('demo-project', { ## 🚧 TODO - [ ] Add worker service for executing tasks -- [ ] Add MongoDB service - [ ] Enable RDS password rotation