mirror of
https://github.com/Sea-Haven-Industries/engineering-handbook.git
synced 2026-09-30 10:23:13 +00:00
Add shared VpnEc2Instance CDK construct
Reference construct for the VPN-accessible EC2 pattern used by file-share and forgejo. Includes VPC/subnet lookup, SG, IAM role, encrypted EBS, and DLM snapshots. Copy into lib/constructs/.
This commit is contained in:
parent
e109101326
commit
3aa634c260
3 changed files with 223 additions and 0 deletions
|
|
@ -16,6 +16,7 @@ Engineering conventions and best practices for Sea Haven Industries.
|
|||
- [CI/CD Pipelines](cicd.md) -- every deployable repo gets a pipeline, no manual deploys
|
||||
- [Git Hooks](hooks/) -- recommended pre-push and pre-commit hooks
|
||||
- [Scripts](scripts/) -- repo provisioning, automation tooling
|
||||
- [CDK Constructs](constructs/) -- shared VPN EC2 instance construct and other reusable patterns
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
41
constructs/README.md
Normal file
41
constructs/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Shared CDK Constructs
|
||||
|
||||
Reference CDK constructs for common Sea Haven infrastructure patterns. Copy into your project's `lib/constructs/` directory.
|
||||
|
||||
## VpnEc2Instance
|
||||
|
||||
Encapsulates the full EC2-on-VPN pattern: VPC/subnet lookup, security group with VPN + VPC ingress, IAM role (SSM + Secrets Manager), encrypted EBS, and DLM daily snapshots.
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { VpnEc2Instance } from "./constructs/vpn-ec2-instance";
|
||||
|
||||
const server = new VpnEc2Instance(this, "Server", {
|
||||
name: "file-share",
|
||||
ingressPorts: [
|
||||
{ port: 445, description: "SMB" },
|
||||
{ port: 8080, description: "FileBrowser" },
|
||||
],
|
||||
secretsPrefix: "file-share",
|
||||
dataVolumeSize: 500,
|
||||
userData: myUserData,
|
||||
});
|
||||
|
||||
// Access underlying resources for further configuration:
|
||||
// server.instance, server.securityGroup, server.role
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `name` | string | required | Resource name prefix (kebab-case) |
|
||||
| `ingressPorts` | `IngressPort[]` | required | Ports to open from VPN and VPC CIDRs |
|
||||
| `secretsPrefix` | string | required | Secrets Manager path prefix for IAM policy |
|
||||
| `instanceType` | `InstanceType` | t4g.small | EC2 instance type |
|
||||
| `rootVolumeSize` | number | 20 | Root EBS volume in GiB |
|
||||
| `dataVolumeSize` | number | — | Optional second EBS volume in GiB (mounted at /dev/xvdf) |
|
||||
| `userData` | `UserData` | — | EC2 user data script |
|
||||
| `additionalPolicies` | `PolicyStatement[]` | — | Extra IAM policies for the instance role |
|
||||
| `snapshotRetentionDays` | number | 30 | DLM snapshot retention count |
|
||||
181
constructs/vpn-ec2-instance.ts
Normal file
181
constructs/vpn-ec2-instance.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* 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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue