diff --git a/examples/ad-hoc/base/index.ts b/examples/ad-hoc/base/index.ts index 86d536e..d1da61a 100644 --- a/examples/ad-hoc/base/index.ts +++ b/examples/ad-hoc/base/index.ts @@ -7,7 +7,7 @@ const adHocBaseEnv = new AdHocBaseEnvComponent('myAdHocEnv', { }); export const vpcId = adHocBaseEnv.vpc.vpcId; -export const assetsBucket = adHocBaseEnv.assetsBucket.id; +export const assetsBucketName = adHocBaseEnv.assetsBucket.id; export const privateSubnetIds = adHocBaseEnv.vpc.privateSubnetIds; export const appSgId = adHocBaseEnv.appSecurityGroup.id; export const albSgId = adHocBaseEnv.albSecurityGroup.id; diff --git a/src/components/ad-hoc/app/index.ts b/src/components/ad-hoc/app/index.ts index fde7cfc..2124d4f 100644 --- a/src/components/ad-hoc/app/index.ts +++ b/src/components/ad-hoc/app/index.ts @@ -222,7 +222,7 @@ export class AdHocAppComponent extends pulumi.ComponentResource { taskRoleArn: iamResources.ecsTaskRole.arn, }, { // this ensures that the priority of the listener rule for the api service is higher than the frontend service - dependsOn: [apiService.listenerRule] + dependsOn: [apiService] }); // Celery Default Worker diff --git a/src/components/ad-hoc/base/index.ts b/src/components/ad-hoc/base/index.ts index 1d3ead5..753d41c 100644 --- a/src/components/ad-hoc/base/index.ts +++ b/src/components/ad-hoc/base/index.ts @@ -1,9 +1,11 @@ import * as aws from "@pulumi/aws" import * as awsx from "@pulumi/awsx"; import * as pulumi from "@pulumi/pulumi"; -import { RdsResources } from '../../internal/rds'; +import { AlbResources } from "../../internal/alb"; import { BastionHostResources } from '../../internal/bastion'; +import { RdsResources } from '../../internal/rds'; import { registerAutoTags } from "../../../util"; +import { SecurityGroupResources } from "../../internal/sg"; // automatically tag all resources registerAutoTags({ @@ -47,7 +49,6 @@ export class AdHocBaseEnvComponent extends pulumi.ComponentResource { const stackName = pulumi.getStack(); this.stackName = stackName; - this.domainName = props.domainName; const vpc = new awsx.ec2.Vpc(stackName, { @@ -55,149 +56,56 @@ export class AdHocBaseEnvComponent extends pulumi.ComponentResource { numberOfAvailabilityZones: 2, enableDnsHostnames: true, enableDnsSupport: true - }); + }, { parent: this }); this.vpc = vpc; const assetsBucket = new aws.s3.Bucket("assetsBucket", { - bucket: `${props.domainName.replace(".", "-")}-${stackName}-assets-bucket` - }); + bucket: `${props.domainName.replace(".", "-")}-${stackName}-assets-bucket`, + forceDestroy: true + }, { parent: this }); this.assetsBucket = assetsBucket; - const albSecurityGroup = new aws.ec2.SecurityGroup('AlbSecurityGroup', { - description: "Allow traffic from ALB", - vpcId: vpc.vpcId, - ingress: [{ - description: "Port 80 Traffic", - fromPort: 80, - toPort: 80, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }, { - description: "Port 443 Traffic", - fromPort: 443, - toPort: 443, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }], - egress: [{ - fromPort: 0, - toPort: 0, - protocol: "-1", - cidrBlocks: ["0.0.0.0/0"], - }], - }); - this.albSecurityGroup = albSecurityGroup; - - const appSecurityGroup = new aws.ec2.SecurityGroup('AppSecurityGroup', { - description: "Allow traffic from ALB SG to apps", - vpcId: vpc.vpcId, - ingress: [{ - description: "Port 80 Traffic", - fromPort: 0, - toPort: 0, - protocol: "-1", - securityGroups: [albSecurityGroup.id], - },{ - description: "Allow traffic from this SG", - fromPort: 0, - toPort: 0, - protocol: "-1", - self: true, - }], - egress: [{ - description: "Allow all outbound traffic", - fromPort: 0, - toPort: 0, - protocol: "-1", - cidrBlocks: ["0.0.0.0/0"], - },{ - description: "Allow all outbound to this SG", - fromPort: 0, - toPort: 0, - protocol: "-1", - self: true - }], - }); - this.appSecurityGroup = appSecurityGroup; - - const loadBalancer = new aws.alb.LoadBalancer("LoadBalancer", { - internal: false, - loadBalancerType: "application", - securityGroups: [albSecurityGroup.id], - subnets: vpc.publicSubnetIds, - }); - this.alb = loadBalancer; - - new aws.alb.TargetGroup("DefaultTg", { - vpcId: vpc.vpc.id, - port: 80, - targetType: "instance", - protocol: "HTTP", - healthCheck: { - protocol: "HTTP", - interval: 300, - path: "/api/health-check/", - timeout: 120, - healthyThreshold: 2, - unhealthyThreshold: 3, - port: '80' - } - }); - - new aws.alb.Listener("HttpListener", { - loadBalancerArn: loadBalancer.arn, - port: 80, - protocol: "HTTP", - defaultActions: [{ - type: "redirect", - redirect: { - port: "443", - protocol: "HTTPS", - statusCode: "HTTP_301", - }, - }] - }); + const securityGroupResources = new SecurityGroupResources("SecurityGroupResources", { + vpcId: vpc.vpcId + }, { parent: this }); + this.appSecurityGroup = securityGroupResources.appSecurityGroup; + this.albSecurityGroup = securityGroupResources.albSecurityGroup; - const httpsListener = new aws.alb.Listener("HttpsListener", { - loadBalancerArn: loadBalancer.arn, - port: 443, - protocol: "HTTPS", + // ALB resources (Load Balancer, Default Target Group, HTTP and HTTPS Listener) + const loadBalancerResources = new AlbResources("AlbResources", { + albSgId: securityGroupResources.albSecurityGroup.id, certificateArn: props.certificateArn, - defaultActions: [{ - type: "fixed-response", - fixedResponse: { - contentType: "text/plain", - messageBody: "Fixed response content", - statusCode: "200", - }, - }], - }); - this.listener = httpsListener; - - const serviceDiscoveryPrivateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace( - "PrivateDnsNamespace", { - description: "private dns namespace for ad hoc environment", - vpc: vpc.vpc.id, - name: `${stackName}-sd-ns` - } - ); - this.serviceDiscoveryNamespace = serviceDiscoveryPrivateDnsNamespace; + publicSubnetIds: vpc.publicSubnetIds, + vpcId: vpc.vpcId, + }, { parent: this }); + this.alb = loadBalancerResources.alb; + this.listener = loadBalancerResources.listener; + + // CloudMap service discovery is only needed in ad hoc environments + // It is needed to support running redis in our ECS cluster + const sdNameSpace = new aws.servicediscovery.PrivateDnsNamespace("PrivateDnsNamespace", { + description: "private dns namespace for ad hoc environment", + vpc: vpc.vpcId, + name: `${stackName}-sd-ns` + }, { parent: this }); + this.serviceDiscoveryNamespace = sdNameSpace; // RDS const rdsResources = new RdsResources("RdsResources", { - appSecurityGroup: appSecurityGroup, + appSgId: securityGroupResources.appSecurityGroup.id, dbSecretName: "DB_SECRET_NAME", port: 5432, - vpc: vpc - }); + vpcId: vpc.vpcId, + privateSubnetIds: vpc.privateSubnetIds + }, { parent: this }); this.databaseInstance = rdsResources.databaseInstance; // BastionHost const bastionHost = new BastionHostResources("BastionHostResources", { - appSgId: appSecurityGroup.id, + appSgId: securityGroupResources.appSecurityGroup.id, rdsAddress: rdsResources.databaseInstance.address, privateSubnet: vpc.privateSubnetIds[0] - }); + }, { parent: this }); this.bastionHostInstanceId = bastionHost.instanceId; } } diff --git a/src/components/internal/alb/index.ts b/src/components/internal/alb/index.ts new file mode 100644 index 0000000..15e475b --- /dev/null +++ b/src/components/internal/alb/index.ts @@ -0,0 +1,79 @@ +import * as aws from "@pulumi/aws" +import * as pulumi from "@pulumi/pulumi"; + +interface AlbResourcesProps { + vpcId: pulumi.Output; + publicSubnetIds: pulumi.Output; + albSgId: pulumi.Output; + certificateArn: string; +} + +export class AlbResources extends pulumi.ComponentResource { + public readonly listener: aws.alb.Listener; + public readonly alb: aws.alb.LoadBalancer; + + /** + * Creates a new static website hosted on AWS. + * @param name The _unique_ name of the resource. + * @param props Props to pass to AdHocBaseEnv component + * @param opts A bag of options that control this resource's behavior. + */ + constructor(name: string, props: AlbResourcesProps, opts?: pulumi.ResourceOptions) { + super(`pulumi-contrib:components:AlbResources`, name, props, opts); + + const loadBalancer = new aws.alb.LoadBalancer("LoadBalancer", { + internal: false, + loadBalancerType: "application", + securityGroups: [props.albSgId], + subnets: props.publicSubnetIds, + }, { parent: this }); + this.alb = loadBalancer; + + new aws.alb.TargetGroup("DefaultTg", { + vpcId: props.vpcId, + port: 80, + targetType: "instance", + protocol: "HTTP", + healthCheck: { + protocol: "HTTP", + interval: 300, + path: "/api/health-check/", + timeout: 120, + healthyThreshold: 2, + unhealthyThreshold: 3, + port: '80' + } + }, { parent: this }); + + new aws.alb.Listener("HttpListener", { + loadBalancerArn: loadBalancer.arn, + port: 80, + protocol: "HTTP", + defaultActions: [{ + type: "redirect", + redirect: { + port: "443", + protocol: "HTTPS", + statusCode: "HTTP_301", + }, + }] + }, { parent: this }); + + const httpsListener = new aws.alb.Listener("HttpsListener", { + loadBalancerArn: loadBalancer.arn, + port: 443, + protocol: "HTTPS", + certificateArn: props.certificateArn, + defaultActions: [{ + type: "fixed-response", + fixedResponse: { + contentType: "text/plain", + messageBody: "Fixed response content", + statusCode: "200", + }, + }], + }, { parent: this }); + this.listener = httpsListener; + } +} + diff --git a/src/components/internal/bastion/index.ts b/src/components/internal/bastion/index.ts index f91e252..dca50ab 100644 --- a/src/components/internal/bastion/index.ts +++ b/src/components/internal/bastion/index.ts @@ -52,7 +52,7 @@ export class BastionHostResources extends pulumi.ComponentResource { }, }], }), - }); + }, { parent: this }); // policy for BastionHostRole const policy = new aws.iam.RolePolicy("BastionHostPolicy", { @@ -70,13 +70,13 @@ export class BastionHostResources extends pulumi.ComponentResource { Resource: "*", }], }), - }); + }, { parent: this }); // instance profile const instanceProfile = new aws.iam.InstanceProfile("BastionHostInstanceProfile", { role: bastionHostRole.name, name: `${stackName}BastionInstanceProfile` - }); + }, { parent: this }); // bastion host user data string const bastionHostUserData = ` @@ -123,7 +123,7 @@ runcmd: vpcSecurityGroupIds: [props.appSgId], subnetId: props.privateSubnet, userData: bastionHostUserData, - }); + }, { parent: this }); this.instanceId = instance.id; } } \ No newline at end of file diff --git a/src/components/internal/ecs/web/index.ts b/src/components/internal/ecs/web/index.ts index 6ed50d4..2d21c7e 100644 --- a/src/components/internal/ecs/web/index.ts +++ b/src/components/internal/ecs/web/index.ts @@ -112,7 +112,7 @@ export class WebEcsService extends pulumi.ComponentResource { tags: { Name: `${stackName}-${props.name}-tg` } - }); + }, { parent: this }); // aws ecs service const ecsService = new aws.ecs.Service(`${props.name}WebService`, { @@ -141,6 +141,7 @@ export class WebEcsService extends pulumi.ComponentResource { subnets: props.privateSubnets } }, { + parent: this, ignoreChanges: ['taskDefinition', 'desiredCount'], dependsOn: [targetGroup] }); @@ -163,7 +164,7 @@ export class WebEcsService extends pulumi.ComponentResource { } } ] - }); + }, { parent: this}); this.listenerRule = listenerRule; } } diff --git a/src/components/internal/rds/index.ts b/src/components/internal/rds/index.ts index 2401fdf..ddd34e9 100644 --- a/src/components/internal/rds/index.ts +++ b/src/components/internal/rds/index.ts @@ -1,10 +1,10 @@ import * as aws from "@pulumi/aws" -import * as awsx from "@pulumi/awsx"; import * as pulumi from "@pulumi/pulumi"; interface RdsResourcesProps { - vpc: awsx.ec2.Vpc; - appSecurityGroup: aws.ec2.SecurityGroup; + vpcId: pulumi.Output; + privateSubnetIds: pulumi.Output; + appSgId: pulumi.Output; dbSecretName: string; port: number; } @@ -24,13 +24,13 @@ export class RdsResources extends pulumi.ComponentResource { // rds security group const rdsSecurityGroup = new aws.ec2.SecurityGroup('RdsSecurityGroup', { description: "Allow traffic from app sg to RDS", - vpcId: props.vpc.vpc.id, + vpcId: props.vpcId, ingress: [{ description: "allow traffic from app sg", fromPort: props.port, toPort: props.port, protocol: "tcp", - securityGroups: [props.appSecurityGroup.id], + securityGroups: [props.appSgId], }], egress: [{ fromPort: 0, @@ -38,7 +38,7 @@ export class RdsResources extends pulumi.ComponentResource { protocol: "-1", cidrBlocks: ["0.0.0.0/0"], }], - }); + }, { parent: this }); // secret? // TODO: add this later with random password @@ -46,9 +46,9 @@ export class RdsResources extends pulumi.ComponentResource { // subnet group const dbSubnetGroup = new aws.rds.SubnetGroup("DbSubnetGroup", { - subnetIds: props.vpc.privateSubnetIds, + subnetIds: props.privateSubnetIds, name: `${stackName}-db-subnet-group` - }); + }, { parent: this }); // instance const dbInstance = new aws.rds.Instance("DbInstance", { @@ -69,7 +69,7 @@ export class RdsResources extends pulumi.ComponentResource { backupRetentionPeriod: 7, dbSubnetGroupName: dbSubnetGroup.name, dbName: "postgres" - }); + }, { parent: this }); this.databaseInstance = dbInstance; } } \ No newline at end of file diff --git a/src/components/internal/sg/index.ts b/src/components/internal/sg/index.ts new file mode 100644 index 0000000..b8d2c46 --- /dev/null +++ b/src/components/internal/sg/index.ts @@ -0,0 +1,77 @@ +import * as aws from "@pulumi/aws" +import * as pulumi from "@pulumi/pulumi"; + +interface SecurityGroupResourcesProps { + vpcId: pulumi.Output; +} + +export class SecurityGroupResources extends pulumi.ComponentResource { + public readonly albSecurityGroup: aws.ec2.SecurityGroup; + public readonly appSecurityGroup: aws.ec2.SecurityGroup; + /** + * Creates ALB and Application Security Groups + * @param name The _unique_ name of the resource. + * @param props Props to pass to AdHocBaseEnv component + * @param opts A bag of options that control this resource's behavior. + */ + constructor(name: string, props: SecurityGroupResourcesProps, opts?: pulumi.ResourceOptions) { + super(`pulumi-contrib:components:SecurityGroupResources`, name, props, opts); + + const albSecurityGroup = new aws.ec2.SecurityGroup('AlbSecurityGroup', { + description: "Allow traffic from ALB", + vpcId: props.vpcId, + ingress: [{ + description: "Port 80 Traffic", + fromPort: 80, + toPort: 80, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }, { + description: "Port 443 Traffic", + fromPort: 443, + toPort: 443, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }], + egress: [{ + fromPort: 0, + toPort: 0, + protocol: "-1", + cidrBlocks: ["0.0.0.0/0"], + }], + }, { parent: this }); + this.albSecurityGroup = albSecurityGroup; + + const appSecurityGroup = new aws.ec2.SecurityGroup('AppSecurityGroup', { + description: "Allow traffic from ALB SG to apps", + vpcId: props.vpcId, + ingress: [{ + description: "Port 80 Traffic", + fromPort: 0, + toPort: 0, + protocol: "-1", + securityGroups: [albSecurityGroup.id], + },{ + description: "Allow traffic from this SG", + fromPort: 0, + toPort: 0, + protocol: "-1", + self: true, + }], + egress: [{ + description: "Allow all outbound traffic", + fromPort: 0, + toPort: 0, + protocol: "-1", + cidrBlocks: ["0.0.0.0/0"], + },{ + description: "Allow all outbound to this SG", + fromPort: 0, + toPort: 0, + protocol: "-1", + self: true + }], + }, { parent: this }); + this.appSecurityGroup = appSecurityGroup; + } +}