From 2c459317417ef1004067f5280a96c89d4ed788a6 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:40:05 -0400 Subject: [PATCH] fix(infra): adopt existing data volume + cache AMI to stop replacement churn (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- cdk.context.json | 3 ++- lib/file-share-stack.ts | 52 +++++++++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cdk.context.json b/cdk.context.json index 068ec0c..34db403 100644 --- a/cdk.context.json +++ b/cdk.context.json @@ -43,5 +43,6 @@ ] } ] - } + }, + "ssm:account=328440206208:parameterName=/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64:region=us-east-1": "ami-0b183bb1259186479" } diff --git a/lib/file-share-stack.ts b/lib/file-share-stack.ts index 1d955a8..471f1a5 100644 --- a/lib/file-share-stack.ts +++ b/lib/file-share-stack.ts @@ -30,6 +30,7 @@ export class FileShareStack extends cdk.Stack { 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", @@ -50,7 +51,11 @@ export class FileShareStack extends cdk.Stack { userData.addCommands( "set -euxo pipefail", "", - "# ── Data volume ──", + "# ── 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\"", @@ -139,6 +144,19 @@ export class FileShareStack extends cdk.Stack { "", "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", { @@ -148,6 +166,10 @@ export class FileShareStack extends cdk.Stack { 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 && cdk synth + cachedInContext: true, }), securityGroup: sg, role, @@ -160,18 +182,26 @@ export class FileShareStack extends cdk.Stack { encrypted: true, }), }, - { - deviceName: "/dev/xvdf", - volume: ec2.BlockDeviceVolume.ebs(500, { - 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"), @@ -193,8 +223,10 @@ export class FileShareStack extends cdk.Stack { 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, - tagsToAdd: [{ key: "file-share-backup", value: "true" }], }], }, }); @@ -205,7 +237,7 @@ export class FileShareStack extends cdk.Stack { new cdk.CfnOutput(this, "PrivateIp", { value: instance.instancePrivateIp, - description: "Use for SMB (smb:///files) and FileBrowser (http://:8080)", + description: "SMB (smb:///files), FileBrowser (http://:8080), SFTP (sftp://)", }); } }