Skip to main content

Overview

The EC2 Instance resource deploys a managed EC2 instance backed by an Auto Scaling Group, with security groups and optional SSH access. Use it for workloads that need direct server access or custom configuration. IMDSv2 is required by default, the root EBS volume is encrypted by default, and AZRebalance is suspended to keep instances pinned to their subnet.

Resource Class

import { Ec2Instance } from "@fjall/components-infrastructure/lib/resources/aws/compute/ec2";

Basic Usage

const instance = new Ec2Instance(this, "MyInstance", {
  serviceName: "my-service",
  instanceType: "t4g.medium",
  vpc: myVpc,
  minCapacity: 1,
  maxCapacity: 3,
});

Configuration Options

Core Properties

PropertyTypeDescriptionDefault
serviceNamestringService identifierRequired
instanceTypestringEC2 instance typeRequired
vpcIVpcVPC for deploymentAuto-created
accountIdstringAWS account IDCurrent account

Capacity Configuration

PropertyTypeDescriptionDefault
minCapacitynumberMinimum instances (passed to the CDK ASG)CDK default 1
maxCapacitynumberMaximum instances (passed to the CDK ASG)CDK default 1
desiredCapacitynumberInitial instance count (passed to the CDK ASG)CDK default
spotCapacityPercentagenumberPercentage of spot instances0
minCapacity and maxCapacity forward straight to the CDK Auto Scaling Group without a Fjall-supplied fallback. When both are omitted, CDK’s default of 1 applies to each.

Instance Configuration

PropertyTypeDescriptionDefault
machineImageIMachineImageAMI to useLatest Amazon Linux 2023
userDataUserDataUser data script-
roleRoleIAM role for instance-
blockDevicesBlockDevice[]EBS volume configuration-
enableSSHbooleanEnable SSH accessfalse

Network Configuration

PropertyTypeDescriptionDefault
subnetConfigurationSubnetConfiguration[]Custom subnet configurationStandard public/private

Machine Images

When machineImage is omitted, the construct uses MachineImage.latestAmazonLinux2023().

Amazon Linux 2023 (default)

const instance = new Ec2Instance(this, "AL2023Instance", {
  serviceName: "al2023-service",
  instanceType: "t4g.micro",
  machineImage: MachineImage.latestAmazonLinux2023(),
});

Ubuntu

const instance = new Ec2Instance(this, "UbuntuInstance", {
  serviceName: "ubuntu-service",
  instanceType: "t4g.small",
  machineImage: MachineImage.fromSsmParameter(
    "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id",
  ),
});

Custom AMI

const instance = new Ec2Instance(this, "CustomInstance", {
  serviceName: "custom-service",
  instanceType: "m5.large",
  machineImage: MachineImage.lookup({
    name: "my-custom-ami-*",
    owners: ["self"],
  }),
});

User Data Configuration

Basic Script

const userData = UserData.forLinux();
userData.addCommands(
  "yum update -y",
  "yum install -y httpd",
  "systemctl start httpd",
  "systemctl enable httpd",
);

const instance = new Ec2Instance(this, "WebServer", {
  serviceName: "web",
  instanceType: "t4g.micro",
  userData: userData,
});

Complex Setup

const userData = UserData.forLinux();
userData.addCommands(
  // Install dependencies
  "yum update -y",
  "yum install -y docker",

  // Start Docker
  "systemctl start docker",
  "usermod -a -G docker ec2-user",

  // Pull and run container
  "docker pull nginx:latest",
  "docker run -d -p 80:80 nginx",
);

Storage Configuration

Additional EBS Volumes

const instance = new Ec2Instance(this, "StorageInstance", {
  serviceName: "storage",
  instanceType: "m5.large",
  blockDevices: [
    {
      deviceName: "/dev/sdf",
      volume: BlockDeviceVolume.ebs(100, {
        volumeType: EbsDeviceVolumeType.GP3,
        encrypted: true,
        deleteOnTermination: false,
      }),
    },
  ],
});

RAID Configuration

const userData = UserData.forLinux();
userData.addCommands(
  // Create RAID 0 array
  "mdadm --create /dev/md0 --level=0 --raid-devices=2 /dev/xvdf /dev/xvdg",
  "mkfs.ext4 /dev/md0",
  "mkdir /data",
  "mount /dev/md0 /data",
);

Security Configuration

SSH Access

const instance = new Ec2Instance(this, "SSHInstance", {
  serviceName: "ssh-enabled",
  instanceType: "t4g.micro",
  enableSSH: true, // Creates key pair and opens port 22
});

Custom Security Rules

const instance = new Ec2Instance(this, "SecureInstance", {
  serviceName: "secure",
  instanceType: "t4g.small",
});

// Add custom ingress rules
instance.asgSecurityGroup.addIngressRule(
  Peer.ipv4("10.0.0.0/8"),
  Port.tcp(443),
  "Allow HTTPS from internal",
);

// Connect to other resources
instance.connections.allowTo(database, Port.tcp(5432), "Allow database access");

IAM Role Configuration

The role prop accepts a standard CDK IAM Role from aws-cdk-lib/aws-iam.
const role = new Role(this, "InstanceRole", {
  assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
  managedPolicies: [
    ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
  ],
  inlinePolicies: {
    S3Access: new PolicyDocument({
      statements: [
        new PolicyStatement({
          actions: ["s3:GetObject", "s3:PutObject"],
          resources: ["arn:aws:s3:::my-bucket/*"],
        }),
      ],
    }),
  },
});

