Package Exports
- @composurecdk/ec2
- @composurecdk/ec2/package.json
Readme
@composurecdk/ec2
EC2 and VPC builders for ComposureCDK.
This package provides fluent builders for AWS EC2 instances and VPCs with secure, AWS-recommended defaults. It wraps the CDK Instance and Vpc constructs — refer to the CDK documentation for the full set of configurable properties.
Instance Builder
import { createInstanceBuilder } from "@composurecdk/ec2";
import { InstanceClass, InstanceSize, InstanceType, MachineImage } from "aws-cdk-lib/aws-ec2";
const server = createInstanceBuilder()
.vpc(vpc)
.instanceType(InstanceType.of(InstanceClass.T3, InstanceSize.MICRO))
.machineImage(MachineImage.latestAmazonLinux2023())
.build(stack, "MyInstance");Every InstanceProps property is available as a fluent setter on the builder, except vpc which is set via the dedicated .vpc() method to support cross-component wiring with ref<T>(...).
Secure Defaults
createInstanceBuilder applies the following defaults. Each can be overridden via the builder's fluent API.
| Property | Default | Rationale |
|---|---|---|
requireImdsv2 |
true |
IMDSv2 requires a session token, blocking the SSRF-based credential exfiltration common to IMDSv1. |
detailedMonitoring |
true |
1-minute CloudWatch metric granularity is required for short-window alarm evaluation. |
ssmSessionPermissions |
true |
Attaches AmazonSSMManagedInstanceCore — Session Manager replaces SSH entirely (no key pairs, bastions). |
ebsOptimized |
true |
Dedicated EBS bandwidth. Free on current-generation instance types. |
blockDevices |
8 GiB GP3 root volume at /dev/xvda, encrypted |
Encrypted at rest with the account's default EBS KMS key. |
Three properties intentionally have no default — they are application-specific and must be supplied explicitly:
vpc(via the.vpc()method)instanceTypemachineImage
The defaults are exported as INSTANCE_DEFAULTS for visibility and testing:
import { INSTANCE_DEFAULTS } from "@composurecdk/ec2";Recommended Alarms
The builder creates AWS-recommended CloudWatch alarms by default. No alarm actions are configured — wire actions via alarmActionsPolicy from @composurecdk/cloudwatch, or by accessing alarms from the build result.
| Alarm | Metric | Default threshold | Created when |
|---|---|---|---|
cpuUtilization |
CPUUtilization (Average, 1 min) | > 80% over 5 min | Always |
statusCheckFailed |
StatusCheckFailed (Sum, 1 min) | > 0 over 2 min | Always |
attachedEbsStatusCheckFailed |
StatusCheckFailed_AttachedEBS (Maximum, 1 min) | >= 1 over 10 min | Always |
cpuCreditBalance |
CPUCreditBalance (Minimum, 5 min) | < 50 over 3 x 5 min | instanceType is burstable (T2, T3, T3a, T4g family) |
The defaults are exported as INSTANCE_ALARM_DEFAULTS for visibility and testing:
import { INSTANCE_ALARM_DEFAULTS } from "@composurecdk/ec2";Customizing thresholds
Override individual alarm properties via recommendedAlarms. Unspecified fields keep their defaults.
const server = createInstanceBuilder()
.vpc(vpc)
.instanceType(InstanceType.of(InstanceClass.T3, InstanceSize.MICRO))
.machineImage(MachineImage.latestAmazonLinux2023())
.recommendedAlarms({
cpuUtilization: { threshold: 90, evaluationPeriods: 3, datapointsToAlarm: 3 },
});Disabling alarms
Disable all recommended alarms:
builder.recommendedAlarms(false);
// or
builder.recommendedAlarms({ enabled: false });Disable individual alarms:
builder.recommendedAlarms({ cpuCreditBalance: false });Custom alarms
Add custom alarms alongside the recommended ones via addAlarm. The callback receives an AlarmDefinitionBuilder typed to the Instance.
import { Metric, Stats } from "aws-cdk-lib/aws-cloudwatch";
import { Duration } from "aws-cdk-lib";
const server = createInstanceBuilder()
.vpc(vpc)
.instanceType(InstanceType.of(InstanceClass.T3, InstanceSize.MICRO))
.machineImage(MachineImage.latestAmazonLinux2023())
.addAlarm("networkIn", (alarm) =>
alarm
.metric(
(instance) =>
new Metric({
namespace: "AWS/EC2",
metricName: "NetworkIn",
dimensionsMap: { InstanceId: instance.instanceId },
statistic: Stats.AVERAGE,
period: Duration.minutes(1),
}),
)
.threshold(1_000_000_000)
.greaterThanOrEqual()
.description("Inbound network traffic is unusually high"),
);Attaching persistent volumes
attachVolume(key, volumeRef, opts) mirrors the call shape of addAlarm and produces an AWS::EC2::VolumeAttachment for an externally-managed EBS volume. The volume reference accepts either a Resolvable<VolumeBuilderResult> (drop a ref<VolumeBuilderResult>("data") straight in) or a Resolvable<IVolume>.
import { compose, ref } from "@composurecdk/core";
import {
createInstanceBuilder,
createVolumeBuilder,
createVpcBuilder,
type VolumeBuilderResult,
type VpcBuilderResult,
} from "@composurecdk/ec2";
import { Size } from "aws-cdk-lib";
import { InstanceClass, InstanceSize, InstanceType, MachineImage } from "aws-cdk-lib/aws-ec2";
compose(
{
network: createVpcBuilder().maxAzs(2).natGateways(0),
data: createVolumeBuilder()
.availabilityZone(ref<VpcBuilderResult>("network").map((r) => r.vpc.availabilityZones[0]))
.size(Size.gibibytes(50)),
agent: createInstanceBuilder()
.vpc(ref<VpcBuilderResult>("network").map((r) => r.vpc))
.instanceType(InstanceType.of(InstanceClass.T3, InstanceSize.MICRO))
.machineImage(MachineImage.latestAmazonLinux2023())
.attachVolume("AgentData", ref<VolumeBuilderResult>("data"), { device: "/dev/sdf" }),
},
{ network: [], data: ["network"], agent: ["network", "data"] },
).build(stack, "AgentApp");The result exposes the attachment under result.agent.volumeAttachments.AgentData and emits a per-attachment volumeStalledIo alarm under result.agent.alarms["AgentData.volumeStalledIo"]. When both AZs are concrete strings at synth, the builder asserts the instance and volume share an Availability Zone — synth-time failure beats boot-time failure.
VolumeStalledIOCheck is published only for Nitro-instance attachments. On non-Nitro instances the alarm sits at INSUFFICIENT_DATA, which the treatMissingData: NOT_BREACHING default makes harmless. To disable the alarm per attachment:
.attachVolume("AgentData", ref<VolumeBuilderResult>("data"), {
device: "/dev/sdf",
recommendedAlarms: false,
})VPC Builder
import { createVpcBuilder } from "@composurecdk/ec2";
const network = createVpcBuilder().maxAzs(3).natGateways(3).build(stack, "Network");Every VpcProps property is available as a fluent setter on the builder.
VPC Defaults
| Property | Default | Rationale |
|---|---|---|
maxAzs |
2 |
Meaningful HA without overspending. Override to 3+ for stricter production guarantees. |
natGateways |
1 |
Cost-conscious default. Production HA workloads should set natGateways to match maxAzs. |
enableDnsSupport |
true |
Required for most AWS managed services (ALB, RDS, VPC endpoints). |
enableDnsHostnames |
true |
Required for instances to receive public DNS hostnames. |
restrictDefaultSecurityGroup |
true |
Strips rules from the default SG, forcing explicit SG design. |
| Flow logs | Auto-created CloudWatch log group | Network audit trail with well-architected retention and removal policies via @composurecdk/logs. |
The defaults are exported as VPC_DEFAULTS for visibility and testing:
import { VPC_DEFAULTS } from "@composurecdk/ec2";Flow logs
By default, the builder auto-creates a CloudWatch-Logs-backed flow log with a managed LogGroup from @composurecdk/logs (two-year retention, RemovalPolicy.RETAIN).
Customize the auto-created LogGroup:
createVpcBuilder().flowLogs({
configure: (lg) => lg.retention(RetentionDays.NINETY_DAYS).removalPolicy(RemovalPolicy.DESTROY),
});Use a user-managed destination (e.g. an S3 bucket):
createVpcBuilder().flowLogs({
destination: FlowLogDestination.toS3(myBucket),
});Disable flow logs entirely:
createVpcBuilder().flowLogs(false);For multiple flow logs against the same VPC, omit this config and create additional FlowLog constructs directly against the returned vpc.
Security Group Builder
import { createSecurityGroupBuilder } from "@composurecdk/ec2";
import { Peer, Port } from "aws-cdk-lib/aws-ec2";
const web = createSecurityGroupBuilder()
.vpc(vpc)
.description("Public web tier")
.addIngressRule(Peer.anyIpv4(), Port.tcp(443), "Public HTTPS")
.addEgressRule(Peer.anyIpv4(), Port.tcp(443), "HTTPS to origin")
.build(stack, "WebSg");Every SecurityGroupProps property is available as a fluent setter on the builder, except vpc which is set via the dedicated .vpc() method to support cross-component wiring with ref<T>(...).
Ingress and egress rules are accumulated via addIngressRule, addEgressRule, and addSelfIngress (for the intra-SG "allow within the cluster" pattern). Each peer is a Resolvable<IPeer>, so it can be a concrete IPeer (a CIDR via Peer.ipv4(...), another ISecurityGroup, a prefix list, …) or a Ref to a sibling component's output.
Security Group Defaults
| Property | Default | Rationale |
|---|---|---|
allowAllOutbound |
false |
Closes the implicit 0.0.0.0/0 egress rule CDK ships by default. Every outbound flow becomes an explicit addEgressRule — the least-privilege default. |
Two properties intentionally have no default — they are application-specific and must be supplied explicitly:
vpc(via the.vpc()method)description(a short, human-readable summary of the SG's purpose; whitespace-only values are rejected)
The defaults are exported as SECURITY_GROUP_DEFAULTS for visibility and testing:
import { SECURITY_GROUP_DEFAULTS } from "@composurecdk/ec2";Wiring two SGs via ref
The canonical cross-component pattern — a bastion SG and a database SG that talks to it — declares the dependency in compose() and resolves the peer at build time:
import { compose, ref } from "@composurecdk/core";
import { createSecurityGroupBuilder, createVpcBuilder } from "@composurecdk/ec2";
import type { SecurityGroupBuilderResult, VpcBuilderResult } from "@composurecdk/ec2";
import { Port } from "aws-cdk-lib/aws-ec2";
compose(
{
network: createVpcBuilder(),
bastion: createSecurityGroupBuilder()
.vpc(ref<VpcBuilderResult>("network").get("vpc"))
.description("Bastion host"),
database: createSecurityGroupBuilder()
.vpc(ref<VpcBuilderResult>("network").get("vpc"))
.description("Database")
.addIngressRule(
ref<SecurityGroupBuilderResult>("bastion").get("securityGroup"),
Port.tcp(5432),
"Bastion to Postgres",
),
},
{ network: [], bastion: ["network"], database: ["network", "bastion"] },
).build(stack, "App");Recommended Alarms
The Security Group builder does not create CloudWatch alarms. Security groups do not emit CloudWatch metrics — the AWS recommended-alarms reference has no SG entry. Operational visibility for SGs comes from adjacent signals (VPC Flow Logs, GuardDuty findings, CloudTrail AuthorizeSecurityGroupIngress/Egress events), none of which belong on the builder result.
Volume Builder
import { createVolumeBuilder } from "@composurecdk/ec2";
import { Size } from "aws-cdk-lib";
const data = createVolumeBuilder()
.availabilityZone("us-east-1a")
.size(Size.gibibytes(50))
.build(stack, "AgentData");Every VolumeProps property is available as a fluent setter on the builder, except availabilityZone which is set via the dedicated .availabilityZone() method (so it can be wired from a sibling VpcBuilder via ref) and encryptionKey which accepts a Resolvable<IKey> for cross-component KMS-key wiring.
Volume Defaults
| Property | Default | Rationale |
|---|---|---|
volumeType |
GP3 |
Current-generation general-purpose SSD — cheaper and faster than GP2 at equivalent sizes. |
encrypted |
true |
Encryption at rest with the account's default EBS KMS key. Pass encryptionKey for a CMK. |
autoEnableIo |
true |
Lets I/O resume on suspected inconsistency so the instance can come up unattended. |
removalPolicy |
RemovalPolicy.RETAIN |
A destroyed volume is unrecoverable; an orphaned volume is a $/month nuisance. Mirrors BUCKET_DEFAULTS. |
Three properties intentionally have no default — they are application-specific and must be supplied explicitly:
availabilityZone(via the.availabilityZone()method)sizeiops/throughput(only when opting into a volume type that requires them)
The defaults are exported as VOLUME_DEFAULTS for visibility and testing:
import { VOLUME_DEFAULTS } from "@composurecdk/ec2";Recommended Volume Alarms
The builder creates AWS-recommended CloudWatch alarms by default. No alarm actions are configured — wire actions via alarmActionsPolicy from @composurecdk/cloudwatch.
| Alarm | Metric | Default threshold | Created when |
|---|---|---|---|
burstBalance |
BurstBalance (Average, 5 min) | < 20% over 3 × 5 min | volumeType is burstable (gp2, st1, sc1) |
The defaults are exported as VOLUME_ALARM_DEFAULTS for visibility and testing:
import { VOLUME_ALARM_DEFAULTS } from "@composurecdk/ec2";VolumeQueueLength and VolumeIdleTime are deferred — both need per-volumeType/per-workload tuning to be a defensible default. Wire them via addAlarm until first-class support lands:
import { Metric, Stats } from "aws-cdk-lib/aws-cloudwatch";
import { Duration } from "aws-cdk-lib";
const data = createVolumeBuilder()
.availabilityZone("us-east-1a")
.size(Size.gibibytes(50))
.addAlarm("volumeQueueLength", (alarm) =>
alarm
.metric(
(volume) =>
new Metric({
namespace: "AWS/EBS",
metricName: "VolumeQueueLength",
dimensionsMap: { VolumeId: volume.volumeId },
statistic: Stats.AVERAGE,
period: Duration.minutes(5),
}),
)
.threshold(10)
.greaterThan()
.description("EBS volume queue length is high"),
);Composing EC2 + VPC
Compose the builders into a single system — the instance is wired to the VPC via ref:
import { compose, ref } from "@composurecdk/core";
import { createInstanceBuilder, createVpcBuilder, type VpcBuilderResult } from "@composurecdk/ec2";
import type { Vpc } from "aws-cdk-lib/aws-ec2";
compose(
{
network: createVpcBuilder(),
server: createInstanceBuilder()
.vpc(ref<VpcBuilderResult>("network").map((r): Vpc => r.vpc))
.instanceType(InstanceType.of(InstanceClass.T3, InstanceSize.MICRO))
.machineImage(MachineImage.latestAmazonLinux2023()),
},
{ network: [], server: ["network"] },
).build(stack, "Ec2App");Examples
- Ec2Stack — VPC + EC2 instance with alarms wired to an SNS topic via
alarmActionsPolicy.