Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
12bd0dd
Create acm certificate v2 component
bornast Dec 5, 2025
b8b8d94
Implement acm certificate tests
bornast Dec 5, 2025
431039c
Add legacy prefix to certificate v1 component
bornast Dec 5, 2025
6249fa5
Add subjectAlternativeNames option arg
bornast Dec 9, 2025
710a102
Add test for cert with SANs
bornast Dec 9, 2025
1c98b9e
Add acm certificate namespace for types
bornast Dec 10, 2025
06184a8
Remove legacy prefix from v1 certificate component
bornast Dec 10, 2025
50ff252
Rename test assertions
bornast Dec 10, 2025
5082c38
Fallback to hosted zone id arg if zone is not found by domain name
bornast Dec 10, 2025
266a103
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Dec 10, 2025
46ca7e8
Fix get zone method args
bornast Dec 10, 2025
80e5f92
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Dec 10, 2025
9a82628
Refactor get zone method args
bornast Dec 10, 2025
359a94b
Add ICB prefix to env variables
bornast Dec 10, 2025
2497cc5
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Dec 10, 2025
034c447
Fix method name typo
bornast Dec 10, 2025
e56724e
Export certificate using esmodule syntax
bornast Dec 16, 2025
5c53176
Merge branch task/certificate-v2-component into task/certificate-v2-s…
bornast Dec 16, 2025
1eee025
Update web server builder with cert method
bornast Jan 1, 2026
b8ef5e8
Enable san record creation from cert
bornast Jan 1, 2026
868ecb5
Implement tests for web server components with certificates
bornast Jan 1, 2026
9704602
Add docs for certificate and domain args edge case
bornast Jan 1, 2026
6fc989d
Remove publicSubnetIds arg
bornast Jan 1, 2026
ec7aba0
Refactor ecs service creation method
bornast Jan 1, 2026
ce4031b
Add load balancing algorithm argument
bornast Jan 1, 2026
9348fe8
Resolve lb algorithm assertion error
bornast Jan 1, 2026
99e85c5
Rephrase assertion text
bornast Jan 1, 2026
4a786f5
Update load balancer listener ssl policy (#111)
bornast Jan 9, 2026
fbf61a3
Merge branch 'master' into task/certificate-v2-component
bornast Jan 9, 2026
18fa3b6
Move setup to the top-level to prevent false positives
bornast Jan 9, 2026
ded476b
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Jan 9, 2026
eea55c9
Merge branch 'task/certificate-v2-san-arg' into task/web-server-args
bornast Jan 9, 2026
6834bc8
Merge branch 'master' into task/web-server-args
bornast Jan 12, 2026
39cf8e6
Extract web server image name as const
bornast Jan 12, 2026
7f019ca
Refactor dns creation method return statement
bornast Jan 12, 2026
ffa0c6d
Change web server constructor error message
bornast Jan 12, 2026
c5beaa0
Refactor web server config
bornast Jan 12, 2026
9a6ce3f
Replace dnsRecord and sanRecords with one dnsRecords prop
bornast Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/v2/components/web-server/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ 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<WebServer.EcsConfig, 'vpc' | 'volumes'>;

export type Args = Omit<
WebServer.Args,
| 'vpc'
| 'publicSubnetIds'
| 'cluster'
| 'volumes'
| 'domain'
Expand All @@ -26,7 +26,9 @@ export class WebServerBuilder {
private _ecsConfig?: WebServerBuilder.EcsConfig;
private _domain?: pulumi.Input<string>;
private _hostedZoneId?: pulumi.Input<string>;
private _certificate?: pulumi.Input<AcmCertificate>;
private _healthCheckPath?: pulumi.Input<string>;
private _loadBalancingAlgorithmType?: pulumi.Input<string>;
private _otelCollector?: pulumi.Input<OtelCollector>;
private _initContainers: pulumi.Input<WebServer.InitContainer>[] = [];
private _sidecarContainers: pulumi.Input<WebServer.SidecarContainer>[] = [];
Expand Down Expand Up @@ -87,6 +89,18 @@ export class WebServerBuilder {
return this;
}

public withCertificate(
certificate: WebServerBuilder.Args['certificate'],
hostedZoneId: pulumi.Input<string>,
domain?: pulumi.Input<string>,
): this {
this._certificate = certificate;
this._hostedZoneId = hostedZoneId;
this._domain = domain;

return this;
}

public withInitContainer(container: WebServer.InitContainer): this {
this._initContainers.push(container);

Expand All @@ -113,6 +127,12 @@ export class WebServerBuilder {
return this;
}

public withLoadBalancingAlgorithm(algorithm: pulumi.Input<string>) {
this._loadBalancingAlgorithmType = algorithm;

return this;
}

public build(opts: pulumi.ComponentResourceOptions = {}): WebServer {
if (!this._container) {
throw new Error(
Expand All @@ -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,
Expand Down
209 changes: 132 additions & 77 deletions src/v2/components/web-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,21 +38,26 @@ export namespace WebServer {

export type Args = EcsConfig &
Container & {
// TODO: Automatically use subnet IDs from passed `vpc`
publicSubnetIds: pulumi.Input<pulumi.Input<string>[]>;
/**
* The domain which will be used to access the service.
* The domain or subdomain must belong to the provided hostedZone.
*/
domain?: pulumi.Input<string>;
hostedZoneId?: pulumi.Input<string>;
/**
* 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<AcmCertificate>;
/**
* Path for the load balancer target group health check request.
*
* @default
* "/healthcheck"
*/
healthCheckPath?: pulumi.Input<string>;
loadBalancingAlgorithmType?: pulumi.Input<string>;
initContainers?: pulumi.Input<pulumi.Input<WebServer.InitContainer>[]>;
sidecarContainers?: pulumi.Input<
pulumi.Input<WebServer.SidecarContainer>[]
Expand All @@ -71,26 +76,30 @@ export class WebServer extends pulumi.ComponentResource {
initContainers?: pulumi.Output<EcsService.Container[]>;
sidecarContainers?: pulumi.Output<EcsService.Container[]>;
volumes?: pulumi.Output<EcsService.PersistentStorageVolume[]>;
certificate?: AcmCertificate;
dnsRecord?: aws.route53.Record;
certificate?: pulumi.Output<AcmCertificate>;
dnsRecords?: pulumi.Output<aws.route53.Record[]>;

constructor(
name: string,
args: WebServer.Args,
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;
Expand All @@ -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 },
);
Expand All @@ -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();
Expand Down Expand Up @@ -273,64 +283,109 @@ export class WebServer extends pulumi.ComponentResource {
lb: WebServerLoadBalancer,
ecsConfig: WebServer.EcsConfig,
volumes?: pulumi.Output<EcsService.PersistentStorageVolume[]>,
containers?: EcsService.Container[],
): EcsService {
return new EcsService(
`${this.name}-ecs`,
{
...ecsConfig,
volumes,
containers: [
initContainers?: pulumi.Output<EcsService.Container[]>,
sidecarContainers?: pulumi.Output<EcsService.Container[]>,
): pulumi.Output<EcsService> {
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<WebServer.Args>,
'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<AcmCertificate>,
hostedZoneId: pulumi.Input<string>,
domain?: pulumi.Input<string>,
): pulumi.Output<aws.route53.Record[]> {
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;
}
}
Loading