mirror of
https://github.com/Sea-Haven-Industries/engineering-handbook.git
synced 2026-10-04 16:02:11 +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
|
- [CI/CD Pipelines](cicd.md) -- every deployable repo gets a pipeline, no manual deploys
|
||||||
- [Git Hooks](hooks/) -- recommended pre-push and pre-commit hooks
|
- [Git Hooks](hooks/) -- recommended pre-push and pre-commit hooks
|
||||||
- [Scripts](scripts/) -- repo provisioning, automation tooling
|
- [Scripts](scripts/) -- repo provisioning, automation tooling
|
||||||
|
- [CDK Constructs](constructs/) -- shared VPN EC2 instance construct and other reusable patterns
|
||||||
|
|
||||||
## Contributing
|
## 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