Skip to content

Commit ccb0b0a

Browse files
authored
Merge pull request #1 from osuAkatsuki/terraform-cx53-migration
Add Terraform for Hetzner Cloud CX53 migration
2 parents a3e1e2b + 51ec3dc commit ccb0b0a

10 files changed

Lines changed: 443 additions & 13 deletions

File tree

.github/workflows/terraform.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Terraform
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths: [tf/**]
7+
pull_request:
8+
branches: [main]
9+
paths: [tf/**]
10+
11+
concurrency:
12+
group: terraform
13+
cancel-in-progress: false
14+
15+
env:
16+
AWS_ACCESS_KEY_ID: ${{ secrets.WASABI_ACCESS_KEY }}
17+
AWS_SECRET_ACCESS_KEY: ${{ secrets.WASABI_SECRET_KEY }}
18+
19+
jobs:
20+
terraform-plan:
21+
name: Terraform Plan
22+
runs-on: ubuntu-latest
23+
outputs:
24+
tfplanExitCode: ${{ steps.tf-plan.outputs.exitcode }}
25+
26+
steps:
27+
- uses: actions/checkout@v4
28+
29+
- uses: hashicorp/setup-terraform@v3
30+
with:
31+
terraform_wrapper: false
32+
33+
- name: Terraform Init
34+
working-directory: ./tf
35+
run: terraform init
36+
37+
- name: Terraform Format
38+
working-directory: ./tf
39+
run: terraform fmt -check
40+
41+
- name: Terraform Plan
42+
id: tf-plan
43+
working-directory: ./tf
44+
env:
45+
TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
46+
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
47+
TF_VAR_cloudflare_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
48+
TF_VAR_ssh_public_key: ${{ secrets.SSH_PUBLIC_KEY }}
49+
run: |
50+
export exitcode=0
51+
terraform plan \
52+
-lock-timeout=5m \
53+
-detailed-exitcode \
54+
-no-color \
55+
-out tfplan \
56+
|| export exitcode=$?
57+
58+
echo "exitcode=$exitcode" >> "$GITHUB_OUTPUT"
59+
60+
if [ "$exitcode" -eq 1 ]; then
61+
echo "Terraform Plan Failed!"
62+
exit 1
63+
else
64+
exit 0
65+
fi
66+
67+
- uses: actions/upload-artifact@v4
68+
with:
69+
name: tfplan
70+
path: tf/tfplan
71+
72+
- name: Create Plan Summary
73+
id: tf-plan-string
74+
working-directory: ./tf
75+
run: |
76+
TERRAFORM_PLAN=$(terraform show -no-color tfplan)
77+
78+
delimiter="$(openssl rand -hex 8)"
79+
{
80+
echo "summary<<${delimiter}"
81+
echo "## Terraform Plan Output"
82+
echo "<details><summary>Click to expand</summary>"
83+
echo ""
84+
echo '```terraform'
85+
echo "$TERRAFORM_PLAN"
86+
echo '```'
87+
echo "</details>"
88+
echo "${delimiter}"
89+
} >> "$GITHUB_OUTPUT"
90+
91+
- name: Publish Plan to Summary
92+
env:
93+
SUMMARY: ${{ steps.tf-plan-string.outputs.summary }}
94+
run: echo "$SUMMARY" >> "$GITHUB_STEP_SUMMARY"
95+
96+
- name: Comment Plan on PR
97+
if: github.event_name == 'pull_request'
98+
uses: actions/github-script@v7
99+
env:
100+
SUMMARY: "${{ steps.tf-plan-string.outputs.summary }}"
101+
with:
102+
github-token: ${{ secrets.GITHUB_TOKEN }}
103+
script: |
104+
const body = process.env.SUMMARY;
105+
github.rest.issues.createComment({
106+
issue_number: context.issue.number,
107+
owner: context.repo.owner,
108+
repo: context.repo.repo,
109+
body: body
110+
})
111+
112+
terraform-apply:
113+
name: Terraform Apply
114+
if: github.ref == 'refs/heads/main' && needs.terraform-plan.outputs.tfplanExitCode == 2
115+
runs-on: ubuntu-latest
116+
needs: [terraform-plan]
117+
118+
steps:
119+
- uses: actions/checkout@v4
120+
121+
- uses: hashicorp/setup-terraform@v3
122+
123+
- name: Terraform Init
124+
working-directory: ./tf
125+
run: terraform init
126+
127+
- uses: actions/download-artifact@v4
128+
with:
129+
name: tfplan
130+
path: tf
131+
132+
- name: Terraform Apply
133+
working-directory: ./tf
134+
run: terraform apply -auto-approve -lock-timeout=5m tfplan

config/mysql/99-akatsuki.cnf

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
[mysqld]
22
# Networking
33
bind-address = 0.0.0.0
4-
max_connections = 1000
4+
max_connections = 500
55

6-
# InnoDB - 40GB buffer pool for 64GB RAM system
7-
innodb_buffer_pool_size = 42949672960
8-
innodb_buffer_pool_instances = 8
6+
# InnoDB - 20GB buffer pool for 32GB RAM system (CX53 VPS)
7+
innodb_buffer_pool_size = 21474836480
8+
innodb_buffer_pool_instances = 4
99
innodb_flush_log_at_trx_commit = 1
1010
innodb_flush_method = O_DIRECT
1111

1212
# Authentication compatibility (services use mysql_native_password)
1313
default_authentication_plugin = mysql_native_password
1414

15-
# I/O tuning for NVMe
16-
innodb_io_capacity = 10000
17-
innodb_io_capacity_max = 20000
18-
innodb_read_io_threads = 8
19-
innodb_write_io_threads = 8
15+
# I/O tuning for VPS NVMe (~18K IOPS)
16+
innodb_io_capacity = 2000
17+
innodb_io_capacity_max = 4000
18+
innodb_read_io_threads = 4
19+
innodb_write_io_threads = 4
2020

2121
# Redo log (replaces deprecated innodb_log_file_size in 8.0.30+)
22-
innodb_redo_log_capacity = 2G
22+
innodb_redo_log_capacity = 1G
2323

2424
# Temp tables
25-
tmp_table_size = 256M
26-
max_heap_table_size = 256M
25+
tmp_table_size = 128M
26+
max_heap_table_size = 128M
2727

2828
# Binary logging
2929
log_bin = /var/log/mysql/mysql-bin.log

setup.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
# Hetzner AX42-U server bootstrap script
4+
# Hetzner Cloud CX53 server bootstrap script
55
# Installs and configures all services for Akatsuki production
66

7+
echo "=== Creating swap file (4GB) ==="
8+
fallocate -l 4G /swapfile
9+
chmod 600 /swapfile
10+
mkswap /swapfile
11+
swapon /swapfile
12+
echo '/swapfile none swap sw 0 0' >> /etc/fstab
13+
714
echo "=== Installing system packages ==="
815
apt-get update
916
DEBIAN_FRONTEND=noninteractive apt-get install -y \

tf/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.terraform/
2+
*.tfstate
3+
*.tfstate.backup
4+
terraform.tfvars
5+
.terraform.lock.hcl

tf/cloudflare.tf

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Phase 2: Uncomment after data migration is complete and records are imported.
2+
#
3+
# Import existing records first to avoid duplicates:
4+
# terraform import cloudflare_record.apex <record_id>
5+
# terraform import 'cloudflare_record.cname["a"]' <record_id>
6+
# ... (for each CNAME and MX record)
7+
#
8+
# Get record IDs with:
9+
# curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
10+
# "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" | jq '.result[]'
11+
12+
# resource "cloudflare_record" "apex" {
13+
# zone_id = var.cloudflare_zone_id
14+
# name = "akatsuki.gg"
15+
# type = "A"
16+
# value = hcloud_server.production.ipv4_address
17+
# proxied = true
18+
# ttl = 1
19+
# }
20+
#
21+
# locals {
22+
# cname_subdomains = [
23+
# "a",
24+
# "air_conditioning",
25+
# "assets",
26+
# "b",
27+
# "beatmaps",
28+
# "c",
29+
# "difficulty",
30+
# "old",
31+
# "osu",
32+
# "payments",
33+
# "performance",
34+
# "relax",
35+
# "rework",
36+
# "reworks",
37+
# "s",
38+
# "vault",
39+
# "www",
40+
# ]
41+
# }
42+
#
43+
# resource "cloudflare_record" "cname" {
44+
# for_each = toset(local.cname_subdomains)
45+
#
46+
# zone_id = var.cloudflare_zone_id
47+
# name = each.value
48+
# type = "CNAME"
49+
# value = "akatsuki.gg"
50+
# proxied = true
51+
# ttl = 1
52+
# }
53+
#
54+
# resource "cloudflare_record" "mx_primary" {
55+
# zone_id = var.cloudflare_zone_id
56+
# name = "akatsuki.gg"
57+
# type = "MX"
58+
# value = "aspmx.l.google.com"
59+
# priority = 1
60+
# proxied = false
61+
# ttl = 1
62+
# }
63+
#
64+
# resource "cloudflare_record" "mx_alt1" {
65+
# zone_id = var.cloudflare_zone_id
66+
# name = "akatsuki.gg"
67+
# type = "MX"
68+
# value = "alt1.aspmx.l.google.com"
69+
# priority = 5
70+
# proxied = false
71+
# ttl = 1
72+
# }
73+
#
74+
# resource "cloudflare_record" "mx_alt2" {
75+
# zone_id = var.cloudflare_zone_id
76+
# name = "akatsuki.gg"
77+
# type = "MX"
78+
# value = "alt2.aspmx.l.google.com"
79+
# priority = 5
80+
# proxied = false
81+
# ttl = 1
82+
# }
83+
#
84+
# resource "cloudflare_record" "mx_alt3" {
85+
# zone_id = var.cloudflare_zone_id
86+
# name = "akatsuki.gg"
87+
# type = "MX"
88+
# value = "alt3.aspmx.l.google.com"
89+
# priority = 10
90+
# proxied = false
91+
# ttl = 1
92+
# }
93+
#
94+
# resource "cloudflare_record" "mx_alt4" {
95+
# zone_id = var.cloudflare_zone_id
96+
# name = "akatsuki.gg"
97+
# type = "MX"
98+
# value = "alt4.aspmx.l.google.com"
99+
# priority = 10
100+
# proxied = false
101+
# ttl = 1
102+
# }
103+
#
104+
# resource "cloudflare_record" "mx_verification" {
105+
# zone_id = var.cloudflare_zone_id
106+
# name = "akatsuki.gg"
107+
# type = "MX"
108+
# value = "3h5azgn53tixa3a2yxyqkgyethll22hdjl7jj5jshsfw2wpalkhq.mx-verification.google.com"
109+
# priority = 15
110+
# proxied = false
111+
# ttl = 1
112+
# }

tf/outputs.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
output "server_ip" {
2+
description = "Public IPv4 address of the production server"
3+
value = hcloud_server.production.ipv4_address
4+
}
5+
6+
output "server_status" {
7+
description = "Server status"
8+
value = hcloud_server.production.status
9+
}

tf/provider.tf

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
terraform {
2+
backend "s3" {
3+
bucket = "akatsuki-terraform-state"
4+
key = "server-infra/terraform.tfstate"
5+
region = "ca-central-1"
6+
7+
endpoints = {
8+
s3 = "https://s3.ca-central-1.wasabisys.com"
9+
}
10+
11+
# Wasabi doesn't support these S3 features
12+
skip_credentials_validation = true
13+
skip_metadata_api_check = true
14+
skip_requesting_account_id = true
15+
skip_region_validation = true
16+
use_path_style = true
17+
}
18+
19+
required_providers {
20+
hcloud = {
21+
source = "hetznercloud/hcloud"
22+
version = "~> 1.49"
23+
}
24+
cloudflare = {
25+
source = "cloudflare/cloudflare"
26+
version = "~> 4"
27+
}
28+
}
29+
}
30+
31+
provider "hcloud" {
32+
token = var.hcloud_token
33+
}
34+
35+
provider "cloudflare" {
36+
api_token = var.cloudflare_api_token
37+
}

0 commit comments

Comments
 (0)