mirror of
https://github.com/Sea-Haven-Industries/file-share.git
synced 2026-09-30 21:43:17 +00:00
Some checks are pending
Deploy / deploy (push) Waiting to run
- Import vol-04d951cccacc435b5 (the real 500GiB data volume, orphaned by the 2026-06-05 redeploy) via Volume.fromVolumeAttributes — unmanaged, so CloudFormation can attach but never create/replace/delete it. - Remove the inline /dev/xvdf blockDevice (this is what created the empty volume that shadowed the data). - cachedInContext: true + committed cdk.context.json so AMI updates are deliberate (uncached lookup replaced the instance on every new AL2023 release). - Includes the previously-deployed-but-uncommitted 2026-05-27 work: standalone volume pattern, volume-wait userdata, SFTP access. Deploy replaces the instance once; userdata's blkid guard mounts the existing filesystem without formatting.
243 lines
9 KiB
TypeScript
243 lines
9 KiB
TypeScript
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 class FileShareStack extends cdk.Stack {
|
|
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
|
super(scope, id, props);
|
|
|
|
const vpc = ec2.Vpc.fromLookup(this, "SeaHavenVpc", {
|
|
vpcId: "vpc-0d3d4b67bd0cf8a68",
|
|
});
|
|
|
|
const privateSubnet1 = ec2.Subnet.fromSubnetAttributes(
|
|
this, "PrivateSubnet1", {
|
|
subnetId: "subnet-04e38c507e96f1926",
|
|
availabilityZone: "us-east-1a",
|
|
}
|
|
);
|
|
|
|
const sg = new ec2.SecurityGroup(this, "SecurityGroup", {
|
|
vpc,
|
|
securityGroupName: "file-share",
|
|
description: "File share - SMB and FileBrowser via VPN",
|
|
allowAllOutbound: true,
|
|
});
|
|
|
|
sg.addIngressRule(ec2.Peer.ipv4("10.10.0.0/16"), ec2.Port.tcp(445), "SMB from office VPN");
|
|
sg.addIngressRule(ec2.Peer.ipv4("10.20.0.0/16"), ec2.Port.tcp(445), "SMB from VPC");
|
|
sg.addIngressRule(ec2.Peer.ipv4("10.10.0.0/16"), ec2.Port.tcp(8080), "FileBrowser from office VPN");
|
|
sg.addIngressRule(ec2.Peer.ipv4("10.20.0.0/16"), ec2.Port.tcp(8080), "FileBrowser from VPC");
|
|
sg.addIngressRule(ec2.Peer.ipv4("10.10.0.0/16"), ec2.Port.tcp(22), "SFTP from office VPN");
|
|
|
|
const role = new iam.Role(this, "InstanceRole", {
|
|
roleName: "file-share-instance",
|
|
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
|
|
managedPolicies: [
|
|
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
|
|
],
|
|
});
|
|
|
|
role.addToPolicy(new iam.PolicyStatement({
|
|
actions: ["secretsmanager:GetSecretValue"],
|
|
resources: [
|
|
`arn:aws:secretsmanager:us-east-1:328440206208:secret:file-share/*`,
|
|
],
|
|
}));
|
|
|
|
const userData = ec2.UserData.forLinux();
|
|
userData.addCommands(
|
|
"set -euxo pipefail",
|
|
"",
|
|
"# ── Data volume (wait for CfnVolumeAttachment) ──",
|
|
"until lsblk -dno NAME | grep -v $(lsblk -no PKNAME $(findmnt -n -o SOURCE /) 2>/dev/null || echo xvda) | grep -q .; do",
|
|
" echo 'Waiting for data volume...'",
|
|
" sleep 5",
|
|
"done",
|
|
"DATA_DEVICE=/dev/$(lsblk -dno NAME | grep -v $(lsblk -no PKNAME $(findmnt -n -o SOURCE /) 2>/dev/null || echo xvda) | head -1)",
|
|
"if ! blkid \"$DATA_DEVICE\"; then",
|
|
" mkfs.ext4 -L file-share-data \"$DATA_DEVICE\"",
|
|
"fi",
|
|
"mkdir -p /data",
|
|
"echo \"LABEL=file-share-data /data ext4 defaults,nofail 0 2\" >> /etc/fstab",
|
|
"mount -a",
|
|
"mkdir -p /data/share",
|
|
"",
|
|
"# ── Samba ──",
|
|
"dnf install -y samba samba-common jq",
|
|
"",
|
|
"useradd --system --no-create-home --shell /sbin/nologin adam || true",
|
|
"chown adam:adam /data/share",
|
|
"",
|
|
"SMB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id file-share/smb-password --query SecretString --output text --region us-east-1)",
|
|
"(echo \"$SMB_PASSWORD\"; echo \"$SMB_PASSWORD\") | smbpasswd -s -a adam",
|
|
"",
|
|
"cat > /etc/samba/smb.conf << 'SMBEOF'",
|
|
"[global]",
|
|
"workgroup = SEAHAVEN",
|
|
"server string = Sea Haven File Share",
|
|
"security = user",
|
|
"map to guest = never",
|
|
"log file = /var/log/samba/log.%m",
|
|
"max log size = 1000",
|
|
"server min protocol = SMB3",
|
|
"",
|
|
"# macOS Finder optimizations",
|
|
"vfs objects = catia fruit streams_xattr",
|
|
"fruit:metadata = stream",
|
|
"fruit:model = MacSamba",
|
|
"fruit:posix_rename = yes",
|
|
"fruit:veto_appledouble = no",
|
|
"fruit:nfs_aces = no",
|
|
"fruit:wipe_intentionally_left_blank_rfork = yes",
|
|
"fruit:delete_empty_adfiles = yes",
|
|
"",
|
|
"[files]",
|
|
"path = /data/share",
|
|
"browseable = yes",
|
|
"writable = yes",
|
|
"valid users = adam",
|
|
"create mask = 0644",
|
|
"directory mask = 0755",
|
|
"SMBEOF",
|
|
"",
|
|
"systemctl enable --now smb nmb",
|
|
"",
|
|
"# ── FileBrowser ──",
|
|
'FB_VERSION=$(curl -sf "https://api.github.com/repos/filebrowser/filebrowser/releases/latest" | jq -r .tag_name)',
|
|
'FB_URL="https://github.com/filebrowser/filebrowser/releases/download/${FB_VERSION}/linux-arm64-filebrowser.tar.gz"',
|
|
"curl -sfL \"$FB_URL\" | tar xz -C /usr/local/bin filebrowser",
|
|
"chmod +x /usr/local/bin/filebrowser",
|
|
"",
|
|
"mkdir -p /etc/filebrowser",
|
|
"FB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id file-share/filebrowser-password --query SecretString --output text --region us-east-1)",
|
|
"",
|
|
"cat > /etc/filebrowser/config.json << FBEOF",
|
|
"{",
|
|
" \"address\": \"0.0.0.0\",",
|
|
" \"port\": 8080,",
|
|
" \"root\": \"/data/share\",",
|
|
" \"database\": \"/etc/filebrowser/filebrowser.db\",",
|
|
" \"log\": \"/var/log/filebrowser.log\"",
|
|
"}",
|
|
"FBEOF",
|
|
"",
|
|
"filebrowser config init --config /etc/filebrowser/config.json",
|
|
"filebrowser users add admin \"$FB_PASSWORD\" --config /etc/filebrowser/config.json --perm.admin",
|
|
"",
|
|
"cat > /etc/systemd/system/filebrowser.service << 'SVCEOF'",
|
|
"[Unit]",
|
|
"Description=FileBrowser",
|
|
"After=network.target",
|
|
"",
|
|
"[Service]",
|
|
"Type=simple",
|
|
"ExecStart=/usr/local/bin/filebrowser --config /etc/filebrowser/config.json",
|
|
"Restart=always",
|
|
"RestartSec=5",
|
|
"",
|
|
"[Install]",
|
|
"WantedBy=multi-user.target",
|
|
"SVCEOF",
|
|
"",
|
|
"systemctl daemon-reload",
|
|
"systemctl enable --now filebrowser",
|
|
"",
|
|
"# ── SFTP access (password auth, same creds as SMB) ──",
|
|
"usermod -s /bin/bash -d /data/share adam",
|
|
"echo \"$SMB_PASSWORD\" | passwd --stdin adam",
|
|
"cat >> /etc/ssh/sshd_config << 'SSHEOF'",
|
|
"",
|
|
"Match User adam",
|
|
" ForceCommand internal-sftp",
|
|
" PasswordAuthentication yes",
|
|
" AllowTcpForwarding no",
|
|
" X11Forwarding no",
|
|
"SSHEOF",
|
|
"systemctl restart sshd",
|
|
);
|
|
|
|
const instance = new ec2.Instance(this, "Instance", {
|
|
instanceName: "file-share",
|
|
vpc,
|
|
vpcSubnets: { subnets: [privateSubnet1] },
|
|
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL),
|
|
machineImage: ec2.MachineImage.latestAmazonLinux2023({
|
|
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
|
|
// Cache the resolved AMI in cdk.context.json so deploys don't pick up
|
|
// new AL2023 releases implicitly (AMI change forces instance replacement).
|
|
// Refresh deliberately with: cdk context --reset <ami key> && cdk synth
|
|
cachedInContext: true,
|
|
}),
|
|
securityGroup: sg,
|
|
role,
|
|
userData,
|
|
blockDevices: [
|
|
{
|
|
deviceName: "/dev/xvda",
|
|
volume: ec2.BlockDeviceVolume.ebs(20, {
|
|
volumeType: ec2.EbsDeviceVolumeType.GP3,
|
|
encrypted: true,
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
cdk.Tags.of(instance).add("file-share-backup", "true");
|
|
|
|
// The data volume is deliberately UNMANAGED (imported by ID): CloudFormation
|
|
// can attach it but can never create, replace, or delete it. The volume
|
|
// survives any instance replacement. Created 2026-05-27; holds /data/share.
|
|
// Tags (Name, file-share-backup for DLM) are set directly on the volume.
|
|
const dataVolume = ec2.Volume.fromVolumeAttributes(this, "DataVolume", {
|
|
volumeId: "vol-04d951cccacc435b5",
|
|
availabilityZone: "us-east-1a",
|
|
});
|
|
|
|
new ec2.CfnVolumeAttachment(this, "DataVolumeAttachment", {
|
|
instanceId: instance.instanceId,
|
|
volumeId: dataVolume.volumeId,
|
|
device: "/dev/xvdf",
|
|
});
|
|
|
|
const dlmRole = new iam.Role(this, "DlmRole", {
|
|
roleName: "file-share-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 file share data volume",
|
|
state: "ENABLED",
|
|
executionRoleArn: dlmRole.roleArn,
|
|
policyDetails: {
|
|
resourceTypes: ["INSTANCE"],
|
|
targetTags: [{ key: "file-share-backup", value: "true" }],
|
|
schedules: [{
|
|
name: "file-share-nightly",
|
|
createRule: { interval: 24, intervalUnit: "HOURS", times: ["06:00"] },
|
|
retainRule: { count: 30 },
|
|
// copyTags already propagates file-share-backup=true from the volume to each
|
|
// snapshot; an explicit tagsToAdd of the same key triggers DLM's duplicate-tag
|
|
// error ("Tag file-share-backup is already defined") and puts the policy in ERROR.
|
|
copyTags: true,
|
|
}],
|
|
},
|
|
});
|
|
|
|
new cdk.CfnOutput(this, "InstanceId", {
|
|
value: instance.instanceId,
|
|
});
|
|
|
|
new cdk.CfnOutput(this, "PrivateIp", {
|
|
value: instance.instancePrivateIp,
|
|
description: "SMB (smb://<ip>/files), FileBrowser (http://<ip>:8080), SFTP (sftp://<ip>)",
|
|
});
|
|
}
|
|
}
|