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"); 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 ──", "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)", "FB_HASH=$(filebrowser hash \"$FB_PASSWORD\")", "", "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", ); 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, }), securityGroup: sg, role, userData, blockDevices: [ { deviceName: "/dev/xvda", volume: ec2.BlockDeviceVolume.ebs(20, { volumeType: ec2.EbsDeviceVolumeType.GP3, 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"); 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: true, tagsToAdd: [{ key: "file-share-backup", value: "true" }], }], }, }); new cdk.CfnOutput(this, "InstanceId", { value: instance.instanceId, }); new cdk.CfnOutput(this, "PrivateIp", { value: instance.instancePrivateIp, description: "Use for SMB (smb:///files) and FileBrowser (http://:8080)", }); } }