const instance = new Ec2Instance(this, "RoleInstance", {
  serviceName: "role-service",
  instanceType: "t4g.medium",
  role: role,
});

Auto Scaling Configuration

Scaling Across AZs

const instance = new Ec2Instance(this, "ScaledInstance", {
  serviceName: "scaled",
  instanceType: "t4g.large",
  minCapacity: 2,
  maxCapacity: 10,
  vpc: vpc, // Ensure VPC has subnets in multiple AZs
});

Spot Instances

const instance = new Ec2Instance(this, "SpotInstance", {
  serviceName: "spot",
  instanceType: "c5.large",
  minCapacity: 3,
  maxCapacity: 10,
  spotCapacityPercentage: 70, // 70% spot, 30% on-demand
});

Methods

Get Security Group

const securityGroup = instance.asgSecurityGroup;
// Returns: SecurityGroup

Get Connections

const connections = instance.connections;
// Use for network access control
connections.allowTo(targetResource, Port.tcp(443));

Get VPC

const vpc = instance.vpc;
// Returns: IVpc

Get Auto Scaling Group

The underlying ASG is private. Read it through getAutoScalingGroup(), for example to register it as a load balancer target.
const asg = instance.getAutoScalingGroup();
// Returns: AutoScalingGroup

Advanced Patterns

Web Server Fleet

const webFleet = new Ec2Instance(this, "WebFleet", {
  serviceName: "web-fleet",
  instanceType: "t4g.small",
  minCapacity: 2,
  maxCapacity: 10,
  userData: UserData.forLinux({
    shebang: "#!/bin/bash -xe",
  }),
});

// Add load balancer
const alb = new ApplicationLoadBalancer(this, "WebALB", {
  vpc: webFleet.vpc,
  internetFacing: true,
});

// Configure target group
const targetGroup = new ApplicationTargetGroup(this, "WebTargets", {
  vpc: webFleet.vpc,
  port: 80,
  targets: [webFleet.getAutoScalingGroup()],
});

Bastion Host

const bastion = new Ec2Instance(this, "Bastion", {
  serviceName: "bastion",
  instanceType: "t4g.nano",
  enableSSH: true,
  minCapacity: 1,
  maxCapacity: 1,
});

// Restrict SSH access
bastion.asgSecurityGroup.addIngressRule(
  Peer.ipv4("203.0.113.0/24"), // Your office IP
  Port.tcp(22),
  "SSH from office only",
);

GPU Instance

const gpuInstance = new Ec2Instance(this, "GPUInstance", {
  serviceName: "ml-gpu",
  instanceType: "p3.2xlarge",
  machineImage: MachineImage.fromSsmParameter(
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2",
  ),
  blockDevices: [
    {
      deviceName: "/dev/sda1",
      volume: BlockDeviceVolume.ebs(100, {
        volumeType: EbsDeviceVolumeType.GP3,
        iops: 3000,
      }),
    },
  ],
});

Complete Example

import { Ec2Instance } from "@fjall/components-infrastructure/lib/resources/aws/compute/ec2";
import { UserData, MachineImage, BlockDeviceVolume } from "aws-cdk-lib/aws-ec2";
import {
  Role,
  ServicePrincipal,
  PolicyStatement,
  PolicyDocument,
} from "aws-cdk-lib/aws-iam";

// Create IAM role
const instanceRole = new Role(this, "AppRole", {
  assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
  inlinePolicies: {
    AppPolicy: new PolicyDocument({
      statements: [
        new PolicyStatement({
          actions: ["s3:GetObject"],
          resources: ["arn:aws:s3:::config-bucket/*"],
        }),
      ],
    }),
  },
});

// Prepare user data
const userData = UserData.forLinux();
userData.addCommands(
  // Update system
  "yum update -y",

  // Install application dependencies
  "yum install -y nodejs npm",

  // Download application
  "aws s3 cp s3://config-bucket/app.tar.gz /opt/",
  "cd /opt && tar -xzf app.tar.gz",

  // Start application
  "npm install --production",
  "npm start",
);

// Create EC2 instance
const appInstance = new Ec2Instance(this, "AppInstance", {
  serviceName: "production-app",
  instanceType: "t4g.medium",
  minCapacity: 2,
  maxCapacity: 6,
  machineImage: MachineImage.latestAmazonLinux2023(),
  userData: userData,
  role: instanceRole,
  blockDevices: [
    {
      deviceName: "/dev/xvda",
      volume: BlockDeviceVolume.ebs(30, {
        encrypted: true,
        deleteOnTermination: true,
      }),
    },
  ],
  enableSSH: false, // Production, no direct SSH
});

// Allow database access
appInstance.connections.allowTo(
  database,
  Port.tcp(5432),
  "Application to database",
);

Best Practices

  1. Use Systems Manager instead of SSH for production
  2. Enable IMDSv2 (enabled by default in this construct)
  3. Encrypt EBS volumes for sensitive data
  4. Use Auto Scaling even for single instances
  5. Apply least privilege IAM policies
  6. Monitor with CloudWatch and set alarms
  7. Use user data for repeatable configuration

Cost Optimisation

  • Use Spot instances for fault-tolerant workloads
  • Right-size instances based on CloudWatch metrics
  • Enable detailed monitoring only when needed
  • Use GP3 volumes instead of GP2 for better price/performance
  • Consider Savings Plans for predictable workloads

Next Steps

Compute Factory

Build EC2 and ECS compute through the Fjall compute factory pattern.

Security Group

Control inbound and outbound traffic for your instances.

VPC

Configure the network your EC2 instances run in.

IAM Role

Grant least-privilege AWS permissions to your instances.