You need to create a reusable Pulumi component resource that provisions a 'Microservice' abstraction including an ECS Fargate service, ALB target group, Route53 record, and CloudWatch alarms. How do you design this component?
Quick Answer
Create a ComponentResource subclass that encapsulates all child resources, accepts a typed args interface, registers child resources with the parent, and exposes outputs. Publish it as an npm/PyPI package for team consumption with versioning and documentation.
Detailed Answer
ComponentResource Pattern
Pulumi's ComponentResource is the abstraction mechanism for creating reusable, higher-level resources. Unlike a regular resource that maps to a single cloud API, a ComponentResource groups multiple resources into a logical unit with a single URN. Child resources are registered with { parent: this } so they appear nested in the Pulumi state tree. The component defines its own typed input interface and exposes outputs.
Design Principles
Follow the principle of sensible defaults with escape hatches. The component should work out of the box for 80% of use cases with minimal configuration (just service name, container image, port), while allowing power users to override every aspect (custom health check paths, CPU/memory, scaling policies). Use TypeScript interfaces with optional fields and spread operators to merge defaults with user overrides.
State and Lifecycle
ComponentResources don't have their own cloud-side state; they're purely logical groupings. However, they do appear in Pulumi state with their own URN. When you update a component (add a new child resource), Pulumi handles the diff correctly because each child resource has its own URN. Deleting a component deletes all child resources. This is critical for cleanup in self-service scenarios.
Distribution and Versioning
Publish your component as an npm package (TypeScript) or PyPI package (Python) with semantic versioning. Breaking changes to the component's interface get a major version bump. Include README documentation with usage examples. Use Pulumi's schema system if you want to create a multi-language component that works across TypeScript, Python, Go, and C# simultaneously.
Code Example
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface MicroserviceArgs {
image: pulumi.Input<string>;
port: number;
cpu?: number;
memory?: number;
desiredCount?: number;
healthCheckPath?: string;
environment?: Record<string, pulumi.Input<string>>;
vpcId: pulumi.Input<string>;
subnetIds: pulumi.Input<string>[];
albListenerArn: pulumi.Input<string>;
hostHeader: string;
hostedZoneId: pulumi.Input<string>;
}
export class Microservice extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>;
public readonly serviceArn: pulumi.Output<string>;
public readonly targetGroupArn: pulumi.Output<string>;
constructor(name: string, args: MicroserviceArgs, opts?: pulumi.ComponentResourceOptions) {
super("acme:platform:Microservice", name, {}, opts);
const cpu = args.cpu || 256;
const memory = args.memory || 512;
const desiredCount = args.desiredCount || 2;
const healthCheckPath = args.healthCheckPath || "/health";
// Task Definition
const taskDef = new aws.ecs.TaskDefinition(`${name}-task`, {
family: name,
networkMode: "awsvpc",
requiresCompatibilities: ["FARGATE"],
cpu: cpu.toString(),
memory: memory.toString(),
executionRoleArn: executionRole.arn,
containerDefinitions: pulumi.output(args.image).apply(image => JSON.stringify([{
name: name,
image: image,
portMappings: [{ containerPort: args.port }],
environment: Object.entries(args.environment || {}).map(([k, v]) => ({ name: k, value: v })),
logConfiguration: {
logDriver: "awslogs",
options: { "awslogs-group": `/ecs/${name}`, "awslogs-region": "us-east-1" },
},
}])),
}, { parent: this });
// Target Group
const tg = new aws.lb.TargetGroup(`${name}-tg`, {
port: args.port,
protocol: "HTTP",
vpcId: args.vpcId,
targetType: "ip",
healthCheck: { path: healthCheckPath, interval: 15 },
}, { parent: this });
// ALB Listener Rule
new aws.lb.ListenerRule(`${name}-rule`, {
listenerArn: args.albListenerArn,
conditions: [{ hostHeader: { values: [args.hostHeader] } }],
actions: [{ type: "forward", targetGroupArn: tg.arn }],
}, { parent: this });
// ECS Service
const service = new aws.ecs.Service(`${name}-svc`, {
cluster: clusterArn,
taskDefinition: taskDef.arn,
desiredCount: desiredCount,
launchType: "FARGATE",
networkConfiguration: {
subnets: args.subnetIds,
assignPublicIp: false,
},
loadBalancers: [{ targetGroupArn: tg.arn, containerName: name, containerPort: args.port }],
}, { parent: this });
// CloudWatch Alarm
new aws.cloudwatch.MetricAlarm(`${name}-5xx-alarm`, {
metricName: "HTTPCode_Target_5XX_Count",
namespace: "AWS/ApplicationELB",
statistic: "Sum",
period: 60,
evaluationPeriods: 3,
threshold: 10,
comparisonOperator: "GreaterThanThreshold",
alarmActions: [snsTopicArn],
}, { parent: this });
this.url = pulumi.interpolate`https://${args.hostHeader}`;
this.serviceArn = service.id;
this.targetGroupArn = tg.arn;
this.registerOutputs({ url: this.url, serviceArn: this.serviceArn });
}
}
// Usage:
const orderService = new Microservice("order-service", {
image: "123456789.dkr.ecr.us-east-1.amazonaws.com/order-service:v1.2.3",
port: 8080,
desiredCount: 3,
hostHeader: "orders.api.acme.com",
vpcId: infra.vpcId,
subnetIds: infra.privateSubnetIds,
albListenerArn: infra.albListenerArn,
hostedZoneId: infra.hostedZoneId,
});Interview Tip
Show that ComponentResource is Pulumi's answer to Terraform modules but with real OOP principles: inheritance, typed interfaces, encapsulation. Emphasize the { parent: this } pattern for state tree organization and the registerOutputs call for proper resource tracking.