forgejo/lib/forgejo-stack.ts

372 lines
14 KiB
TypeScript
Raw Normal View History

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 s3 from "aws-cdk-lib/aws-s3";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as elbv2_targets from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as route53Targets from "aws-cdk-lib/aws-route53-targets";
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,
});
const albSg = ec2.SecurityGroup.fromSecurityGroupId(
this, "AlbSg", "sg-0b0301deed193258a",
{ allowAllOutbound: false }
);
albSg.addEgressRule(sg, ec2.Port.tcp(3000), "Forgejo HTTP");
sg.addIngressRule(albSg, ec2.Port.tcp(3000), "HTTP from ALB");
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"),
],
});
role.addToPolicy(new iam.PolicyStatement({
actions: ["secretsmanager:GetSecretValue"],
resources: [
`arn:aws:secretsmanager:us-east-1:328440206208:secret:forgejo/*`,
],
}));
const backupBucket = new s3.Bucket(this, "BackupBucket", {
bucketName: "forgejo-backups-328440206208",
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [{
transitions: [
{ storageClass: s3.StorageClass.GLACIER, transitionAfter: cdk.Duration.days(30) },
],
expiration: cdk.Duration.days(365),
}],
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
backupBucket.grantReadWrite(role);
const userData = ec2.UserData.forLinux();
userData.addCommands(
"set -euxo pipefail",
"",
`FORGEJO_VERSION="${FORGEJO_VERSION}"`,
"",
"dnf install -y git cronie",
"systemctl enable --now crond",
"",
"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 = https://forgejo.seahaven.com/",
"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",
"",
"cat > /usr/local/bin/forgejo-backup.sh << 'BAKEOF'",
"#!/bin/bash",
"set -euo pipefail",
"TIMESTAMP=$(date +%Y-%m-%d)",
"DUMP_DIR=$(mktemp -d)",
"chown forgejo:forgejo \"$DUMP_DIR\"",
"cd \"$DUMP_DIR\"",
"sudo -u forgejo /usr/local/bin/forgejo dump --config /etc/forgejo/app.ini --type tar.gz --file \"$DUMP_DIR/forgejo-${TIMESTAMP}.tar.gz\"",
"aws s3 cp \"$DUMP_DIR/forgejo-${TIMESTAMP}.tar.gz\" s3://forgejo-backups-328440206208/${TIMESTAMP}/forgejo-${TIMESTAMP}.tar.gz",
"rm -rf \"$DUMP_DIR\"",
"BAKEOF",
"chmod +x /usr/local/bin/forgejo-backup.sh",
"",
"echo '0 5 * * * root /usr/local/bin/forgejo-backup.sh >> /var/log/forgejo-backup.log 2>&1' > /etc/cron.d/forgejo-backup",
"chmod 644 /etc/cron.d/forgejo-backup",
"",
"cat > /usr/local/bin/forgejo-autodiscover.sh << 'ADEOF'",
"#!/bin/bash",
"set -euo pipefail",
"GH_PAT=$(aws secretsmanager get-secret-value --secret-id forgejo/github-pat --query SecretString --output text --region us-east-1)",
"FORGEJO_TOKEN=$(aws secretsmanager get-secret-value --secret-id forgejo/api-token --query SecretString --output text --region us-east-1)",
"FORGEJO_URL=https://forgejo.seahaven.com/api/v1",
"GH_ORG=Sea-Haven-Industries",
"",
"gh_repos=$(curl -sf -H \"Authorization: token $GH_PAT\" \"https://api.github.com/orgs/$GH_ORG/repos?per_page=100&type=all\" | python3 -c \"",
"import sys, json",
"for r in json.load(sys.stdin):",
" print(f\\\"{r['name']}\\\\t{r['archived']}\\\")",
"\")",
"",
"forgejo_repos=$(curl -sf -H \"Authorization: token $FORGEJO_TOKEN\" \"$FORGEJO_URL/repos/search?limit=100\" | python3 -c \"",
"import sys, json",
"data = json.load(sys.stdin)",
"repos = data.get('data', data) if isinstance(data, dict) else data",
"for r in repos:",
" print(r['name'])",
"\")",
"",
"while IFS=$'\\t' read -r name archived; do",
" if ! echo \"$forgejo_repos\" | grep -qx \"$name\"; then",
" mirror=true",
" [ \"$archived\" = \"True\" ] && mirror=false",
" echo \"$(date -Is) Discovering: $name (mirror=$mirror)\"",
" curl -sf -X POST \"$FORGEJO_URL/repos/migrate\" \\",
" -H \"Authorization: token $FORGEJO_TOKEN\" \\",
" -H \"Content-Type: application/json\" \\",
" -d \"{",
" \\\"clone_addr\\\": \\\"https://github.com/$GH_ORG/${name}.git\\\",",
" \\\"auth_token\\\": \\\"${GH_PAT}\\\",",
" \\\"repo_name\\\": \\\"${name}\\\",",
" \\\"repo_owner\\\": \\\"adam\\\",",
" \\\"service\\\": \\\"github\\\",",
" \\\"mirror\\\": ${mirror},",
" \\\"issues\\\": true,",
" \\\"labels\\\": true,",
" \\\"milestones\\\": true,",
" \\\"pull_requests\\\": true,",
" \\\"releases\\\": true,",
" \\\"wiki\\\": true",
" }\" > /dev/null",
" fi",
"done <<< \"$gh_repos\"",
"ADEOF",
"chmod +x /usr/local/bin/forgejo-autodiscover.sh",
"",
"cat > /usr/local/bin/forgejo-refresh-tokens.sh << 'RTEOF'",
"#!/bin/bash",
"set -euo pipefail",
"GH_PAT=$(aws secretsmanager get-secret-value --secret-id forgejo/github-pat --query SecretString --output text --region us-east-1)",
"FORGEJO_TOKEN=$(aws secretsmanager get-secret-value --secret-id forgejo/api-token --query SecretString --output text --region us-east-1)",
"FORGEJO_URL=https://forgejo.seahaven.com/api/v1",
"REPO_ROOT=/var/lib/forgejo/data/repositories/adam",
"",
"export GIT_CONFIG_COUNT=1",
"export GIT_CONFIG_KEY_0=safe.directory",
"export GIT_CONFIG_VALUE_0='*'",
"",
"mirrors=$(curl -sf -H \"Authorization: token $FORGEJO_TOKEN\" \"$FORGEJO_URL/repos/search?limit=100\" | python3 -c \"",
"import sys, json",
"data = json.load(sys.stdin)",
"repos = data.get('data', data) if isinstance(data, dict) else data",
"for r in repos:",
" if r.get('mirror', False):",
" print(r['name'])",
"\")",
"",
"while read -r repo_name; do",
" [ -z \"$repo_name\" ] && continue",
" repo_dir=\"$REPO_ROOT/${repo_name}.git\"",
" if [ -d \"$repo_dir\" ]; then",
" new_url=\"https://${GH_PAT}@github.com/Sea-Haven-Industries/${repo_name}.git\"",
" git -C \"$repo_dir\" remote set-url origin \"$new_url\" 2>/dev/null && echo \"$(date -Is) Refreshed: $repo_name\"",
" fi",
"done <<< \"$mirrors\"",
"RTEOF",
"chmod +x /usr/local/bin/forgejo-refresh-tokens.sh",
"",
"printf '%s\\n' '0 * * * * root /usr/local/bin/forgejo-autodiscover.sh >> /var/log/forgejo-autodiscover.log 2>&1' > /etc/cron.d/forgejo-autodiscover",
"printf '%s\\n' '30 4 * * * root /usr/local/bin/forgejo-refresh-tokens.sh >> /var/log/forgejo-refresh-tokens.log 2>&1' > /etc/cron.d/forgejo-refresh-tokens",
"chmod 644 /etc/cron.d/forgejo-autodiscover /etc/cron.d/forgejo-refresh-tokens",
);
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 alb = elbv2.ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(
this, "Alb", {
loadBalancerArn:
"arn:aws:elasticloadbalancing:us-east-1:328440206208:loadbalancer/app/seahaven-com/222c3257354ab559",
securityGroupId: "sg-0b0301deed193258a",
loadBalancerDnsName: "seahaven-com-1856441924.us-east-1.elb.amazonaws.com",
loadBalancerCanonicalHostedZoneId: "Z35SXDOTRQ7X7K",
}
);
const httpsListener = elbv2.ApplicationListener.fromApplicationListenerAttributes(
this, "HttpsListener", {
listenerArn:
"arn:aws:elasticloadbalancing:us-east-1:328440206208:listener/app/seahaven-com/222c3257354ab559/bab8bcf0da0e2927",
securityGroup: albSg,
}
);
const targetGroup = new elbv2.ApplicationTargetGroup(this, "TargetGroup", {
targetGroupName: "forgejo",
vpc,
port: 3000,
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.INSTANCE,
healthCheck: {
path: "/",
healthyHttpCodes: "200,302",
},
targets: [new elbv2_targets.InstanceIdTarget(instance.instanceId, 3000)],
});
new elbv2.ApplicationListenerRule(this, "ListenerRule", {
listener: httpsListener,
priority: 4,
conditions: [elbv2.ListenerCondition.hostHeaders(["forgejo.seahaven.com"])],
targetGroups: [targetGroup],
});
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(
this, "SeaHavenZone", {
hostedZoneId: "Z06652411XKH89KTZD3XA",
zoneName: "seahaven.com",
}
);
new route53.ARecord(this, "DnsRecord", {
zone: hostedZone,
recordName: "forgejo",
target: route53.RecordTarget.fromAlias(
new route53Targets.LoadBalancerTarget(alb)
),
});
new cdk.CfnOutput(this, "ForgejoUrl", {
value: "https://forgejo.seahaven.com",
});
new cdk.CfnOutput(this, "InstanceId", {
value: instance.instanceId,
});
new cdk.CfnOutput(this, "PrivateIp", {
value: instance.instancePrivateIp,
});
}
}