diff --git a/src/v2/components/web-server/builder.ts b/src/v2/components/web-server/builder.ts index 9fea21d3..5a7bbf9b 100644 --- a/src/v2/components/web-server/builder.ts +++ b/src/v2/components/web-server/builder.ts @@ -3,6 +3,7 @@ import * as awsx from '@pulumi/awsx'; import { EcsService } from '../ecs-service'; import { WebServer } from '.'; import { OtelCollector } from '../../otel'; +import { AcmCertificate } from '../acm-certificate'; export namespace WebServerBuilder { export type EcsConfig = Omit; @@ -10,7 +11,6 @@ export namespace WebServerBuilder { export type Args = Omit< WebServer.Args, | 'vpc' - | 'publicSubnetIds' | 'cluster' | 'volumes' | 'domain' @@ -26,7 +26,9 @@ export class WebServerBuilder { private _ecsConfig?: WebServerBuilder.EcsConfig; private _domain?: pulumi.Input; private _hostedZoneId?: pulumi.Input; + private _certificate?: pulumi.Input; private _healthCheckPath?: pulumi.Input; + private _loadBalancingAlgorithmType?: pulumi.Input; private _otelCollector?: pulumi.Input; private _initContainers: pulumi.Input[] = []; private _sidecarContainers: pulumi.Input[] = []; @@ -87,6 +89,18 @@ export class WebServerBuilder { return this; } + public withCertificate( + certificate: WebServerBuilder.Args['certificate'], + hostedZoneId: pulumi.Input, + domain?: pulumi.Input, + ): this { + this._certificate = certificate; + this._hostedZoneId = hostedZoneId; + this._domain = domain; + + return this; + } + public withInitContainer(container: WebServer.InitContainer): this { this._initContainers.push(container); @@ -113,6 +127,12 @@ export class WebServerBuilder { return this; } + public withLoadBalancingAlgorithm(algorithm: pulumi.Input) { + this._loadBalancingAlgorithmType = algorithm; + + return this; + } + public build(opts: pulumi.ComponentResourceOptions = {}): WebServer { if (!this._container) { throw new Error( @@ -137,10 +157,11 @@ export class WebServerBuilder { ...this._container, vpc: this._vpc, volumes: this._volumes, - publicSubnetIds: this._vpc.publicSubnetIds, domain: this._domain, hostedZoneId: this._hostedZoneId, + certificate: this._certificate, healthCheckPath: this._healthCheckPath, + loadBalancingAlgorithmType: this._loadBalancingAlgorithmType, otelCollector: this._otelCollector, initContainers: this._initContainers, sidecarContainers: this._sidecarContainers, diff --git a/src/v2/components/web-server/index.ts b/src/v2/components/web-server/index.ts index 107f34c4..1472ccce 100644 --- a/src/v2/components/web-server/index.ts +++ b/src/v2/components/web-server/index.ts @@ -2,7 +2,7 @@ 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 '../../../components/acm-certificate'; +import { AcmCertificate } from '../acm-certificate'; import { EcsService } from '../ecs-service'; import { WebServerLoadBalancer } from './load-balancer'; import { OtelCollector } from '../../otel'; @@ -38,14 +38,18 @@ export namespace WebServer { export type Args = EcsConfig & Container & { - // TODO: Automatically use subnet IDs from passed `vpc` - publicSubnetIds: 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; hostedZoneId?: pulumi.Input; + /** + * If provided without `domain` argument, Route53 A records will be created for the certificate's + * primary domain and all subject alternative names (SANs). + * If `domain` argument is also provided, only a single A record for that domain will be created. + */ + certificate?: pulumi.Input; /** * Path for the load balancer target group health check request. * @@ -53,6 +57,7 @@ export namespace WebServer { * "/healthcheck" */ healthCheckPath?: pulumi.Input; + loadBalancingAlgorithmType?: pulumi.Input; initContainers?: pulumi.Input[]>; sidecarContainers?: pulumi.Input< pulumi.Input[] @@ -71,8 +76,8 @@ export class WebServer extends pulumi.ComponentResource { initContainers?: pulumi.Output; sidecarContainers?: pulumi.Output; volumes?: pulumi.Output; - certificate?: AcmCertificate; - dnsRecord?: aws.route53.Record; + certificate?: pulumi.Output; + dnsRecords?: pulumi.Output; constructor( name: string, @@ -80,17 +85,21 @@ export class WebServer extends pulumi.ComponentResource { opts: pulumi.ComponentResourceOptions = {}, ) { super('studion:WebServer', name, args, opts); + const { vpc, domain, hostedZoneId, certificate } = args; - const { vpc, domain, hostedZoneId } = args; - - if (domain && !hostedZoneId) { + if ((domain || certificate) && !hostedZoneId) { throw new Error( - 'WebServer:hostedZoneId must be provided when the domain is specified', + 'HostedZoneId must be provided when domain or certificate are provided', ); } + const hasCustomDomain = !!domain && !!hostedZoneId; - if (hasCustomDomain) { - this.certificate = this.createTlsCertificate({ domain, hostedZoneId }); + if (certificate) { + this.certificate = pulumi.output(certificate); + } else if (hasCustomDomain) { + this.certificate = pulumi.output( + this.createTlsCertificate({ domain, hostedZoneId }), + ); } this.name = name; @@ -101,6 +110,7 @@ export class WebServer extends pulumi.ComponentResource { port: args.port, certificate: this.certificate?.certificate, healthCheckPath: args.healthCheckPath, + loadBalancingAlgorithmType: args.loadBalancingAlgorithmType, }, { parent: this }, ); @@ -112,21 +122,21 @@ export class WebServer extends pulumi.ComponentResource { this.ecsConfig = this.createEcsConfig(args); this.volumes = this.getVolumes(args); - // TODO: Move output mapping to createEcsService - this.service = pulumi - .all([this.initContainers, this.sidecarContainers]) - .apply(([initContainers, sidecarContainers]) => { - return this.createEcsService( - this.container, - this.lb, - this.ecsConfig, - this.volumes, - [...initContainers, ...sidecarContainers], - ); - }); + this.service = this.createEcsService( + this.container, + this.lb, + this.ecsConfig, + this.volumes, + this.initContainers, + this.sidecarContainers, + ); - if (hasCustomDomain) { - this.dnsRecord = this.createDnsRecord({ domain, hostedZoneId }); + if (this.certificate) { + this.dnsRecords = this.createDnsRecords( + this.certificate, + hostedZoneId!, + domain, + ); } this.registerOutputs(); @@ -273,64 +283,109 @@ export class WebServer extends pulumi.ComponentResource { lb: WebServerLoadBalancer, ecsConfig: WebServer.EcsConfig, volumes?: pulumi.Output, - containers?: EcsService.Container[], - ): EcsService { - return new EcsService( - `${this.name}-ecs`, - { - ...ecsConfig, - volumes, - containers: [ + initContainers?: pulumi.Output, + sidecarContainers?: pulumi.Output, + ): pulumi.Output { + return pulumi + .all([ + initContainers || pulumi.output([]), + sidecarContainers || pulumi.output([]), + ]) + .apply(([inits, sidecars]) => { + return new EcsService( + `${this.name}-ecs`, { - ...webServerContainer, - name: this.name, - portMappings: [ - EcsService.createTcpPortMapping(webServerContainer.port), + ...ecsConfig, + volumes, + containers: [ + { + ...webServerContainer, + name: this.name, + portMappings: [ + EcsService.createTcpPortMapping(webServerContainer.port), + ], + essential: true, + }, + ...inits, + ...sidecars, + ], + enableServiceAutoDiscovery: false, + loadBalancers: [ + { + containerName: this.name, + containerPort: webServerContainer.port, + targetGroupArn: lb.targetGroup.arn, + }, ], - essential: true, + assignPublicIp: true, + securityGroup: this.serviceSecurityGroup, }, - ...(containers || []), - ], - enableServiceAutoDiscovery: false, - loadBalancers: [ { - containerName: this.name, - containerPort: webServerContainer.port, - targetGroupArn: lb.targetGroup.arn, + parent: this, + dependsOn: [lb, lb.targetGroup], }, - ], - assignPublicIp: true, - securityGroup: this.serviceSecurityGroup, - }, - { - parent: this, - dependsOn: [lb, lb.targetGroup], - }, - ); + ); + }); } - private createDnsRecord({ - domain, - hostedZoneId, - }: Pick< - Required, - 'domain' | 'hostedZoneId' - >): aws.route53.Record { - return new aws.route53.Record( - `${this.name}-route53-record`, - { - type: 'A', - name: domain, - zoneId: hostedZoneId, - aliases: [ - { - name: this.lb.lb.dnsName, - zoneId: this.lb.lb.zoneId, - evaluateTargetHealth: true, - }, - ], - }, - { parent: this }, - ); + private createDnsRecords( + certificate: pulumi.Output, + hostedZoneId: pulumi.Input, + domain?: pulumi.Input, + ): pulumi.Output { + if (domain) { + const record = new aws.route53.Record( + `${this.name}-route53-record`, + { + type: 'A', + name: domain, + zoneId: hostedZoneId, + aliases: [ + { + name: this.lb.lb.dnsName, + zoneId: this.lb.lb.zoneId, + evaluateTargetHealth: true, + }, + ], + }, + { parent: this }, + ); + + return pulumi.output([record]); + } + + const records = pulumi + .all([ + certificate.certificate.domainName, + certificate.certificate.subjectAlternativeNames, + ]) + .apply(([primaryDomain, sans]) => { + const allDomains = [ + primaryDomain, + ...(sans || []).filter(san => san !== primaryDomain), + ]; + + return allDomains.map( + (domain, index) => + new aws.route53.Record( + `${this.name}-route53-record${index === 0 ? '' : `-${index}`}`, + { + type: 'A', + name: domain, + zoneId: hostedZoneId, + aliases: [ + { + name: this.lb.lb.dnsName, + zoneId: this.lb.lb.zoneId, + evaluateTargetHealth: true, + }, + ], + }, + { parent: this }, + ), + ); + }); + + return records; } } diff --git a/src/v2/components/web-server/load-balancer.ts b/src/v2/components/web-server/load-balancer.ts index 2552c3f9..a5369fef 100644 --- a/src/v2/components/web-server/load-balancer.ts +++ b/src/v2/components/web-server/load-balancer.ts @@ -7,8 +7,9 @@ export namespace WebServerLoadBalancer { export type Args = { vpc: pulumi.Input; port: pulumi.Input; - certificate?: aws.acm.Certificate; + certificate?: pulumi.Input; healthCheckPath?: pulumi.Input; + loadBalancingAlgorithmType?: pulumi.Input; }; } @@ -58,7 +59,8 @@ export class WebServerLoadBalancer extends pulumi.ComponentResource { this.name = name; const vpc = pulumi.output(args.vpc); - const { port, certificate, healthCheckPath } = args; + const { port, certificate, healthCheckPath, loadBalancingAlgorithmType } = + args; this.securityGroup = this.createLbSecurityGroup(vpc.vpcId); @@ -80,6 +82,7 @@ export class WebServerLoadBalancer extends pulumi.ComponentResource { port, vpc.vpcId, healthCheckPath, + loadBalancingAlgorithmType, ); this.httpListener = this.createLbHttpListener( this.lb, @@ -88,7 +91,11 @@ export class WebServerLoadBalancer extends pulumi.ComponentResource { ); this.tlsListener = certificate && - this.createLbTlsListener(this.lb, this.targetGroup, certificate); + this.createLbTlsListener( + this.lb, + this.targetGroup, + pulumi.output(certificate), + ); this.registerOutputs(); } @@ -96,7 +103,7 @@ export class WebServerLoadBalancer extends pulumi.ComponentResource { private createLbTlsListener( lb: aws.lb.LoadBalancer, lbTargetGroup: aws.lb.TargetGroup, - certificate: aws.acm.Certificate, + certificate: pulumi.Output, ): aws.lb.Listener { return new aws.lb.Listener( `${this.name}-listener-443`, @@ -104,7 +111,7 @@ export class WebServerLoadBalancer extends pulumi.ComponentResource { loadBalancerArn: lb.arn, port: 443, protocol: 'HTTPS', - sslPolicy: 'ELBSecurityPolicy-2016-08', + sslPolicy: 'ELBSecurityPolicy-TLS13-1-2-2021-06', certificateArn: certificate.arn, defaultActions: [ { @@ -154,6 +161,7 @@ export class WebServerLoadBalancer extends pulumi.ComponentResource { port: pulumi.Input, vpcId: awsx.ec2.Vpc['vpcId'], healthCheckPath: pulumi.Input | undefined, + loadBalancingAlgorithmType?: pulumi.Input, ): aws.lb.TargetGroup { return new aws.lb.TargetGroup( `${this.name}-tg`, @@ -163,6 +171,7 @@ export class WebServerLoadBalancer extends pulumi.ComponentResource { protocol: 'HTTP', targetType: 'ip', vpcId, + loadBalancingAlgorithmType, healthCheck: { healthyThreshold: 3, unhealthyThreshold: 2, diff --git a/tests/build/index.tst.ts b/tests/build/index.tst.ts index 749e9af9..6406fec9 100644 --- a/tests/build/index.tst.ts +++ b/tests/build/index.tst.ts @@ -51,7 +51,6 @@ describe('Build output', () => { cluster: new aws.ecs.Cluster('clusterName'), image: 'sample/image', port: 8080, - publicSubnetIds: ['sub-1', 'sub-2', 'sub-3'], }); }); diff --git a/tests/web-server/domain.test.ts b/tests/web-server/domain.test.ts new file mode 100644 index 00000000..5d0721a3 --- /dev/null +++ b/tests/web-server/domain.test.ts @@ -0,0 +1,165 @@ +import { it } from 'node:test'; +import * as assert from 'node:assert'; +import { WebServerTestContext } from './test-context'; +import { DescribeListenersCommand } from '@aws-sdk/client-elastic-load-balancing-v2'; +import { ListResourceRecordSetsCommand } from '@aws-sdk/client-route-53'; +import { backOff } from 'exponential-backoff'; +import { request } from 'undici'; +import status from 'http-status'; + +export function testWebServerWithDomain(ctx: WebServerTestContext) { + it('should configure HTTPS listener with certificate for web server with custom domain', async () => { + const webServer = ctx.outputs.webServerWithDomain.value; + await assertHttpsListenerWithCertificate(ctx, webServer); + }); + + it('should create single DNS A record for web server with custom domain', async () => { + const webServer = ctx.outputs.webServerWithDomain.value; + const { primary } = ctx.config.webServerWithDomainConfig; + + assert.ok(webServer.dnsRecords, 'DNS records should be configured'); + assert.strictEqual( + webServer.dnsRecords.length, + 1, + 'Should have exactly one DNS record', + ); + await assertDnsARecord(ctx, primary, webServer.lb.lb.dnsName); + }); + + it('should make web server accessible via custom domain over HTTPS', async () => { + const { primary } = ctx.config.webServerWithDomainConfig; + await assertHealthCheckAccessible(ctx, primary); + }); + + it('should configure HTTPS listener with certificate for web server with SAN certificate', async () => { + const webServer = ctx.outputs.webServerWithSanCertificate.value; + await assertHttpsListenerWithCertificate(ctx, webServer); + }); + + it('should create DNS records for primary domain and all SANs', async () => { + const webServer = ctx.outputs.webServerWithSanCertificate.value; + const { primary, sans } = ctx.config.webServerWithSanCertificateConfig; + + assert.ok(webServer.dnsRecords, 'DNS records should exist'); + assert.strictEqual( + webServer.dnsRecords.length, + 1 + sans.length, + `Should have ${1 + sans.length} DNS records (1 primary + ${sans.length} SANs)`, + ); + + await assertDnsARecord(ctx, primary, webServer.lb.lb.dnsName); + + for (const san of sans) { + await assertDnsARecord(ctx, san, webServer.lb.lb.dnsName); + } + }); + + it('should be accessible via all SAN domains over HTTPS', async () => { + const { primary, sans } = ctx.config.webServerWithSanCertificateConfig; + const allDomains = [primary, ...sans]; + + for (const domain of allDomains) { + await assertHealthCheckAccessible(ctx, domain); + } + }); + + it('should configure HTTPS listener with certificate for web server', async () => { + const webServer = ctx.outputs.webServerWithCertificate.value; + await assertHttpsListenerWithCertificate(ctx, webServer); + }); + + it('should create DNS record only for specified domain in web server with certificate', async () => { + const webServer = ctx.outputs.webServerWithCertificate.value; + const { primary } = ctx.config.webServerWithCertificateConfig; + + assert.ok(webServer.dnsRecords, 'DNS records should exist'); + assert.strictEqual( + webServer.dnsRecords.length, + 1, + 'Should have exactly one DNS record', + ); + await assertDnsARecord(ctx, primary, webServer.lb.lb.dnsName); + }); + + it('should be accessible via specified domain over HTTPS', async () => { + const { primary } = ctx.config.webServerWithCertificateConfig; + await assertHealthCheckAccessible(ctx, primary); + }); +} + +async function assertHttpsListenerWithCertificate( + ctx: WebServerTestContext, + webServer: any, +) { + assert.ok(webServer.certificate, 'Certificate should be configured'); + assert.ok(webServer.lb.tlsListener, 'TLS listener should exist'); + + const command = new DescribeListenersCommand({ + ListenerArns: [webServer.lb.tlsListener.arn], + }); + + const response = await ctx.clients.elb.send(command); + const [listener] = response.Listeners ?? []; + + assert.ok(listener, 'HTTPS listener should exist in AWS'); + assert.strictEqual( + listener.Port, + 443, + 'HTTPS listener should be on port 443', + ); + assert.strictEqual( + listener.Protocol, + 'HTTPS', + 'Listener protocol should be HTTPS', + ); + + const certificateArn = listener.Certificates?.[0]?.CertificateArn; + assert.strictEqual( + certificateArn, + webServer.certificate.certificate.arn, + 'Certificate ARN should match the configured certificate', + ); +} + +async function assertDnsARecord( + ctx: WebServerTestContext, + domain: string, + loadBalancerDnsName: string, +) { + const hostedZoneId = process.env.ICB_HOSTED_ZONE_ID!; + + const command = new ListResourceRecordSetsCommand({ + HostedZoneId: hostedZoneId, + StartRecordName: domain, + StartRecordType: 'A', + MaxItems: 1, + }); + + const response = await ctx.clients.route53.send(command); + const record = response.ResourceRecordSets?.find( + r => r.Name === `${domain}.` && r.Type === 'A', + ); + + assert.ok(record, `A record for ${domain} should exist in Route53`); + assert.ok(record.AliasTarget, 'Record should be an alias record'); + assert.ok( + record.AliasTarget?.DNSName?.includes(loadBalancerDnsName), + `Record for ${domain} should point to load balancer`, + ); +} + +async function assertHealthCheckAccessible( + ctx: WebServerTestContext, + domain: string, +) { + return backOff(async () => { + const response = await request( + `https://${domain}${ctx.config.healthCheckPath}`, + ); + assert.strictEqual( + response.statusCode, + status.OK, + `Should receive 200 from ${domain}`, + ); + }, ctx.config.exponentialBackOffConfig); +} diff --git a/tests/web-server/index.test.ts b/tests/web-server/index.test.ts index 088a78f6..d413e3f7 100644 --- a/tests/web-server/index.test.ts +++ b/tests/web-server/index.test.ts @@ -12,6 +12,7 @@ import { DescribeLoadBalancersCommand, DescribeTargetGroupsCommand, DescribeListenersCommand, + DescribeTargetGroupAttributesCommand, } from '@aws-sdk/client-elastic-load-balancing-v2'; import { ACMClient } from '@aws-sdk/client-acm'; import { Route53Client } from '@aws-sdk/client-route-53'; @@ -21,6 +22,7 @@ import status from 'http-status'; import * as automation from '../automation'; import { WebServerTestContext } from './test-context'; import * as config from './infrastructure/config'; +import { testWebServerWithDomain } from './domain.test'; import { requireEnv } from '../util'; const programArgs: InlineProgramArgs = { @@ -30,6 +32,8 @@ const programArgs: InlineProgramArgs = { }; const region = requireEnv('AWS_REGION'); +const domainName = requireEnv('ICB_DOMAIN_NAME'); +const hostedZoneId = requireEnv('ICB_HOSTED_ZONE_ID'); const ctx: WebServerTestContext = { outputs: {}, config, @@ -89,7 +93,7 @@ describe('Web server component deployment', () => { ); }); - it('should create target group with correct health check path', async () => { + it('should create target group with the correct configuration', async () => { const webServer = ctx.outputs.webServer.value; const command = new DescribeTargetGroupsCommand({ @@ -105,6 +109,19 @@ describe('Web server component deployment', () => { ctx.config.healthCheckPath, 'Target group should have correct health check path', ); + + const attributesCommand = new DescribeTargetGroupAttributesCommand({ + TargetGroupArn: webServer.lb.targetGroup.arn, + }); + const attributesResponse = await ctx.clients.elb.send(attributesCommand); + const algorithmAttribute = attributesResponse.Attributes?.find( + attr => attr.Key === 'load_balancing.algorithm.type', + ); + assert.strictEqual( + algorithmAttribute?.Value, + 'least_outstanding_requests', + 'Target group should use least_outstanding_requests algorithm', + ); }); it('should create HTTP listener on port 80', async () => { @@ -327,4 +344,6 @@ describe('Web server component deployment', () => { }, ); }); + + describe('With domain', () => testWebServerWithDomain(ctx)); }); diff --git a/tests/web-server/infrastructure/config.ts b/tests/web-server/infrastructure/config.ts index 765de42f..78b4fd2e 100644 --- a/tests/web-server/infrastructure/config.ts +++ b/tests/web-server/infrastructure/config.ts @@ -1,2 +1,29 @@ export const webServerName = 'web-server-test'; export const healthCheckPath = '/healthcheck'; + +export const webServerImageName = 'nginxdemos/nginx-hello:plain-text'; +export const webServerPort = 8080; + +const baseDomain = process.env.ICB_DOMAIN_NAME!; + +export const webServerWithDomainConfig = { + primary: `domain.${baseDomain}`, +}; + +export const webServerWithSanCertificateConfig = { + primary: baseDomain, + sans: [`api.${baseDomain}`, `app.${baseDomain}`], +}; + +export const webServerWithCertificateConfig = { + primary: `test.${baseDomain}`, + sans: [`test.api.${baseDomain}`, `test.app.${baseDomain}`], +}; + +export const exponentialBackOffConfig = { + delayFirstAttempt: true, + numOfAttempts: 10, + startingDelay: 2000, + timeMultiple: 2, + jitter: 'full' as const, +}; diff --git a/tests/web-server/infrastructure/index.ts b/tests/web-server/infrastructure/index.ts index 21daf6bd..cf15c8a9 100644 --- a/tests/web-server/infrastructure/index.ts +++ b/tests/web-server/infrastructure/index.ts @@ -1,7 +1,14 @@ import { Project, next as studion } from '@studion/infra-code-blocks'; import * as aws from '@pulumi/aws'; import * as pulumi from '@pulumi/pulumi'; -import { webServerName, healthCheckPath } from './config'; +import { + webServerName, + healthCheckPath, + webServerWithDomainConfig, + webServerWithSanCertificateConfig, + webServerWithCertificateConfig, + webServerImageName, +} from './config'; const stackName = pulumi.getStack(); const project: Project = new Project(webServerName, { services: [] }); @@ -38,20 +45,77 @@ const cluster = new aws.ecs.Cluster(`${webServerName}-cluster`, { name: `${webServerName}-cluster-${stackName}`, tags, }); +const ecs = { + cluster, + desiredCount: 1, + size: 'small' as const, + autoscaling: { enabled: false }, +}; const webServer = new studion.WebServerBuilder(webServerName) - .configureWebServer('nginxdemos/nginx-hello:plain-text', 8080) - .configureEcs({ - cluster, - desiredCount: 1, - size: 'small', - autoscaling: { enabled: false }, - }) + .configureWebServer(webServerImageName, 8080) + .configureEcs(ecs) .withInitContainer(init) .withSidecarContainer(sidecar) .withVpc(project.vpc) .withOtelCollector(otelCollector) .withCustomHealthCheckPath(healthCheckPath) + .withLoadBalancingAlgorithm('least_outstanding_requests') + .build({ parent: cluster }); + +const hostedZone = aws.route53.getZoneOutput({ + zoneId: process.env.ICB_HOSTED_ZONE_ID, + privateZone: false, +}); + +const webServerWithDomain = new studion.WebServerBuilder(`web-server-domain`) + .configureWebServer(webServerImageName, 8080) + .configureEcs(ecs) + .withVpc(project.vpc) + .withCustomHealthCheckPath(healthCheckPath) + .withCustomDomain(webServerWithDomainConfig.primary, hostedZone.zoneId) + .build({ parent: cluster }); + +const sanWebServerCert = new studion.AcmCertificate( + `${webServerName}-san-cert`, + { + domain: webServerWithSanCertificateConfig.primary, + subjectAlternativeNames: webServerWithSanCertificateConfig.sans, + hostedZoneId: hostedZone.zoneId, + }, +); +const webServerWithSanCertificate = new studion.WebServerBuilder( + `web-server-san`, +) + .configureWebServer(webServerImageName, 8080) + .configureEcs(ecs) + .withVpc(project.vpc) + .withCustomHealthCheckPath(healthCheckPath) + .withCertificate(sanWebServerCert, hostedZone.zoneId) + .build({ parent: cluster }); + +const certWebServer = new studion.AcmCertificate(`${webServerName}-cert`, { + domain: webServerWithCertificateConfig.primary, + subjectAlternativeNames: webServerWithCertificateConfig.sans, + hostedZoneId: hostedZone.zoneId, +}); +const webServerWithCertificate = new studion.WebServerBuilder(`web-server-cert`) + .configureWebServer(webServerImageName, 8080) + .configureEcs(ecs) + .withVpc(project.vpc) + .withCustomHealthCheckPath(healthCheckPath) + .withCertificate( + certWebServer, + hostedZone.zoneId, + webServerWithCertificateConfig.primary, + ) .build({ parent: cluster }); -export { project, webServer, otelCollector }; +export { + project, + webServer, + otelCollector, + webServerWithSanCertificate, + webServerWithCertificate, + webServerWithDomain, +}; diff --git a/tests/web-server/test-context.ts b/tests/web-server/test-context.ts index 44833651..4c2090f1 100644 --- a/tests/web-server/test-context.ts +++ b/tests/web-server/test-context.ts @@ -5,12 +5,35 @@ import { ElasticLoadBalancingV2Client } from '@aws-sdk/client-elastic-load-balan import { ACMClient } from '@aws-sdk/client-acm'; import { Route53Client } from '@aws-sdk/client-route-53'; -interface ConfigContext { - config: { - [key: string]: any; +interface WebServerTestConfig { + webServerName: string; + healthCheckPath: string; + webServerImageName: string; + webServerPort: number; + webServerWithDomainConfig: { + primary: string; + }; + webServerWithSanCertificateConfig: { + primary: string; + sans: string[]; + }; + webServerWithCertificateConfig: { + primary: string; + sans: string[]; + }; + exponentialBackOffConfig: { + delayFirstAttempt: boolean; + numOfAttempts: number; + startingDelay: number; + timeMultiple: number; + jitter: 'full' | 'none'; }; } +interface ConfigContext { + config: WebServerTestConfig; +} + interface PulumiProgramContext { outputs: OutputMap; }