/** * 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}`, }); } }