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 route53 from "aws-cdk-lib/aws-route53"; import * as dlm from "aws-cdk-lib/aws-dlm"; import { Construct } from "constructs"; const FORGEJO_VERSION = "10.0.1"; export class ForgejoStack 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: "forgejo", description: "Forgejo git server - VPC and VPN access", allowAllOutbound: true, }); sg.addIngressRule(ec2.Peer.ipv4("10.20.0.0/16"), ec2.Port.tcp(3000), "HTTP from VPC"); sg.addIngressRule(ec2.Peer.ipv4("10.10.0.0/16"), ec2.Port.tcp(3000), "HTTP from office VPN"); sg.addIngressRule(ec2.Peer.ipv4("10.20.0.0/16"), ec2.Port.tcp(2222), "SSH git from VPC"); sg.addIngressRule(ec2.Peer.ipv4("10.10.0.0/16"), ec2.Port.tcp(2222), "SSH git from office VPN"); const role = new iam.Role(this, "InstanceRole", { roleName: "forgejo-instance", assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"), ], }); const userData = ec2.UserData.forLinux(); userData.addCommands( "set -euxo pipefail", "", `FORGEJO_VERSION="${FORGEJO_VERSION}"`, "", "dnf install -y git", "", "useradd --system --shell /bin/bash --home-dir /home/forgejo --create-home forgejo", "", "mkdir -p /var/lib/forgejo/{data,log}", "chown -R forgejo:forgejo /var/lib/forgejo", "chmod 750 /var/lib/forgejo", "", 'curl -Lo /usr/local/bin/forgejo "https://codeberg.org/forgejo/forgejo/releases/download/v${FORGEJO_VERSION}/forgejo-${FORGEJO_VERSION}-linux-arm64"', "chmod +x /usr/local/bin/forgejo", "", "mkdir -p /etc/forgejo", "chown root:forgejo /etc/forgejo", "chmod 770 /etc/forgejo", "", "cat > /etc/forgejo/app.ini << 'INIEOF'", "APP_NAME = Sea Haven Git", "", "[server]", "DOMAIN = forgejo.seahaven.com", "ROOT_URL = http://forgejo.seahaven.com:3000/", "HTTP_PORT = 3000", "START_SSH_SERVER = true", "SSH_PORT = 2222", "SSH_LISTEN_PORT = 2222", "LFS_START_SERVER = true", "", "[database]", "DB_TYPE = sqlite3", "PATH = /var/lib/forgejo/data/forgejo.db", "", "[repository]", "ROOT = /var/lib/forgejo/data/repositories", "", "[log]", "ROOT_PATH = /var/lib/forgejo/log", "", "[security]", "INSTALL_LOCK = true", "", "[service]", "DISABLE_REGISTRATION = true", "", "[mirror]", "DEFAULT_INTERVAL = 1h", "INIEOF", "", "chown root:forgejo /etc/forgejo/app.ini", "chmod 660 /etc/forgejo/app.ini", "", "cat > /etc/systemd/system/forgejo.service << 'SVCEOF'", "[Unit]", "Description=Forgejo", "After=network.target", "", "[Service]", "Type=simple", "User=forgejo", "Group=forgejo", "WorkingDirectory=/var/lib/forgejo", "ExecStart=/usr/local/bin/forgejo web --config /etc/forgejo/app.ini", "Restart=always", "RestartSec=5", "Environment=USER=forgejo HOME=/home/forgejo FORGEJO_WORK_DIR=/var/lib/forgejo", "", "[Install]", "WantedBy=multi-user.target", "SVCEOF", "", "systemctl daemon-reload", "systemctl enable --now forgejo", ); const instance = new ec2.Instance(this, "Instance", { instanceName: "forgejo", 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(50, { volumeType: ec2.EbsDeviceVolumeType.GP3, encrypted: true, }), }], }); cdk.Tags.of(instance).add("forgejo-backup", "true"); const dlmRole = new iam.Role(this, "DlmRole", { roleName: "forgejo-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 Forgejo", state: "ENABLED", executionRoleArn: dlmRole.roleArn, policyDetails: { resourceTypes: ["INSTANCE"], targetTags: [{ key: "forgejo-backup", value: "true" }], schedules: [{ name: "forgejo-nightly", createRule: { interval: 24, intervalUnit: "HOURS", times: ["06:00"] }, retainRule: { count: 7 }, copyTags: true, }], }, }); const hostedZone = route53.HostedZone.fromHostedZoneAttributes( this, "SeaHavenZone", { hostedZoneId: "Z06652411XKH89KTZD3XA", zoneName: "seahaven.com", } ); new route53.ARecord(this, "DnsRecord", { zone: hostedZone, recordName: "forgejo", target: route53.RecordTarget.fromIpAddresses(instance.instancePrivateIp), ttl: cdk.Duration.minutes(5), }); new cdk.CfnOutput(this, "ForgejoUrl", { value: "http://forgejo.seahaven.com:3000", }); new cdk.CfnOutput(this, "InstanceId", { value: instance.instanceId, }); new cdk.CfnOutput(this, "PrivateIp", { value: instance.instancePrivateIp, }); } }