Compare commits
10 Commits
d16e73bda8
...
3471374cb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3471374cb6 | ||
|
|
155ef0345e | ||
|
|
d8528f64dc | ||
|
|
a47b5cf50e | ||
|
|
68e6587ea1 | ||
|
|
99348d9eaa | ||
|
|
4c22465a59 | ||
|
|
610e68cf40 | ||
|
|
3e055e2c6a | ||
|
|
7eb51120f2 |
@@ -14,6 +14,10 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- infra.yml
|
||||
- gitea.yml
|
||||
- vaultwarden.yml
|
||||
- runner.yml
|
||||
- os-update.yml
|
||||
|
||||
jobs:
|
||||
ansible:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ terraform/.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
ansible/inventory.ini
|
||||
*.swp
|
||||
|
||||
@@ -4,3 +4,5 @@ gitea_instance_url: "https://git.lunarfront.tech"
|
||||
vaultwarden_domain: "vault.lunarfront.tech"
|
||||
vaultwarden_signups_allowed: false
|
||||
gitea_db_type: postgres
|
||||
gitea_registry_domain: "registry.lunarfront.tech"
|
||||
gitea_registry_domain: "registry.lunarfront.tech"
|
||||
|
||||
@@ -4,7 +4,4 @@
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- gitea
|
||||
- gitea-runner
|
||||
- vaultwarden
|
||||
- backup
|
||||
|
||||
15
ansible/os-update.yml
Normal file
15
ansible/os-update.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
- name: OS updates
|
||||
hosts: infra
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Update apt cache and upgrade packages
|
||||
apt:
|
||||
update_cache: true
|
||||
upgrade: safe
|
||||
|
||||
- name: Remove unused packages
|
||||
apt:
|
||||
autoremove: true
|
||||
purge: true
|
||||
@@ -5,14 +5,14 @@ runner:
|
||||
file: /data/.runner
|
||||
capacity: 2 # max concurrent jobs — lower if droplet is under load
|
||||
labels:
|
||||
- "ubuntu-latest:docker://node:20"
|
||||
- "ubuntu-22.04:docker://node:20"
|
||||
- "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
|
||||
- "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04"
|
||||
|
||||
cache:
|
||||
enabled: true
|
||||
dir: /data/cache
|
||||
|
||||
container:
|
||||
network: bridge
|
||||
network: host
|
||||
force_pull: false # reuse cached images to speed up builds
|
||||
docker_sock_path: /var/run/docker.sock
|
||||
|
||||
7
ansible/runner.yml
Normal file
7
ansible/runner.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Set up Gitea runner
|
||||
hosts: infra
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- gitea-runner
|
||||
7
ansible/vaultwarden.yml
Normal file
7
ansible/vaultwarden.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Set up Vaultwarden
|
||||
hosts: infra
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- vaultwarden
|
||||
@@ -6,6 +6,7 @@ RUN apk add --no-cache \
|
||||
curl \
|
||||
git \
|
||||
nodejs \
|
||||
npm \
|
||||
openssh-client \
|
||||
python3 \
|
||||
py3-pip \
|
||||
@@ -16,7 +17,11 @@ RUN curl -fsSL https://releases.hashicorp.com/terraform/1.10.5/terraform_1.10.5_
|
||||
&& unzip terraform.zip -d /usr/local/bin \
|
||||
&& rm terraform.zip
|
||||
|
||||
# Bun
|
||||
ENV BUN_INSTALL="/usr/local"
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Ansible collections
|
||||
RUN ansible-galaxy collection install community.docker
|
||||
|
||||
RUN terraform --version && ansible --version
|
||||
RUN terraform --version && ansible --version && bun --version
|
||||
|
||||
30
terraform/doks.tf
Normal file
30
terraform/doks.tf
Normal file
@@ -0,0 +1,30 @@
|
||||
# ─── DOKS Cluster ─────────────────────────────────────────────────────────────
|
||||
|
||||
resource "digitalocean_kubernetes_cluster" "main" {
|
||||
name = "lunarfront"
|
||||
region = var.region
|
||||
version = var.k8s_version
|
||||
|
||||
node_pool {
|
||||
name = "workers"
|
||||
size = var.k8s_node_size
|
||||
min_nodes = var.k8s_min_nodes
|
||||
max_nodes = var.k8s_max_nodes
|
||||
auto_scale = true
|
||||
}
|
||||
|
||||
tags = ["lunarfront", "k8s"]
|
||||
}
|
||||
|
||||
# ─── DNS — wildcard for customer subdomains → cluster load balancer ───────────
|
||||
# Uncomment after the cluster is up and nginx ingress load balancer IP is known.
|
||||
# Set cluster_lb_ip in terraform.tfvars then re-run terraform apply.
|
||||
|
||||
resource "cloudflare_record" "apps_wildcard" {
|
||||
zone_id = data.cloudflare_zone.main.id
|
||||
name = "*"
|
||||
type = "A"
|
||||
content = var.cluster_lb_ip
|
||||
proxied = true
|
||||
ttl = 1
|
||||
}
|
||||
@@ -112,11 +112,11 @@ resource "digitalocean_firewall" "gitea" {
|
||||
source_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
# Gitea SSH for git push/pull — your IP only
|
||||
# Gitea SSH for git push/pull — open until Gitea is migrated to DOKS
|
||||
inbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "2222"
|
||||
source_addresses = ["${var.admin_ip}/32"]
|
||||
source_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
@@ -138,7 +138,7 @@ resource "cloudflare_record" "gitea" {
|
||||
zone_id = data.cloudflare_zone.main.id
|
||||
name = "git"
|
||||
type = "A"
|
||||
value = digitalocean_droplet.gitea.ipv4_address
|
||||
content = digitalocean_droplet.gitea.ipv4_address
|
||||
proxied = true
|
||||
ttl = 1
|
||||
}
|
||||
@@ -147,7 +147,7 @@ resource "cloudflare_record" "vaultwarden" {
|
||||
zone_id = data.cloudflare_zone.main.id
|
||||
name = "vault"
|
||||
type = "A"
|
||||
value = digitalocean_droplet.gitea.ipv4_address
|
||||
content = digitalocean_droplet.gitea.ipv4_address
|
||||
proxied = true
|
||||
ttl = 1
|
||||
}
|
||||
@@ -157,7 +157,7 @@ resource "cloudflare_record" "git_ssh" {
|
||||
zone_id = data.cloudflare_zone.main.id
|
||||
name = "git-ssh"
|
||||
type = "A"
|
||||
value = digitalocean_droplet.gitea.ipv4_address
|
||||
content = digitalocean_droplet.gitea.ipv4_address
|
||||
proxied = false
|
||||
ttl = 3600
|
||||
}
|
||||
@@ -167,7 +167,7 @@ resource "cloudflare_record" "registry" {
|
||||
zone_id = data.cloudflare_zone.main.id
|
||||
name = "registry"
|
||||
type = "A"
|
||||
value = digitalocean_droplet.gitea.ipv4_address
|
||||
content = digitalocean_droplet.gitea.ipv4_address
|
||||
proxied = false
|
||||
ttl = 3600
|
||||
}
|
||||
|
||||
@@ -2,3 +2,28 @@ output "gitea_ip" {
|
||||
description = "Public IP of the Gitea droplet"
|
||||
value = digitalocean_droplet.gitea.ipv4_address
|
||||
}
|
||||
|
||||
output "k8s_cluster_id" {
|
||||
description = "DOKS cluster ID"
|
||||
value = digitalocean_kubernetes_cluster.main.id
|
||||
}
|
||||
|
||||
output "postgres_host" {
|
||||
description = "Managed Postgres host"
|
||||
value = digitalocean_database_cluster.postgres.host
|
||||
}
|
||||
|
||||
output "postgres_port" {
|
||||
description = "Managed Postgres port"
|
||||
value = digitalocean_database_cluster.postgres.port
|
||||
}
|
||||
|
||||
output "redis_host" {
|
||||
description = "Managed Redis/Valkey host"
|
||||
value = digitalocean_database_cluster.redis.host
|
||||
}
|
||||
|
||||
output "redis_port" {
|
||||
description = "Managed Redis/Valkey port"
|
||||
value = digitalocean_database_cluster.redis.port
|
||||
}
|
||||
|
||||
23
terraform/postgres.tf
Normal file
23
terraform/postgres.tf
Normal file
@@ -0,0 +1,23 @@
|
||||
# ─── Managed PostgreSQL cluster ───────────────────────────────────────────────
|
||||
# Shared across all customers — each customer gets their own database.
|
||||
|
||||
resource "digitalocean_database_cluster" "postgres" {
|
||||
name = "lunarfront-postgres"
|
||||
engine = "pg"
|
||||
version = "16"
|
||||
size = var.postgres_size
|
||||
region = var.region
|
||||
node_count = 1
|
||||
|
||||
tags = ["lunarfront", "postgres"]
|
||||
}
|
||||
|
||||
# Restrict access to the DOKS cluster only
|
||||
resource "digitalocean_database_firewall" "postgres" {
|
||||
cluster_id = digitalocean_database_cluster.postgres.id
|
||||
|
||||
rule {
|
||||
type = "k8s"
|
||||
value = digitalocean_kubernetes_cluster.main.id
|
||||
}
|
||||
}
|
||||
15
terraform/project.tf
Normal file
15
terraform/project.tf
Normal file
@@ -0,0 +1,15 @@
|
||||
# ─── DO Project assignment ────────────────────────────────────────────────────
|
||||
|
||||
data "digitalocean_project" "main" {
|
||||
name = "lunarfront-infra"
|
||||
}
|
||||
|
||||
resource "digitalocean_project_resources" "main" {
|
||||
project = data.digitalocean_project.main.id
|
||||
resources = [
|
||||
digitalocean_droplet.gitea.urn,
|
||||
digitalocean_kubernetes_cluster.main.urn,
|
||||
digitalocean_database_cluster.postgres.urn,
|
||||
digitalocean_database_cluster.redis.urn,
|
||||
]
|
||||
}
|
||||
22
terraform/redis.tf
Normal file
22
terraform/redis.tf
Normal file
@@ -0,0 +1,22 @@
|
||||
# ─── Managed Valkey/Redis cluster ─────────────────────────────────────────────
|
||||
# Shared across all customers.
|
||||
|
||||
resource "digitalocean_database_cluster" "redis" {
|
||||
name = "lunarfront-redis"
|
||||
engine = "valkey"
|
||||
version = "8"
|
||||
size = var.redis_size
|
||||
region = var.region
|
||||
node_count = 1
|
||||
|
||||
tags = ["lunarfront", "redis"]
|
||||
}
|
||||
|
||||
resource "digitalocean_database_firewall" "redis" {
|
||||
cluster_id = digitalocean_database_cluster.redis.id
|
||||
|
||||
rule {
|
||||
type = "k8s"
|
||||
value = digitalocean_kubernetes_cluster.main.id
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
do_token = "your-digitalocean-api-token"
|
||||
ssh_key_name = "your-key-name-in-do"
|
||||
region = "nyc3"
|
||||
droplet_size = "s-1vcpu-2gb"
|
||||
droplet_size = "s-2vcpu-4gb"
|
||||
cloudflare_api_token = "your-cloudflare-api-token"
|
||||
domain = "example.com"
|
||||
cluster_lb_ip = "your-ingress-lb-ip"
|
||||
|
||||
@@ -18,7 +18,7 @@ variable "region" {
|
||||
variable "droplet_size" {
|
||||
description = "Droplet size slug"
|
||||
type = string
|
||||
default = "s-1vcpu-2gb"
|
||||
default = "s-2vcpu-4gb"
|
||||
}
|
||||
|
||||
variable "cloudflare_api_token" {
|
||||
@@ -35,4 +35,51 @@ variable "domain" {
|
||||
variable "admin_ip" {
|
||||
description = "Your public IP for SSH and git access (without /32)"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ─── DOKS ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "k8s_version" {
|
||||
description = "Kubernetes version slug (run: doctl kubernetes options versions)"
|
||||
type = string
|
||||
default = "1.32.13-do.2"
|
||||
}
|
||||
|
||||
variable "k8s_node_size" {
|
||||
description = "Node pool droplet size"
|
||||
type = string
|
||||
default = "s-2vcpu-4gb"
|
||||
}
|
||||
|
||||
variable "k8s_min_nodes" {
|
||||
description = "Minimum nodes in the pool"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "k8s_max_nodes" {
|
||||
description = "Maximum nodes in the pool"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
variable "cluster_lb_ip" {
|
||||
description = "IP of the DOKS ingress load balancer (set after first cluster apply)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ─── Managed databases ────────────────────────────────────────────────────────
|
||||
|
||||
variable "postgres_size" {
|
||||
description = "Managed Postgres cluster size slug"
|
||||
type = string
|
||||
default = "db-s-1vcpu-1gb"
|
||||
}
|
||||
|
||||
variable "redis_size" {
|
||||
description = "Managed Valkey/Redis cluster size slug"
|
||||
type = string
|
||||
default = "db-s-1vcpu-1gb"
|
||||
}
|
||||
|
||||
25
terraform/waf.tf
Normal file
25
terraform/waf.tf
Normal file
@@ -0,0 +1,25 @@
|
||||
# ─── Cloudflare WAF — restrict admin subdomains to admin IP ───────────────────
|
||||
|
||||
resource "cloudflare_ruleset" "admin_ip_allowlist" {
|
||||
zone_id = data.cloudflare_zone.main.id
|
||||
name = "Admin IP allowlist"
|
||||
description = "Block access to admin subdomains from non-admin IPs"
|
||||
kind = "zone"
|
||||
phase = "http_request_firewall_custom"
|
||||
|
||||
rules {
|
||||
action = "block"
|
||||
description = "Block non-admin IPs from admin subdomains"
|
||||
enabled = true
|
||||
expression = <<-EOT
|
||||
(
|
||||
http.host in {
|
||||
"git.${var.domain}"
|
||||
"vault.${var.domain}"
|
||||
"argocd.${var.domain}"
|
||||
}
|
||||
and not ip.src eq ${var.admin_ip}
|
||||
)
|
||||
EOT
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user