Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions .github/workflows/terraform.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Terraform

on:
push:
branches: [main]
paths: [tf/**]
pull_request:
branches: [main]
paths: [tf/**]

concurrency:
group: terraform
cancel-in-progress: false

env:
AWS_ACCESS_KEY_ID: ${{ secrets.WASABI_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.WASABI_SECRET_KEY }}

jobs:
terraform-plan:
name: Terraform Plan
runs-on: ubuntu-latest
outputs:
tfplanExitCode: ${{ steps.tf-plan.outputs.exitcode }}

steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

- name: Terraform Init
working-directory: ./tf
run: terraform init

- name: Terraform Format
working-directory: ./tf
run: terraform fmt -check

- name: Terraform Plan
id: tf-plan
working-directory: ./tf
env:
TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_cloudflare_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
TF_VAR_ssh_public_key: ${{ secrets.SSH_PUBLIC_KEY }}
run: |
export exitcode=0
terraform plan \
-lock-timeout=5m \
-detailed-exitcode \
-no-color \
-out tfplan \
|| export exitcode=$?

echo "exitcode=$exitcode" >> "$GITHUB_OUTPUT"

if [ "$exitcode" -eq 1 ]; then
echo "Terraform Plan Failed!"
exit 1
else
exit 0
fi

- uses: actions/upload-artifact@v4
with:
name: tfplan
path: tf/tfplan

- name: Create Plan Summary
id: tf-plan-string
working-directory: ./tf
run: |
TERRAFORM_PLAN=$(terraform show -no-color tfplan)

delimiter="$(openssl rand -hex 8)"
{
echo "summary<<${delimiter}"
echo "## Terraform Plan Output"
echo "<details><summary>Click to expand</summary>"
echo ""
echo '```terraform'
echo "$TERRAFORM_PLAN"
echo '```'
echo "</details>"
echo "${delimiter}"
} >> "$GITHUB_OUTPUT"

- name: Publish Plan to Summary
env:
SUMMARY: ${{ steps.tf-plan-string.outputs.summary }}
run: echo "$SUMMARY" >> "$GITHUB_STEP_SUMMARY"

- name: Comment Plan on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
SUMMARY: "${{ steps.tf-plan-string.outputs.summary }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const body = process.env.SUMMARY;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
})

terraform-apply:
name: Terraform Apply
if: github.ref == 'refs/heads/main' && needs.terraform-plan.outputs.tfplanExitCode == 2
runs-on: ubuntu-latest
needs: [terraform-plan]

steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3

- name: Terraform Init
working-directory: ./tf
run: terraform init

- uses: actions/download-artifact@v4
with:
name: tfplan
path: tf

- name: Terraform Apply
working-directory: ./tf
run: terraform apply -auto-approve -lock-timeout=5m tfplan
24 changes: 12 additions & 12 deletions config/mysql/99-akatsuki.cnf
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
[mysqld]
# Networking
bind-address = 0.0.0.0
max_connections = 1000
max_connections = 500

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

# Authentication compatibility (services use mysql_native_password)
default_authentication_plugin = mysql_native_password

# I/O tuning for NVMe
innodb_io_capacity = 10000
innodb_io_capacity_max = 20000
innodb_read_io_threads = 8
innodb_write_io_threads = 8
# I/O tuning for VPS NVMe (~18K IOPS)
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_read_io_threads = 4
innodb_write_io_threads = 4

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

# Temp tables
tmp_table_size = 256M
max_heap_table_size = 256M
tmp_table_size = 128M
max_heap_table_size = 128M

# Binary logging
log_bin = /var/log/mysql/mysql-bin.log
Expand Down
9 changes: 8 additions & 1 deletion setup.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail

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

echo "=== Creating swap file (4GB) ==="
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

echo "=== Installing system packages ==="
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y \
Expand Down
5 changes: 5 additions & 0 deletions tf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.terraform/
*.tfstate
*.tfstate.backup
terraform.tfvars
.terraform.lock.hcl
112 changes: 112 additions & 0 deletions tf/cloudflare.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Phase 2: Uncomment after data migration is complete and records are imported.
#
# Import existing records first to avoid duplicates:
# terraform import cloudflare_record.apex <record_id>
# terraform import 'cloudflare_record.cname["a"]' <record_id>
# ... (for each CNAME and MX record)
#
# Get record IDs with:
# curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
# "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" | jq '.result[]'

# resource "cloudflare_record" "apex" {
# zone_id = var.cloudflare_zone_id
# name = "akatsuki.gg"
# type = "A"
# value = hcloud_server.production.ipv4_address
# proxied = true
# ttl = 1
# }
#
# locals {
# cname_subdomains = [
# "a",
# "air_conditioning",
# "assets",
# "b",
# "beatmaps",
# "c",
# "difficulty",
# "old",
# "osu",
# "payments",
# "performance",
# "relax",
# "rework",
# "reworks",
# "s",
# "vault",
# "www",
# ]
# }
#
# resource "cloudflare_record" "cname" {
# for_each = toset(local.cname_subdomains)
#
# zone_id = var.cloudflare_zone_id
# name = each.value
# type = "CNAME"
# value = "akatsuki.gg"
# proxied = true
# ttl = 1
# }
#
# resource "cloudflare_record" "mx_primary" {
# zone_id = var.cloudflare_zone_id
# name = "akatsuki.gg"
# type = "MX"
# value = "aspmx.l.google.com"
# priority = 1
# proxied = false
# ttl = 1
# }
#
# resource "cloudflare_record" "mx_alt1" {
# zone_id = var.cloudflare_zone_id
# name = "akatsuki.gg"
# type = "MX"
# value = "alt1.aspmx.l.google.com"
# priority = 5
# proxied = false
# ttl = 1
# }
#
# resource "cloudflare_record" "mx_alt2" {
# zone_id = var.cloudflare_zone_id
# name = "akatsuki.gg"
# type = "MX"
# value = "alt2.aspmx.l.google.com"
# priority = 5
# proxied = false
# ttl = 1
# }
#
# resource "cloudflare_record" "mx_alt3" {
# zone_id = var.cloudflare_zone_id
# name = "akatsuki.gg"
# type = "MX"
# value = "alt3.aspmx.l.google.com"
# priority = 10
# proxied = false
# ttl = 1
# }
#
# resource "cloudflare_record" "mx_alt4" {
# zone_id = var.cloudflare_zone_id
# name = "akatsuki.gg"
# type = "MX"
# value = "alt4.aspmx.l.google.com"
# priority = 10
# proxied = false
# ttl = 1
# }
#
# resource "cloudflare_record" "mx_verification" {
# zone_id = var.cloudflare_zone_id
# name = "akatsuki.gg"
# type = "MX"
# value = "3h5azgn53tixa3a2yxyqkgyethll22hdjl7jj5jshsfw2wpalkhq.mx-verification.google.com"
# priority = 15
# proxied = false
# ttl = 1
# }
9 changes: 9 additions & 0 deletions tf/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
output "server_ip" {
description = "Public IPv4 address of the production server"
value = hcloud_server.production.ipv4_address
}

output "server_status" {
description = "Server status"
value = hcloud_server.production.status
}
37 changes: 37 additions & 0 deletions tf/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
terraform {
backend "s3" {
bucket = "akatsuki-terraform-state"
key = "server-infra/terraform.tfstate"
region = "ca-central-1"

endpoints = {
s3 = "https://s3.ca-central-1.wasabisys.com"
}

# Wasabi doesn't support these S3 features
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
skip_region_validation = true
use_path_style = true
}

required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.49"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4"
}
}
}

provider "hcloud" {
token = var.hcloud_token
}

provider "cloudflare" {
api_token = var.cloudflare_api_token
}
Loading