Major changes

This commit is contained in:
2025-06-05 23:27:50 +02:00
parent a2dc223d1f
commit 3ea65f060f
107 changed files with 714 additions and 76 deletions
@@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- "terraform/k8sapps/**"
- "terraform/apps/contabo/**"
jobs:
iac-apply:
runs-on: ubuntu-latest
@@ -35,6 +35,7 @@ jobs:
- name: Setup Kubernetes Environment
uses: ./.github/actions/setup-k8s-env
with:
host: prod-01.alexpires.me
project-id: ${{ secrets.BW_PROJECT_ID }}
organization-id: ${{ secrets.BW_ORGANIZATION_ID }}
access-token: ${{ secrets.BW_ACCESS_TOKEN }}
@@ -1,26 +0,0 @@
FROM python:3.11-slim
ARG MCP_VERSION=0.2.0
ARG UID=1000
ARG GID=1000
RUN groupadd --system --gid ${GID} worker && \
adduser --system --gid ${GID} --uid ${UID} --home /home/worker worker
ENV PATH=/home/worker/.local/bin:$PATH
WORKDIR /home/worker
# Super user custom actions
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
tar \
&& rm -rf /var/lib/apt/lists/*
RUN curl -L https://gitea.com/gitea/gitea-mcp/releases/download/v${MCP_VERSION}/gitea-mcp_Linux_x86_64.tar.gz \
| tar -xz -C /usr/local/bin \
&& chmod +x /usr/local/bin/gitea-mcp
# End of super user custom actions
USER worker
RUN pip install mcpo uv
EXPOSE 8000
# Worker custom actions
ENV GITEA_HOST="https://gitea.com"
ENV GITEA_INSECURE="false"
ENV GITEA_ACCESS_TOKEN="<your personal access token>"
CMD ["uvx", "mcpo", "--host", "0.0.0.0", "--port", "8000", "--", "gitea-mcp"]
# End of worker custom actions
+21 -5
View File
@@ -32,8 +32,7 @@ sshd_max_auth_tries: 10
ufw_enable: true
fw_allowed_ports:
- { rule: "allow", port: "22", proto: "tcp" }
- { rule: "allow", port: "8000", proto: "tcp" }
- { rule: "allow", port: "8080", proto: "tcp" }
- { rule: "allow", port: "443", proto: "tcp" }
ufw_outgoing_traffic:
- 22 # SSH
@@ -42,8 +41,13 @@ ufw_outgoing_traffic:
- 123 # NTP
- 443 # HTTPS
- 853 # DNS over TLS
- 11434 # Ollama
- 10250 # Kubelet
- 8880 # Kokoro-FastAPI
- 11434 # Ollama
- 16443 # Kubernetes API
- 4443
- 9443
- 995 # POP3S
ufw_default_forward_policy: "ACCEPT"
@@ -77,6 +81,18 @@ unattended_reboot_time: "04:30"
duo_users: ["*", "!provision"]
duo_api_hostname: api-7cda1654.duosecurity.com
# Kubernetes specific settings
microk8s_allowed_hosts:
- 10.19.4.0/24
- 10.5.5.5
microk8s_alt_names: "{{ hostname }}"
# Let's encrypt issuer
microk8s_lets_encrypt_issuer: false
microk8s_encryption_secret: "{{ lookup('env', 'MICROK8S_ENCRYPTION_SECRET') }}"
microk8s_users:
- username: operator
enabled: true
# podman services settings
open_webui_home: "/srv/open-webui"
mcp_home: "/srv/mcp"
persistent_data: "/srv"
+20 -18
View File
@@ -1,22 +1,24 @@
- name: Setup open-webui
hosts: openwebui
vars:
podman_user: "{{ open_webui_user | default('openwebui') }}"
podman_group: "{{ open_webui_group | default('openwebui') }}"
podman_user_home: "{{ open_webui_home | default('/home/open-webui') }}"
podman_user_folders:
- data
pods:
- name: open-webui
repo:
image: ghcr.io/open-webui/open-webui
tag: main
ports:
- "0.0.0.0:{{ open_webui_port | default(8080) }}:8080/tcp"
volumes:
- "{{ open_webui_home }}/data:/app/backend/data:Z"
tasks:
- name: Setup Pods
ansible.builtin.include_tasks:
file: tasks/podman.yml
- name: Set required facts
ansible.builtin.set_fact:
open_webui_home: "{{ persistent_data | default('/var/lib') }}/open_webui"
open_webui_user_id: 0
open_webui_group_id: 0
tags: always
- name: Create required open_webui data folder
become: true
ansible.builtin.file:
state: directory
path: "{{ item }}"
mode: "0750"
owner: "{{ open_webui_user_id }}"
group: "{{ open_webui_group_id }}"
with_items:
- "{{ open_webui_home }}"
tags:
- tasks
- tasks::folders
+2 -1
View File
@@ -11,6 +11,7 @@ microk8s_metallb_version: 0.14.9
# certificate manager
microk8s_issuer_email: webmaster@localhost.localdomain
microk8s_certmgr_version: 1.14.5
microk8s_lets_encrypt_issuer: true
# microk8s specific
microk8s_channel: "{{ microk8s_k8s_version }}/stable"
@@ -39,4 +40,4 @@ microk8s_iac_user: "provision"
microk8s_encryption_secret: "a13labs"
microk8s_encryption_cfg: "/etc/microk8s_encryption.yaml"
microk8s_encryption_kms_socket:
microk8s_encryption_kms_socket: ""
@@ -16,6 +16,7 @@
wait: true
- name: CertManager | Register LetsEncrypt (prod)
when: microk8s_lets_encrypt_issuer is true
environment:
PATH: "{{ k8s_venv }}/bin:{{ ansible_env.PATH }}"
vars:
@@ -46,6 +47,7 @@
class: public
- name: CertManager | Register LetsEncrypt (staging)
when: microk8s_lets_encrypt_issuer is true
environment:
PATH: "{{ k8s_venv }}/bin:{{ ansible_env.PATH }}"
vars:
@@ -6,7 +6,7 @@ resources:
- secrets
- configmaps
providers:
{% if microk8s_encryption_kms_socket is defined %}
{% if microk8s_encryption_kms_socket != "" %}
- kms:
apiVersion: v2
name: aws-encryption-provider
+1
View File
@@ -40,6 +40,7 @@ prod-01.alexpires.me
[microk8s]
prod-01.alexpires.me
dev-01.alexpires.dev
[iac]
prod-01.alexpires.me
+6
View File
@@ -0,0 +1,6 @@
module "open-webui" {
source = "../../modules/apps/open-webui"
persistent_folder = local.persistent_folder
fqdn = local.open_webui_fqdn
}
+7
View File
@@ -0,0 +1,7 @@
terraform {
backend "s3" {
bucket = "a13labs.infra.eu"
key = "apps.dev-01.core.infra.tfstate"
region = "eu-west-1"
}
}
+9
View File
@@ -0,0 +1,9 @@
locals {
# storage
persistent_folder = "/srv"
primary_domain = "alexpires.me"
open_webui_fqdn = "ai.${local.primary_domain}"
internal_issuer = "internal-ca"
}
+56
View File
@@ -0,0 +1,56 @@
resource "tls_private_key" "tls" {
algorithm = "ED25519"
}
resource "tls_self_signed_cert" "tls" {
private_key_pem = tls_private_key.tls.private_key_pem
subject {
common_name = "kube-ca"
organization = "Alexandre Pires, Portugal"
}
validity_period_hours = 87600
is_ca_certificate = true
allowed_uses = [
"cert_signing",
"key_encipherment",
"digital_signature",
"crl_signing"
]
}
resource "kubernetes_secret" "root_ca" {
metadata {
name = "root-ca"
namespace = "cert-manager"
}
data = {
"tls.crt" = tls_self_signed_cert.tls.cert_pem
"tls.key" = tls_private_key.tls.private_key_pem
}
type = "kubernetes.io/tls"
}
resource "kubernetes_manifest" "cluster_issuer" {
manifest = {
apiVersion = "cert-manager.io/v1"
kind = "ClusterIssuer"
metadata = {
name = local.internal_issuer
annotations = {
"cert-manager.io/issuer-group" = "cert-manager.io"
"cert-manager.io/issuer-kind" = "Issuer"
"cert-manager.io/issuer-name" = local.internal_issuer
}
}
spec = {
ca = {
secretName = "root-ca"
}
}
}
}
@@ -47,8 +47,6 @@ resource "kubernetes_cluster_role" "operator_maintenance_role" {
verbs = ["get", "list", "watch", "create", "delete", "patch", "update"]
}
# Allow reading Pod logs
rule {
api_groups = [""]
+20
View File
@@ -0,0 +1,20 @@
terraform {
required_version = "~>1.8"
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "2.36.0"
}
tls = {
source = "hashicorp/tls"
version = "4.0.5"
}
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
config_context = "microk8s-dev-01"
insecure = true
}
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
@@ -1,11 +1,11 @@
module "websites" {
source = "../modules/apps/nginx"
source = "../../modules/apps/nginx"
persistent_folder = local.persistent_folder
websites = local.websites
}
module "rustdesk" {
source = "../modules/apps/rustdesk"
source = "../../modules/apps/rustdesk"
persistent_folder = local.persistent_folder
fqdn = local.rustdesk_fqdn
@@ -14,7 +14,7 @@ module "rustdesk" {
}
module "m3uproxy" {
source = "../modules/apps/m3uproxy"
source = "../../modules/apps/m3uproxy"
persistent_folder = local.persistent_folder
tag = local.m3uproxy_version
@@ -36,7 +36,7 @@ module "m3uproxy" {
}
module "mail" {
source = "../modules/apps/mail"
source = "../../modules/apps/mail"
persistent_folder = local.persistent_folder
@@ -57,7 +57,7 @@ module "mail" {
}
module "gitea" {
source = "../modules/apps/gitea"
source = "../../modules/apps/gitea"
persistent_folder = local.persistent_folder
database_folder = local.database_folder
@@ -94,7 +94,7 @@ module "gitea" {
}
module "sftpgo" {
source = "../modules/apps/sftpgo"
source = "../../modules/apps/sftpgo"
persistent_folder = local.persistent_folder
backup_folder = local.sftpgo_backup_path
@@ -116,7 +116,7 @@ module "sftpgo" {
}
module "trivy" {
source = "../modules/utils/trivy"
source = "../../modules/utils/trivy"
persistent_folder = local.persistent_folder
cron_schedule = local.trivy_cronjob_schedule
@@ -130,7 +130,7 @@ module "trivy" {
}
module "ssh_locker_web" {
source = "../modules/apps/ssh_locker_web"
source = "../../modules/apps/ssh_locker_web"
duo_client_id = var.server_api_duo_client_id
duo_client_secret = var.server_api_duo_client_secret
@@ -141,7 +141,7 @@ module "ssh_locker_web" {
}
module "reverse_proxy" {
source = "../modules/apps/nginx-reverse-proxy"
source = "../../modules/apps/nginx-reverse-proxy"
services = local.reverse_proxy_services
wireguard_config = local.reverse_proxy_wireguard_config
@@ -1,6 +1,6 @@
module "mail_backup" {
source = "../modules/utils/scw_rclone"
source = "../../modules/utils/scw_rclone"
namespace = module.mail.namespace
app_name = module.mail.app_name
@@ -19,7 +19,7 @@ module "mail_backup" {
}
module "gitea_backup" {
source = "../modules/utils/scw_rclone"
source = "../../modules/utils/scw_rclone"
namespace = module.gitea.namespace
app_name = module.gitea.app_name
@@ -39,7 +39,7 @@ module "gitea_backup" {
module "sftpgo_backup" {
source = "../modules/utils/scw_rclone"
source = "../../modules/utils/scw_rclone"
namespace = module.sftpgo.namespace
app_name = module.sftpgo.app_name
@@ -98,21 +98,23 @@ locals {
https_port = 8443
tls = true
fqdn = ["ai.alexpires.me"]
upstream = "http://10.19.4.136:8080"
upstream = "https://10.19.4.136"
locations = [
{
path = "/ws/socket.io/"
headers = {
"Upgrade" = "$http_upgrade"
"Connection" = "upgrade"
"Host" = "$host"
"X-Forwarded-For" = "$remote_addr"
"Upgrade" = "$http_upgrade"
"Connection" = "upgrade"
}
},
{
private = true
path = "/tools/gitea"
upstream = "http://10.19.4.136:8000"
rewrite = "^/tools/gitea/(.*)$ /$1 break"
},
# {
# private = true
# path = "/tools/gitea"
# upstream = "http://10.19.4.136:8000"
# rewrite = "^/tools/gitea/(.*)$ /$1 break"
# },
{
private = true
path = "/providers/ollama"
@@ -121,6 +123,10 @@ locals {
},
{
path = "/"
headers = {
"Host" = "$host"
"X-Forwarded-For" = "$remote_addr"
}
},
]
},
+129
View File
@@ -0,0 +1,129 @@
resource "kubernetes_service_account" "operator_service_account" {
metadata {
name = "operator"
namespace = "default"
}
}
resource "kubernetes_cluster_role" "operator_maintenance_role" {
metadata {
name = "operator-maintenance-role"
}
# Allow managing Pods (create, delete, exec, restart)
rule {
api_groups = [""]
resources = ["pods"]
verbs = ["get", "list", "watch", "create", "delete", "patch", "update", "exec"]
}
rule {
api_groups = [""]
resources = ["replicationcontrollers"]
verbs = ["get", "list"]
}
rule {
api_groups = ["autoscaling"]
resources = ["horizontalpodautoscalers"]
verbs = ["get", "list", "watch"]
}
rule {
api_groups = ["apps"]
resources = ["deployments", "replicasets", "statefulsets", "daemonsets"]
verbs = ["get", "list", "watch"]
}
rule {
api_groups = ["batch"]
resources = ["cronjobs"]
verbs = ["get", "list", "watch"]
}
rule {
api_groups = ["batch"]
resources = ["jobs"]
verbs = ["get", "list", "watch", "create", "delete", "patch", "update"]
}
# Allow reading Pod logs
rule {
api_groups = [""]
resources = ["pods/log"]
verbs = ["get", "list"]
}
# Allow managing Pod eviction for draining nodes, etc.
rule {
api_groups = ["policy"]
resources = ["pods/eviction"]
verbs = ["create"]
}
# Allow reading Namespaces (usually required for multi-namespace tools)
rule {
api_groups = [""]
resources = ["namespaces"]
verbs = ["get", "list", "watch"]
}
# Allow reading and managing PodDisruptionBudgets (useful for draining nodes)
rule {
api_groups = ["policy"]
resources = ["poddisruptionbudgets"]
verbs = ["get", "list", "watch", "patch", "update"]
}
# Allow managing Nodes (e.g., for cordoning, draining)
rule {
api_groups = [""]
resources = ["nodes"]
verbs = ["get", "list", "patch", "update"]
}
# Allow reading PersistentVolumeClaims (PVCs) but not PersistentVolumes
rule {
api_groups = [""]
resources = ["persistentvolumeclaims"]
verbs = ["get", "list", "watch"]
}
# Allow listing and getting services and endpoints (but not updating)
rule {
api_groups = [""]
resources = ["services", "endpoints"]
verbs = ["get", "list", "watch"]
}
}
resource "kubernetes_cluster_role_binding" "operator_maintenance_binding" {
metadata {
name = "operator-maintenance-binding"
}
role_ref {
kind = "ClusterRole"
name = kubernetes_cluster_role.operator_maintenance_role.metadata[0].name
api_group = "rbac.authorization.k8s.io"
}
subject {
kind = "ServiceAccount"
namespace = "default"
name = kubernetes_service_account.operator_service_account.metadata[0].name
}
}
resource "kubernetes_secret" "operator_access_token" {
metadata {
annotations = {
"kubernetes.io/service-account.name" = kubernetes_service_account.operator_service_account.metadata[0].name
}
name = "operator-token"
namespace = "default"
}
type = "kubernetes.io/service-account-token"
wait_for_service_account_token = true
}
@@ -30,7 +30,7 @@ terraform {
provider "kubernetes" {
config_path = "~/.kube/config"
config_context = "microk8s"
config_context = "microk8s-prod-01"
insecure = true
}
+2
View File
@@ -0,0 +1,2 @@
AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=$AWS_SECRET_KEY
+5
View File
@@ -102,6 +102,7 @@ resource "kubernetes_deployment" "m3uproxy" {
container_port = 8080
name = "http"
}
readiness_probe {
http_get {
path = "/health"
@@ -111,6 +112,7 @@ resource "kubernetes_deployment" "m3uproxy" {
period_seconds = 10
failure_threshold = 10
}
env {
name = "USERNAME"
value_from {
@@ -120,6 +122,7 @@ resource "kubernetes_deployment" "m3uproxy" {
}
}
}
env {
name = "PASSWORD"
value_from {
@@ -129,11 +132,13 @@ resource "kubernetes_deployment" "m3uproxy" {
}
}
}
security_context {
allow_privilege_escalation = false
run_as_user = 1000
run_as_group = 1000
}
volume_mount {
name = "m3uproxy-config"
mount_path = "/app/conf/m3uproxy.json"
@@ -322,6 +322,9 @@ resource "kubernetes_deployment" "reverse_proxy" {
}
initial_delay_seconds = 5
period_seconds = 10
failure_threshold = 10
timeout_seconds = 5
success_threshold = 1
}
security_context {
@@ -0,0 +1,8 @@
locals {
persistent_folder = var.persistent_folder
app_path = "${local.persistent_folder}/open-webui"
app_version = var.tag
app_fqdn = var.fqdn
}

Some files were not shown because too many files have changed in this diff Show More