diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 0000000..fd83ef5 --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1,32 @@ +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +*tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/infra/.terraform.lock.hcl b/infra/.terraform.lock.hcl new file mode 100644 index 0000000..2417eba --- /dev/null +++ b/infra/.terraform.lock.hcl @@ -0,0 +1,126 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/aiven/aiven" { + version = "4.52.0" + constraints = "4.52.0" + hashes = [ + "h1:dLC5KaPjLv+9JAv3iciQ/5QyWs2cLHma0/Nzql8MSRs=", + "zh:3861adbc2af6c20c3d9a009deb72665299c0b052345c899e02d70dd4d75e9836", + "zh:3bcf8018fe2d6aabee0d0ff4fa991aa830590ff4337445e49d12636bab5cc29e", + "zh:472c048c118b9311358d4ce14137351e24af2079cf9dfa5da81be6a09bf46de2", + "zh:5dfb9922b1b5c253accd9c7609614bff50224887fb6296a9bb0e57653bacb2e7", + "zh:65a27c83e65e2dfd0d64637355c4aeee0949a639c1cb8a9ce573fc06afa1ebbe", + "zh:7a5f98c363ae3d0a5af4be7e8770d4f721c3e81c5664c626b47ed623cac5bdc6", + "zh:88bde4f9f0a8ef0a2e92e0c737577595c47270792e93351f9bca1df16b5f4620", + "zh:8bf1277959019b614ad1f0f5f9d2e2512b27c5dcdf0440f93bce202900419b44", + "zh:8d2035e6e37fb7a1f7d400a4f13bde7c7c0301aafc322ea8d6d1c9029ab1bf7b", + "zh:995c7e3cf65098af2217c4e7ca41c5c7e9514a03ed2b5183748d01de8a97340a", + "zh:a637c8a87fcc579a12e6018a7138608a7d41945a4f1ea4918a97b60c952ab278", + "zh:a71e4728c2a804120b441c607282240a3849ca4048846478255eb6ea278ce103", + "zh:edc7d97576fb5a6a720303bcefaadb32050bae563b47b426ca00cfb1ed8845e4", + "zh:fdc29b6d096ea90e628b6674674247c94f263ffb70954341d67991e61533e0af", + ] +} + +provider "registry.opentofu.org/cloudflare/cloudflare" { + version = "5.19.0-beta.1" + constraints = "5.19.0-beta.1" + hashes = [ + "h1:ww+abkB/1fPvl2+U8MGPI2pmlu0FosxFFbIoT8RhKyI=", + "zh:35bb2678ae60b19fbeb2907f6716e79b30e56a0f84e3f3e48152d0f33b9615e6", + "zh:47e8a190296f81e3a752ad31fc3bac6d71fbace8ba5588ff51722552d997b5ea", + "zh:4ca15c9e280ba454133b9ed9b8ce5ec41fef48237b2b2030b09990e4dd226d1e", + "zh:7135772003b4f652436b6ef35f4f585c553627b73b9458a8f7745e4ce3e6e337", + "zh:84d59a18fd5c2712e890638101b83242463d80d63985b6c9bafa7a7253e3c4e0", + "zh:a0df0a27b752cab9e777852d1e42860890775ec65ae758af834893f8776d309e", + "zh:afd2638778185984b2b608510fcc136d1a3583d9f471c328c724800534c16e2b", + "zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32", + "zh:f94dadaff6f77352933d242c95708c05f91b8a06e1f4394a8a3725b2ad2a730b", + ] +} + +provider "registry.opentofu.org/newrelic/newrelic" { + version = "3.81.0" + constraints = "3.81.0" + hashes = [ + "h1:G2Ie0sSjDLNV4sxodzmH9USaeU1cN8VIlyGqdGfiFU8=", + "zh:0af973805868bcfe093375c5f3572cfdf1d237c28405a7bcde25a2749e392481", + "zh:144e3ac38e15115c1bb5c490d9ed88b5dcbc864d5cb4908e0211affff25fa791", + "zh:1617e14a29ae815aa0a502a23cc69463fcc15b2ae8538ecfa2dabcef2ce9c133", + "zh:445e5bed4f8ef59b602ec6471e6c6849903e4a2b63b376276fa4ca69ea18bee5", + "zh:61206498e9c22726251515f608e0e87f3d6f990b39c24915f4ac84ff78412bce", + "zh:6eaa4ca7b1b5d55f24d1dd8c4b998c17fe424b56a3cb0705d8e43e3046cb361c", + "zh:8a4975dbcbc200375d329e41a584c88d6ece58ab2db857dcfdabd86f8f9d4c19", + "zh:8f834afbb3bf30da27fc8b1b0580184c5dd832a89992d29e6b59565fa6cbcf2e", + "zh:903fbe54028f4f2c1c19dded286867b9d0c54da361215e0e944f6c7fb6e2728a", + "zh:9ed526701509a966fa1d47bf907ee8c297e94702703b05dee0d6cd1f637733e5", + "zh:d60fef0fa44c70abfba5064e789335316481ac3ca1be3a00d1434e951d4ab0fb", + "zh:d9e039c74162ab7e48444ea8a0357f411676aaa19905c55f804c84df296c2944", + "zh:dc11afea5db3ed4bb42406cc1c6725ff01f4d599b0093daa4456862aee5488f7", + "zh:e22d6ea423c7e784b20c6835d2eb57022716fbf0f25cb0cc0d2b61d5cc73ac86", + "zh:e639038f303d8de0d227d36c5f43cef27dd40bde944c218eef84c21cb3060505", + "zh:ef0f54494a0d27d55f06cf983160a743c7892d14d7d7b0539b90f4483f4369ed", + "zh:f999e4e43c9ecf75dc5300741ad473c9a27894b87606c93090736b9a7cd2874e", + "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25", + ] +} + +provider "registry.opentofu.org/tencentcloudstack/tencentcloud" { + version = "1.82.74" + constraints = "1.82.74" + hashes = [ + "h1:LYmsbmzkBC1l8+uZt0J7oREtKX9cVPyCGUTmI7IyvEk=", + "zh:07a1b81f6e83d49d395dbbc3e33a116e720eb5a2b35ba6cf56173ea7729b8f74", + "zh:0bac52b6355512aa2bd8da1b066886382cb23b03b3fe98c4e516354f8a87ae18", + "zh:1822de94e4ca9d953c67ac433e78704d1a9ae43e7515ed4da642dcbefe37e0ff", + "zh:79def4f6c35f16b13b0f6614f21ecaf1ee12e6b05a00f0174ee1cebde63b68d6", + "zh:7ebdd88b2a6108a9676837f30e5eb04bc4956a9c8662f0ca67466a150f9f9dc0", + "zh:9552c9f317a3c8dc83a83607d168e14c3c1fea9ebddbc8bd900751e5b7de703d", + "zh:9cf055e94923b96a634c96a24dff63f9bfe2028b9bc04ddd1af56449add1fa90", + "zh:a76c19b19126a143afbe50c3a6585bfc291766d337289e6a73eabbb74de13ce0", + "zh:b84b1a1bdea4703f8247a921b14d1d9cf1a6a174165bb0dad895c8177bb123f7", + "zh:bf9be2d950de3836306889b30596bc6ae0a11a5a0c8c95573b9efa82f68e5fdf", + "zh:c5fc31bf9068aab9d48132cce35e29fb2b111bd289f18cdd504d0f3d04b8df8b", + "zh:c690a1d9eb68ebfab0f5440093f38bf14e2c238f238744e5e10448ebd4064a5c", + "zh:e465cd49359c8dc63f005f64b53be156611ce7a87bfa638668416698d298e26e", + "zh:ff876a14d9573dd66759192ac7f96c397109692db3775230c36e01811909740b", + ] +} + +provider "registry.opentofu.org/vercel/vercel" { + version = "4.6.1" + constraints = "4.6.1" + hashes = [ + "h1:xCN8oCbSFE0XC2Dqqz1Q0sPE7Pn+23b0JvaZIH0iLPU=", + "zh:240f12e66b5bfbc216b78b38761b4e62ebc5590fd8051d3f681ee2789c82437c", + "zh:34a6a8ecf0a389d9755ea3740fa48b4e12de57595209b26ac0a88aa65f0dd15b", + "zh:3feb6e58cebe1d441fd3caacfc8341e742905d22cceb7189fd0ea16cc8e66a66", + "zh:40826f814d6509a3e17f70425598214da3aead8ad8149821f4394b92fb8f1e83", + "zh:40ab52d0e84df0b98be68585e0be785de11c0e073264b3695eda314afdb62a66", + "zh:504f5e2d0d860ae9a8696a49a8855a3894badd928a5672521af09e13d0d68f10", + "zh:9317aafc789716433b4ad5b1f54ea807cba0faa7262752d298f24ee330503189", + "zh:9a4f4d4027182fea739a6d30e86c1f83f42f819e61344f9dbd84ade7d4fc3a27", + "zh:c2a196e526986546dee3825bb13c26aace145f0af26ac3ddbfe55499d1fd05c9", + "zh:cb69fc86f5ba56e5cd35b8044f56e26f8fe1e99e9c4571f368f834ca52a0c74f", + "zh:d2737c20efb5d5e08f153732a0dfe5685fb3989aa71db95805c921c0a3c77e55", + "zh:ee17a1817bc5220cb35f10feb737ec1bac6aedc31a7ba8c020965a1d1a610764", + "zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597", + "zh:f5798497eb2a7ec6015b5416987228019f9efe9ab05ca6332cf3f64f2cdef729", + "zh:fb984f9e18673acdd72ca4d29bf8bad34376bd9ff3252437e24c770862c85454", + ] +} + +provider "registry.terraform.io/jhoward321/resend" { + version = "0.1.3" + constraints = "0.1.3" + hashes = [ + "h1:EfoTJu8ZDYwSgyHJ0cILBwWhHdUxqiPAaSsIFRqxZ2w=", + "zh:1386c1a76e0b058535f35dd4718ebb4be2fbc6967aae7132bdb65b67b2c8af2a", + "zh:1d4e78bd3eebacf3ee3a1f6c5d635615a893a2d385956e82566775ea6d387c92", + "zh:3995967d8e4796ccdcff64d733589ef11bc4c30cad06ecb1eb45c0b33684a7e8", + "zh:447d7da914070a63aba10127b0eebbb1a507c1787d23873618b16fa702060502", + "zh:8135e0e17b8ee698f3dc76756f974a4b1b3e0a37a9f68cb04b7804bc701319f0", + "zh:9a37fddac3c1705e7916549406abf3a2dcee39d59aa97e76cc70b8f0a25b7a75", + ] +} diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..073344d --- /dev/null +++ b/infra/README.md @@ -0,0 +1,6 @@ +# `git.mari.zip` infrastructure + +This folder contains Terraform (OpenTofu) files for managing the `git.mari.zip` GitArena instance and the required stack. +Use it as reference for deploying your GitArena on your own infrastructure. + +Later down the road, ansible playbooks will be added to auto setup GitArena on the terraform-provisioned cloud instance. diff --git a/infra/cloudflare.tf b/infra/cloudflare.tf new file mode 100644 index 0000000..bbe43f2 --- /dev/null +++ b/infra/cloudflare.tf @@ -0,0 +1,30 @@ +resource "cloudflare_r2_bucket" "artifacts" { + account_id = var.cloudflare_account_id + name = "gitarena" + location = "WEUR" +} + +resource "cloudflare_r2_custom_domain" "artifacts" { + account_id = var.cloudflare_account_id + bucket_name = cloudflare_r2_bucket.artifacts.name + domain = "objects.${var.frontend_domain}" + zone_id = var.cloudflare_zone_id + enabled = true +} + +resource "cloudflare_r2_bucket_cors" "artifacts" { + account_id = var.cloudflare_account_id + bucket_name = cloudflare_r2_bucket.artifacts.name + + rules = [{ + allowed = { + origins = ["https://${var.frontend_domain}", "https://${var.backend_domain}"] + methods = ["GET", "PUT"] + headers = ["*"] + } + expose = { + headers = ["ETag"] + } + max_age_seconds = 3600 + }] +} diff --git a/infra/dns.tf b/infra/dns.tf new file mode 100644 index 0000000..1a489b4 --- /dev/null +++ b/infra/dns.tf @@ -0,0 +1,45 @@ +resource "cloudflare_dns_record" "backend" { + zone_id = var.cloudflare_zone_id + name = var.backend_domain + type = "A" + content = tencentcloud_lighthouse_instance.main.public_addresses[0] + proxied = false + ttl = 1 +} + +resource "cloudflare_dns_record" "frontend" { + zone_id = var.cloudflare_zone_id + name = var.frontend_domain + type = "CNAME" + content = trimsuffix(data.vercel_domain_config.frontend.recommended_cname, ".") + proxied = false + ttl = 600 +} + +resource "cloudflare_dns_record" "mail" { + zone_id = var.cloudflare_zone_id + name = resend_domain.main.spf_mx_record.name + type = resend_domain.main.spf_mx_record.type + content = resend_domain.main.spf_mx_record.value + priority = resend_domain.main.spf_mx_record.priority + proxied = false + ttl = try(tonumber(resend_domain.main.spf_mx_record.ttl), 3600) +} + +resource "cloudflare_dns_record" "mail_txt" { + zone_id = var.cloudflare_zone_id + name = resend_domain.main.spf_txt_record.name + type = resend_domain.main.spf_txt_record.type + content = resend_domain.main.spf_txt_record.value + proxied = false + ttl = try(tonumber(resend_domain.main.spf_txt_record.ttl), 3600) +} + +resource "cloudflare_dns_record" "mail_dkim" { + zone_id = var.cloudflare_zone_id + name = resend_domain.main.dkim_records[0].name + type = resend_domain.main.dkim_records[0].type + content = resend_domain.main.dkim_records[0].value + proxied = false + ttl = try(tonumber(resend_domain.main.dkim_records[0].ttl), 3600) +} diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..11ff629 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,32 @@ +terraform { + required_version = ">= 1.11" + + required_providers { + tencentcloud = { + source = "tencentcloudstack/tencentcloud" + version = "1.82.74" + } + cloudflare = { + source = "cloudflare/cloudflare" + version = "5.19.0-beta.1" + } + aiven = { + source = "aiven/aiven" + version = "4.52.0" + } + vercel = { + source = "vercel/vercel" + version = "4.6.1" + } + resend = { + source = "registry.terraform.io/jhoward321/resend" + version = "0.1.3" + } + newrelic = { + source = "newrelic/newrelic" + version = "3.81.0" + } + } +} + + diff --git a/infra/postgres.tf b/infra/postgres.tf new file mode 100644 index 0000000..46680b3 --- /dev/null +++ b/infra/postgres.tf @@ -0,0 +1,17 @@ +resource "aiven_pg" "main" { + project = "gitarena" + service_name = "gitarena" + cloud_name = "do-ams" + plan = "free-1-1gb" + + termination_protection = true + + pg_user_config { + pg_version = "17" + + ip_filter_string = [ + "${tencentcloud_lighthouse_instance.main.public_addresses[0]}/32", + var.local_ip_block, + ] + } +} diff --git a/infra/providers.tf b/infra/providers.tf new file mode 100644 index 0000000..b000880 --- /dev/null +++ b/infra/providers.tf @@ -0,0 +1,28 @@ +provider "tencentcloud" { + secret_id = var.tencent_cloud_secret_id + secret_key = var.tencent_cloud_secret_key + region = "eu-frankfurt" +} + +provider "cloudflare" { + api_token = var.cloudflare_api_token +} + +provider "aiven" { + api_token = var.aiven_api_token +} + +provider "vercel" { + api_token = var.vercel_api_token + team = var.vercel_team +} + +provider "resend" { + api_key = var.resend_api_key +} + +provider "newrelic" { + account_id = var.newrelic_account_id + api_key = var.newrelic_api_token + region = "EU" +} diff --git a/infra/resend.tf b/infra/resend.tf new file mode 100644 index 0000000..a534dd1 --- /dev/null +++ b/infra/resend.tf @@ -0,0 +1,4 @@ +resource "resend_domain" "main" { + name = var.frontend_domain + region = "eu-west-1" +} diff --git a/infra/tencent.tf b/infra/tencent.tf new file mode 100644 index 0000000..ac59a7f --- /dev/null +++ b/infra/tencent.tf @@ -0,0 +1,44 @@ +resource "tencentcloud_lighthouse_instance" "main" { + instance_name = "Ubuntu-YUz3" + zone = "eu-frankfurt-1" + bundle_id = "bundle_starter_nmc_lin_med2_01" + blueprint_id = "lhbp-b46k6f98" + renew_flag = "NOTIFY_AND_MANUAL_RENEW" + firewall_template_id = tencentcloud_lighthouse_firewall_template.empty.id + + lifecycle { + ignore_changes = [firewall_template_id] + } +} + +resource "tencentcloud_lighthouse_firewall_rule" "main_firewall" { + instance_id = tencentcloud_lighthouse_instance.main.id + + firewall_rules { + protocol = "TCP" + port = "443,80" + cidr_block = "0.0.0.0/0" + action = "ACCEPT" + firewall_rule_description = "caddy" + } + + firewall_rules { + protocol = "TCP" + port = "22,2222" + cidr_block = "0.0.0.0/0" + action = "ACCEPT" + firewall_rule_description = "ssh" + } + + firewall_rules { + protocol = "ICMP" + port = "ALL" + cidr_block = "0.0.0.0/0" + action = "ACCEPT" + firewall_rule_description = "ping" + } +} + +resource "tencentcloud_lighthouse_firewall_template" "empty" { + template_name = "empty" +} diff --git a/infra/vars.tf b/infra/vars.tf new file mode 100644 index 0000000..83f4866 --- /dev/null +++ b/infra/vars.tf @@ -0,0 +1,72 @@ +// Local vars +variable "frontend_domain" { + type = string + default = "git.mari.zip" +} + +variable "backend_domain" { + type = string + default = "api.git.mari.zip" +} + +variable "local_ip_block" { + type = string + sensitive = true +} + +// Tencent Cloud +variable "tencent_cloud_secret_id" { + type = string + sensitive = true +} + +variable "tencent_cloud_secret_key" { + type = string + sensitive = true +} + +// Cloudflare +variable "cloudflare_api_token" { + type = string + sensitive = true +} + +variable "cloudflare_zone_id" { + type = string +} + +variable "cloudflare_account_id" { + type = string +} + +// Aiven +variable "aiven_api_token" { + type = string + sensitive = true +} + +// Vercel +variable "vercel_api_token" { + type = string + sensitive = true +} + +variable "vercel_team" { + type = string +} + +// Resend +variable "resend_api_key" { + type = string + sensitive = true +} + +// NewRelic +variable "newrelic_account_id" { + type = string +} + +variable "newrelic_api_token" { + type = string + sensitive = true +} diff --git a/infra/vercel.tf b/infra/vercel.tf new file mode 100644 index 0000000..c916e3a --- /dev/null +++ b/infra/vercel.tf @@ -0,0 +1,40 @@ +resource "vercel_project" "main" { + name = "gitarena" + framework = "nextjs" + root_directory = "gitarena-frontend" + + git_repository = { + type = "github" + repo = "mellowagain/gitarena" + } +} + +resource "vercel_project_domain" "frontend" { + project_id = vercel_project.main.id + domain = var.frontend_domain +} + +data "vercel_domain_config" "frontend" { + domain = var.frontend_domain + project_id_or_name = vercel_project.main.id +} + +resource "vercel_project_environment_variable" "next_public_api_url" { + project_id = vercel_project.main.id + + key = "NEXT_PUBLIC_API_URL" + value = "https://${var.backend_domain}" + sensitive = false + + target = ["production", "preview", "development"] +} + +resource "terraform_data" "vercel_redeploy_reminder" { + triggers_replace = { + env_vars = vercel_project_environment_variable.next_public_api_url.value + } + + provisioner "local-exec" { + command = "echo 'Vercel env vars changed, redeploy to apply: https://vercel.com/dashboard'" + } +}