engineering-handbook/constructs/vpn-ec2-instance.ts

182 lines
5.6 KiB
TypeScript
Raw Normal View History

/**
* Shared CDK construct: VPN-accessible EC2 instance on the Sea Haven private subnet.
*
* Encapsulates the repeating pattern from file-share and forgejo stacks:
* - Looks up the Sea Haven VPC and private subnet
* - Creates a security group with VPN (10.10.0.0/16) and VPC (10.20.0.0/16) ingress
* - Creates an IAM role with SSM and Secrets Manager access
* - Launches a t4g ARM64 AL2023 instance with encrypted EBS
* - Sets up DLM daily snapshots with 30-day retention
* - Exports InstanceId and PrivateIp as CloudFormation outputs
*
* Copy this file into your project's lib/constructs/ directory and import it.
*/
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import * as dlm from "aws-cdk-lib/aws-dlm";
import { Construct } from "constructs";
export interface IngressPort {
readonly port: number;
readonly description: string;
}
export interface VpnEc2InstanceProps {
readonly name: string;
readonly instanceType?: ec2.InstanceType;
readonly rootVolumeSize?: number;
readonly dataVolumeSize?: number;
readonly ingressPorts: IngressPort[];
readonly secretsPrefix: string;
readonly userData?: ec2.UserData;
readonly additionalPolicies?: iam.PolicyStatement[];
readonly snapshotRetentionDays?: number;
}
const VPC_ID = "vpc-0d3d4b67bd0cf8a68";
const PRIVATE_SUBNET_ID = "subnet-04e38c507e96f1926";
const PRIVATE_SUBNET_AZ = "us-east-1a";
const ACCOUNT_ID = "328440206208";
const REGION = "us-east-1";
const VPN_CIDR = "10.10.0.0/16";
const VPC_CIDR = "10.20.0.0/16";
export class VpnEc2Instance extends Construct {
public readonly instance: ec2.Instance;
public readonly securityGroup: ec2.SecurityGroup;
public readonly role: iam.Role;
constructor(scope: Construct, id: string, props: VpnEc2InstanceProps) {
super(scope, id);
const vpc = ec2.Vpc.fromLookup(this, "Vpc", { vpcId: VPC_ID });
const subnet = ec2.Subnet.fromSubnetAttributes(this, "PrivateSubnet", {
subnetId: PRIVATE_SUBNET_ID,
availabilityZone: PRIVATE_SUBNET_AZ,
});
this.securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
vpc,
securityGroupName: props.name,
description: `${props.name} — VPN and VPC access`,
allowAllOutbound: true,
});
for (const ingress of props.ingressPorts) {
this.securityGroup.addIngressRule(
ec2.Peer.ipv4(VPN_CIDR),
ec2.Port.tcp(ingress.port),
`${ingress.description} from VPN`
);
this.securityGroup.addIngressRule(
ec2.Peer.ipv4(VPC_CIDR),
ec2.Port.tcp(ingress.port),
`${ingress.description} from VPC`
);
}
this.role = new iam.Role(this, "InstanceRole", {
roleName: `${props.name}-instance`,
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
],
});
this.role.addToPolicy(
new iam.PolicyStatement({
actions: ["secretsmanager:GetSecretValue"],
resources: [
`arn:aws:secretsmanager:${REGION}:${ACCOUNT_ID}:secret:${props.secretsPrefix}/*`,
],
})
);
if (props.additionalPolicies) {
for (const policy of props.additionalPolicies) {
this.role.addToPolicy(policy);
}
}
const blockDevices: ec2.BlockDevice[] = [
{
deviceName: "/dev/xvda",
volume: ec2.BlockDeviceVolume.ebs(props.rootVolumeSize ?? 20, {
volumeType: ec2.EbsDeviceVolumeType.GP3,
encrypted: true,
}),
},
];
if (props.dataVolumeSize) {
blockDevices.push({
deviceName: "/dev/xvdf",
volume: ec2.BlockDeviceVolume.ebs(props.dataVolumeSize, {
volumeType: ec2.EbsDeviceVolumeType.GP3,
encrypted: true,
}),
});
}
this.instance = new ec2.Instance(this, "Instance", {
instanceName: props.name,
vpc,
vpcSubnets: { subnets: [subnet] },
instanceType:
props.instanceType ??
ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL),
machineImage: ec2.MachineImage.latestAmazonLinux2023({
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
}),
securityGroup: this.securityGroup,
role: this.role,
userData: props.userData,
blockDevices,
});
const backupTag = `${props.name}-backup`;
cdk.Tags.of(this.instance).add(backupTag, "true");
const dlmRole = new iam.Role(this, "DlmRole", {
roleName: `${props.name}-dlm`,
assumedBy: new iam.ServicePrincipal("dlm.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSDataLifecycleManagerServiceRole"
),
],
});
new dlm.CfnLifecyclePolicy(this, "SnapshotPolicy", {
description: `Nightly EBS snapshots for ${props.name}`,
state: "ENABLED",
executionRoleArn: dlmRole.roleArn,
policyDetails: {
resourceTypes: ["INSTANCE"],
targetTags: [{ key: backupTag, value: "true" }],
schedules: [
{
name: `${props.name}-nightly`,
createRule: { interval: 24, intervalUnit: "HOURS", times: ["06:00"] },
retainRule: { count: props.snapshotRetentionDays ?? 30 },
copyTags: true,
tagsToAdd: [{ key: backupTag, value: "true" }],
},
],
},
});
new cdk.CfnOutput(this, "InstanceId", {
value: this.instance.instanceId,
});
new cdk.CfnOutput(this, "PrivateIp", {
value: this.instance.instancePrivateIp,
description: `Private IP for ${props.name}`,
});
}
}