mirror of
https://github.com/Sea-Haven-Industries/engineering-handbook.git
synced 2026-09-30 15:03:14 +00:00
182 lines
5.6 KiB
TypeScript
182 lines
5.6 KiB
TypeScript
|
|
/**
|
||
|
|
* 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}`,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|