mirror of
https://github.com/Sea-Haven-Industries/file-share.git
synced 2026-09-30 17:03:17 +00:00
The SnapshotPolicy schedule set both copyTags:true and tagsToAdd file-share-backup=true. Since that tag is already on the data volume, copyTags propagates it to each snapshot and the explicit tagsToAdd of the same key triggers DLM's duplicate-tag error, leaving the policy in ERROR state -- nightly snapshots of the 500 GB NAS volume were silently failing. Dropping the redundant tagsToAdd (copyTags still tags the snapshots) lets the policy run. The live policy was already corrected out-of-band to stop the failing backups; this commit codifies that change so the next clean deploy shows no drift on the policy.
213 lines
7.5 KiB
TypeScript
213 lines
7.5 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");
|
|
|
|
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)",
|
|
"",
|
|
"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 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: "Use for SMB (smb://<ip>/files) and FileBrowser (http://<ip>:8080)",
|
|
});
|
|
}
|
|
}
|