diff --git a/.github/workflows/ansible-apply.yaml b/.github/workflows/ansible-apply.yaml
index fdb01ff..7da3b04 100644
--- a/.github/workflows/ansible-apply.yaml
+++ b/.github/workflows/ansible-apply.yaml
@@ -1,4 +1,4 @@
-name: Apply ansible playbooks
+name: Ansible Apply
on:
push:
@@ -9,7 +9,7 @@ on:
- .github/workflows/ansible-apply.yaml
jobs:
- apply-playbooks:
+ ansible-apply:
runs-on: ubuntu-latest
steps:
@@ -61,7 +61,7 @@ jobs:
DISPLAY=None SSH_ASKPASS=~/.ssh_askpass ssh-add ~/.ssh/id_ecdsa < /dev/null
rm ~/.ssh_askpass
- - name: Check SSH Connection
+ - name: Check SSH Connection to remote hosts
env:
TRUSTED_HOSTS: ${{ secrets.TRUSTED_HOSTS }}
ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }}
diff --git a/.github/workflows/ansible-validate.yaml b/.github/workflows/ansible-validate.yaml
index ecdf42b..ebcddfb 100644
--- a/.github/workflows/ansible-validate.yaml
+++ b/.github/workflows/ansible-validate.yaml
@@ -1,4 +1,4 @@
-name: Validate ansible playbooks
+name: Ansible Validate
on:
pull_request:
@@ -10,7 +10,7 @@ on:
- reopened
jobs:
- validate-playbooks:
+ ansible-validate:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/terraform-k8apps-apply.yaml b/.github/workflows/terraform-k8apps-apply.yaml
index c939cfe..0374509 100644
--- a/.github/workflows/terraform-k8apps-apply.yaml
+++ b/.github/workflows/terraform-k8apps-apply.yaml
@@ -1,4 +1,4 @@
-name: Build and Release
+name: Terraform Apply
on:
push:
@@ -9,7 +9,7 @@ on:
- .github/workflows/terraform-k8apps-apply.yaml
jobs:
- apply:
+ terraform-apply:
runs-on: ubuntu-latest
steps:
@@ -60,7 +60,7 @@ jobs:
DISPLAY=None SSH_ASKPASS=~/.ssh_askpass ssh-add ~/.ssh/id_ecdsa < /dev/null
rm ~/.ssh_askpass
- - name: Check SSH Connection
+ - name: Check SSH Connection to remote hosts
env:
TRUSTED_HOSTS: ${{ secrets.TRUSTED_HOSTS }}
ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }}
@@ -72,12 +72,12 @@ jobs:
- name: Copy Kube Config from remote host
env:
- ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }}
VAULT_MASTER_PASSWORD: ${{ secrets.VAULT_MASTER_PASSWORD }}
+ ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.kube
- ansible -i ansible/inventory/hosts -m fetch -a "src=/home/$ANSIBLE_USER/.kube/config dest=~/.kube/config flat=yes" microk8s
+ ansible -i ansible/inventory/hosts -u $ANSIBLE_USER -m fetch -a "src=/home/$ANSIBLE_USER/.kube/config dest=~/.kube/config flat=yes" microk8s
- name: Terraform Init
env:
diff --git a/.github/workflows/terraform-k8apps-validate.yaml b/.github/workflows/terraform-k8apps-validate.yaml
index 1e6b6c0..51c5bc8 100644
--- a/.github/workflows/terraform-k8apps-validate.yaml
+++ b/.github/workflows/terraform-k8apps-validate.yaml
@@ -1,4 +1,4 @@
-name: Validate Terraform
+name: Terraform Validate
on:
pull_request:
@@ -12,7 +12,7 @@ on:
- synchronize
- reopened
jobs:
- apply:
+ terraform-validate:
runs-on: ubuntu-latest
steps:
@@ -63,7 +63,7 @@ jobs:
DISPLAY=None SSH_ASKPASS=~/.ssh_askpass ssh-add ~/.ssh/id_ecdsa < /dev/null
rm ~/.ssh_askpass
- - name: Check SSH Connection
+ - name: Check SSH Connection to remote hosts
env:
TRUSTED_HOSTS: ${{ secrets.TRUSTED_HOSTS }}
ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }}
@@ -75,12 +75,12 @@ jobs:
- name: Copy Kube Config from remote host
env:
- ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }}
VAULT_MASTER_PASSWORD: ${{ secrets.VAULT_MASTER_PASSWORD }}
+ ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.kube
- ansible -i ansible/inventory/hosts -m fetch -a "src=/home/$ANSIBLE_USER/.kube/config dest=~/.kube/config flat=yes" microk8s
+ ansible -i ansible/inventory/hosts -u $ANSIBLE_USER -m fetch -a "src=/home/$ANSIBLE_USER/.kube/config dest=~/.kube/config flat=yes" microk8s
- name: Terraform Init
env:
@@ -89,9 +89,14 @@ jobs:
cd terraform/k8sapps
sectool exec terraform init
- - name: Terraform Apply
+ - name: Terraform Plan
env:
VAULT_MASTER_PASSWORD: ${{ secrets.VAULT_MASTER_PASSWORD }}
run: |
cd terraform/k8sapps
- sectool exec terraform plan
+ sectool exec -n terraform plan -out=tfplan
+
+ - name: Show Terraform Plan
+ run: |
+ cd terraform/k8sapps
+ terraform show tfplan
diff --git a/terraform/k8sapps/.vault b/terraform/k8sapps/.vault
new file mode 100644
index 0000000..7007d14
--- /dev/null
+++ b/terraform/k8sapps/.vault
@@ -0,0 +1 @@
+../../repository.vault
\ No newline at end of file
diff --git a/terraform/k8sapps/backend.tf b/terraform/k8sapps/backend.tf
new file mode 100644
index 0000000..38df914
--- /dev/null
+++ b/terraform/k8sapps/backend.tf
@@ -0,0 +1,24 @@
+#
+# Copyright 2019 Alexandre Pires (alexandre.pires@mov.ai)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# File: backend.tf
+
+terraform {
+ backend "s3" {
+ bucket = "a13labs.infra"
+ key = "scaleway-apps.tfstate"
+ region = "us-east-1"
+ }
+}
diff --git a/terraform/k8sapps/buckets.tf b/terraform/k8sapps/buckets.tf
new file mode 100644
index 0000000..2c9b910
--- /dev/null
+++ b/terraform/k8sapps/buckets.tf
@@ -0,0 +1,49 @@
+resource "random_string" "bucket_suffix" {
+ length = 8
+ lower = true
+ upper = false
+ special = false
+}
+
+resource "scaleway_object_bucket" "backup" {
+ name = "a13labs-backup-${random_string.bucket_suffix.result}"
+ project_id = var.scaleway_project_id
+ region = "fr-par"
+
+ lifecycle_rule {
+ id = "clean"
+ prefix = "gitea-backup/"
+ enabled = true
+
+ expiration {
+ days = local.cloud_backup_retention_days
+ }
+ }
+
+ lifecycle_rule {
+ id = "clean"
+ prefix = "wordpress-backup/"
+ enabled = true
+
+ expiration {
+ days = local.cloud_backup_retention_days
+ }
+ }
+
+ lifecycle_rule {
+ id = "clean"
+ prefix = "nextcloud-backup/"
+ enabled = true
+
+ expiration {
+ days = local.cloud_backup_retention_days
+ }
+ }
+
+}
+
+resource "scaleway_object_bucket" "nextcloud" {
+ name = "a13labs-nextcloud-${random_string.bucket_suffix.result}"
+ project_id = var.scaleway_project_id
+ region = "fr-par"
+}
diff --git a/terraform/k8sapps/collabora.tf.disabled b/terraform/k8sapps/collabora.tf.disabled
new file mode 100644
index 0000000..fadbf6b
--- /dev/null
+++ b/terraform/k8sapps/collabora.tf.disabled
@@ -0,0 +1,213 @@
+#
+# Namespace
+#
+resource "kubernetes_namespace" "collabora" {
+ metadata {
+ annotations = {
+ name = "collabora"
+ }
+
+ labels = {
+ name = "collabora"
+ }
+
+ name = "collabora"
+ }
+}
+
+resource "kubernetes_secret" "collabora_admin_credentials" {
+ metadata {
+ name = "collabora-admin-credentials"
+ namespace = kubernetes_namespace.collabora.metadata[0].name
+ }
+ data = {
+ username = "admin"
+ password = var.collabora_admin_password
+ }
+ type = "Opaque"
+}
+
+resource "kubernetes_config_map" "collabora" {
+
+ metadata {
+ name = "collabora-config"
+ namespace = kubernetes_namespace.collabora.metadata[0].name
+ }
+ data = {
+ "extra_params" = "-o:ssl.enable=true"
+ "aliasgroup1" = "https://${local.nextcloud_fqdn}"
+ }
+}
+
+resource "kubernetes_service_account" "collabora_service_account" {
+
+ metadata {
+ name = "collabora-app-sa"
+ namespace = kubernetes_namespace.collabora.metadata[0].name
+ }
+
+ automount_service_account_token = false
+}
+
+
+resource "kubernetes_service" "collabora_office" {
+ metadata {
+ name = "http"
+ namespace = kubernetes_namespace.collabora.metadata[0].name
+ }
+ spec {
+ port {
+ port = 80
+ target_port = "http"
+ }
+ selector = {
+ app = "collabora"
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_ingress_v1" "collabora_ingress" {
+ depends_on = [
+ kubernetes_stateful_set.collabora_office
+ ]
+ metadata {
+ name = "collabora-ingress"
+ namespace = kubernetes_namespace.collabora.metadata[0].name
+
+ annotations = {
+ "kubernetes.io/ingress.class" = "public"
+ "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
+ "kubernetes.io/tls-acme" = "true"
+ "nginx.ingress.kubernetes.io/backend-protocol" = "HTTPS"
+ }
+ }
+ spec {
+ tls {
+ hosts = [local.collabora_fqdn]
+ secret_name = "collabora-tls"
+ }
+ rule {
+ host = local.collabora_fqdn
+ http {
+ path {
+ backend {
+ service {
+ name = "http"
+ port {
+ number = 80
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+
+resource "kubernetes_stateful_set" "collabora_office" {
+
+ metadata {
+ name = "collabora"
+ namespace = kubernetes_namespace.collabora.metadata[0].name
+ }
+
+ spec {
+ service_name = "collabora"
+ selector {
+ match_labels = {
+ app = "collabora"
+ }
+ }
+ template {
+ metadata {
+ annotations = {
+ "cluster-autoscaler.kubernetes.io/safe-to-evict" = "true"
+ }
+ labels = {
+ app = "collabora"
+ }
+ }
+ spec {
+ termination_grace_period_seconds = 60
+ service_account_name = kubernetes_service_account.collabora_service_account.metadata[0].name
+ container {
+ name = "collabora"
+ image = "collabora/code:${local.collabora_version}"
+ image_pull_policy = "IfNotPresent"
+ port {
+ name = "http"
+ container_port = 9980
+ protocol = "TCP"
+ }
+ startup_probe {
+ tcp_socket {
+ # path = "/"
+ port = 9980
+ # scheme = "HTTP"
+ }
+ failure_threshold = 30
+ period_seconds = 3
+ }
+ liveness_probe {
+ tcp_socket {
+ # path = "/"
+ port = 9980
+ # scheme = "HTTP"
+ }
+ initial_delay_seconds = 0
+ period_seconds = 30
+ timeout_seconds = 30
+ success_threshold = 1
+ failure_threshold = 4
+ }
+ readiness_probe {
+ tcp_socket {
+ # path = "/"
+ port = 9980
+ # scheme = "HTTP"
+ }
+ initial_delay_seconds = 0
+ period_seconds = 30
+ timeout_seconds = 30
+ success_threshold = 1
+ failure_threshold = 2
+ }
+ env_from {
+ config_map_ref {
+ name = kubernetes_config_map.collabora.metadata[0].name
+ }
+ }
+ env {
+ name = "username"
+ value_from {
+ secret_key_ref {
+ name = kubernetes_secret.collabora_admin_credentials.metadata[0].name
+ key = "username"
+ }
+ }
+ }
+ env {
+ name = "password"
+ value_from {
+ secret_key_ref {
+ name = kubernetes_secret.collabora_admin_credentials.metadata[0].name
+ key = "password"
+ }
+ }
+ }
+ volume_mount {
+ name = "tmp"
+ mount_path = "/tmp"
+ }
+ }
+ volume {
+ name = "tmp"
+ empty_dir {
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/terraform/k8sapps/config.tf b/terraform/k8sapps/config.tf
new file mode 100644
index 0000000..4252430
--- /dev/null
+++ b/terraform/k8sapps/config.tf
@@ -0,0 +1,40 @@
+locals {
+
+ # apps
+ wordpress_version = "6.5.2"
+ wordpress_fqdn = "blog.alexpires.me"
+ wordpress_sql_backup_cron_schedule = "0 2 * * *"
+ wordpress_files_backup_cron_schedule = "10 2 * * *"
+
+ gitea_version = "1.21.3"
+ gitea_fqdn = "code.alexpires.me"
+ gitea_sql_backup_cron_schedule = "20 2 * * *"
+ gitea_files_backup_cron_schedule = "30 2 * * *"
+
+ nextcloud_version = "28.0.7-apache"
+ nextcloud_fqdn = "cloud.alexpires.me"
+ nextcloud_sql_backup_cron_schedule = "40 2 * * *"
+ nextcloud_files_backup_cron_schedule = "50 2 * * *"
+
+ # collabora
+ collabora_fqdn = "office.alexpires.me"
+ collabora_version = "24.04.1.4.1"
+
+ # tvheadend
+ m3uproxy_version = "latest"
+ wireguard_version = "latest"
+ m3uproxy_fqdn = "tv.alexpires.me"
+
+ # backup
+ local_backup_retention_days = 2
+ cloud_backup_retention_days = 3
+
+ # notification
+ scaleway_smtp_host = "smtp.tem.scw.cloud"
+ scaleway_smtp_port = 587
+}
+
+data "scaleway_account_project" "default" {
+ project_id = var.scaleway_project_id
+ organization_id = var.scaleway_organization_id
+}
diff --git a/terraform/k8sapps/gitea.tf b/terraform/k8sapps/gitea.tf
new file mode 100644
index 0000000..fc23a63
--- /dev/null
+++ b/terraform/k8sapps/gitea.tf
@@ -0,0 +1,352 @@
+locals {
+ gitea_env_vars = {
+ APP_NAME = "A13Labs git repository"
+ HTTP_PORT = "3000"
+ RUN_MODE = "prod"
+ LFS_START_SERVER = "true"
+ SSH_DOMAIN = local.gitea_fqdn
+ DISABLE_REGISTRATION = true
+ INSTALL_LOCK = "true"
+ DB_TYPE = "mysql"
+ DB_NAME = "gitea"
+ DB_HOST = module.gitea_database.host
+ SSH_LISTEN_PORT = 22
+ GITEA__mailer__ENABLED = true
+ GITEA__mailer__FROM = "gitea@alexpires.me"
+ GITEA__mailer__SMTP_ADDR = local.scaleway_smtp_host
+ GITEA__mailer__SMTP_PORT = local.scaleway_smtp_port
+ GITEA__mailer__PROTOCOL = "smtps"
+ }
+
+ gitea_secrets = {
+ DB_USER = {
+ key = "username"
+ name = module.gitea_database.app_crendentials_name
+ }
+ DB_PASSWD = {
+ key = "password"
+ name = module.gitea_database.app_crendentials_name
+ }
+ GITEA__mailer__USER = {
+ key = "username"
+ name = kubernetes_secret.gitea_mail_credentials.metadata[0].name
+ }
+ GITEA__mailer__PASSWD = {
+ key = "password"
+ name = kubernetes_secret.gitea_mail_credentials.metadata[0].name
+ }
+ }
+
+ gitea_path = "/var/lib/gitea"
+ gitea_mysql_path = "/var/lib/mysql.gitea"
+}
+
+#
+# Namespace
+#
+resource "kubernetes_namespace" "gitea" {
+ metadata {
+ annotations = {
+ name = "gitea"
+ }
+
+ labels = {
+ name = "gitea"
+ }
+
+ name = "gitea"
+ }
+}
+
+#
+# Mail credentials
+#
+resource "kubernetes_secret" "gitea_mail_credentials" {
+ metadata {
+ name = "gitea-mail-credentials"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+ data = {
+ username = var.scaleway_project_id
+ password = scaleway_iam_api_key.mail_app_api.secret_key
+ }
+ type = "Opaque"
+}
+
+#
+# Setup MariaDB database
+#
+module "gitea_database" {
+ source = "./modules/k8s_mariadb"
+
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ app_name = "gitea"
+ app_backup_folder = "gitea.backups"
+
+ app_password = var.gitea_mysql_password
+ root_password = var.mysql_root_password
+
+ retention_days = local.local_backup_retention_days
+ cron_schedule = local.gitea_sql_backup_cron_schedule
+ host_storage_path = local.gitea_mysql_path
+}
+
+#
+# Setup rsync backup job to scaleway bucket
+#
+module "gitea_backup" {
+ source = "./modules/scw_backup_job"
+
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ app_name = "gitea"
+ app_files_folder = "gitea"
+ app_backup_folder = "mysql.gitea/gitea.backups"
+
+ scaleway_organization_id = var.scaleway_organization_id
+ scaleway_project_id = var.scaleway_organization_id
+ access_key = scaleway_iam_api_key.backup_app_api.access_key
+ secret_key = scaleway_iam_api_key.backup_app_api.secret_key
+ bucket_name = scaleway_object_bucket.backup.name
+
+ retention_days = local.local_backup_retention_days
+ cron_schedule = local.gitea_files_backup_cron_schedule
+}
+
+#
+# Gitea service, ingress, deployment and file backup job
+#
+resource "kubernetes_service_account" "gitea_service_account" {
+
+ metadata {
+ name = "gitea-app-sa"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+
+ automount_service_account_token = false
+}
+
+resource "kubernetes_service" "gitea_http" {
+ metadata {
+ name = "http"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+ spec {
+ port {
+ port = 80
+ target_port = "http"
+ }
+ selector = {
+ app = "gitea"
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_service" "gitea_ssh" {
+ metadata {
+ name = "ssh"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+ spec {
+ port {
+ port = 22
+ target_port = "ssh"
+ node_port = 30036
+ }
+ selector = {
+ app = "gitea"
+ }
+ type = "NodePort"
+ }
+}
+
+resource "kubernetes_ingress_v1" "gitea_ingress" {
+ metadata {
+ name = "gitea-ingress"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+
+ annotations = {
+ "kubernetes.io/ingress.class" = "public"
+ "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
+ "nginx.ingress.kubernetes.io/ssl-redirect" = "true"
+ "nginx.ingress.kubernetes.io/proxy-body-size" = "8m"
+ "nginx.ingress.kubernetes.io/proxy-request-buffering" = "on"
+ "nginx.ingress.kubernetes.io/proxy-buffer-size" = "32k"
+ "nginx.ingress.kubernetes.io/proxy-buffers-number" = "16"
+ }
+ }
+ spec {
+ tls {
+ hosts = [local.gitea_fqdn]
+ secret_name = "gitea-tls"
+ }
+ rule {
+ host = local.gitea_fqdn
+ http {
+ path {
+ backend {
+ service {
+ name = "http"
+ port {
+ number = 80
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+resource "kubernetes_deployment" "gitea" {
+
+ depends_on = [module.gitea_database]
+
+ metadata {
+ name = "gitea-app"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+ spec {
+ selector {
+ match_labels = {
+ app = "gitea"
+ }
+ }
+ strategy {
+ type = "Recreate"
+ }
+ template {
+ metadata {
+ labels = {
+ app = "gitea"
+ }
+ }
+ spec {
+ service_account_name = kubernetes_service_account.gitea_service_account.metadata[0].name
+ container {
+ name = "gitea"
+ image = "gitea/gitea:${local.gitea_version}"
+ image_pull_policy = "Always"
+
+ resources {
+ requests = {
+ memory = "100Mi"
+ }
+ limits = {
+ memory = "300Mi"
+ }
+ }
+
+ liveness_probe {
+ http_get {
+ path = "/api/healthz"
+ port = "http"
+ }
+ initial_delay_seconds = 200
+ period_seconds = 10
+ success_threshold = 1
+ failure_threshold = 5
+ }
+
+ readiness_probe {
+ tcp_socket {
+ port = "http"
+ }
+ initial_delay_seconds = 5
+ period_seconds = 10
+ success_threshold = 1
+ failure_threshold = 3
+ }
+
+ dynamic "env" {
+ for_each = local.gitea_env_vars
+ content {
+ name = env.key
+ value = env.value
+ }
+ }
+
+ dynamic "env" {
+ for_each = local.gitea_secrets
+ content {
+ name = env.key
+ value_from {
+ secret_key_ref {
+ key = env.value.key
+ name = env.value.name
+ }
+ }
+ }
+ }
+
+ port {
+ container_port = 3000
+ name = "http"
+ }
+ port {
+ container_port = 22
+ name = "ssh"
+ }
+
+ volume_mount {
+ mount_path = "/data"
+ name = "persistent-storage"
+ }
+ }
+ volume {
+ name = "persistent-storage"
+ host_path {
+ path = local.gitea_path
+ }
+ }
+ }
+ }
+ }
+}
+
+resource "kubernetes_role" "gitea_ssh_access" {
+ metadata {
+ name = "gitea-ssh-access"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+ rule {
+ api_groups = [""]
+ resources = ["pods", "pods/log"]
+ verbs = ["get", "list"]
+ }
+ rule {
+ api_groups = [""]
+ resources = ["pods/exec"]
+ verbs = ["create"]
+ }
+}
+
+resource "kubernetes_role_binding" "gitea_ssh_access" {
+ metadata {
+ name = "gitea-ssh-access"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+ role_ref {
+ api_group = "rbac.authorization.k8s.io"
+ kind = "Role"
+ name = kubernetes_role.gitea_ssh_access.metadata[0].name
+ }
+ subject {
+ kind = "ServiceAccount"
+ name = kubernetes_service_account.gitea_service_account.metadata[0].name
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+}
+
+resource "kubernetes_secret" "gitea_access_token" {
+ metadata {
+ annotations = {
+ "kubernetes.io/service-account.name" = kubernetes_service_account.gitea_service_account.metadata[0].name
+ }
+ name = "git-token"
+ namespace = kubernetes_namespace.gitea.metadata[0].name
+ }
+
+ type = "kubernetes.io/service-account-token"
+ wait_for_service_account_token = true
+}
diff --git a/terraform/k8sapps/iam.tf b/terraform/k8sapps/iam.tf
new file mode 100644
index 0000000..7fb624e
--- /dev/null
+++ b/terraform/k8sapps/iam.tf
@@ -0,0 +1,72 @@
+resource "scaleway_iam_application" "backup_app" {
+ name = "k8s_backup_app"
+}
+
+resource "scaleway_iam_application" "mail_app" {
+ name = "mail_app"
+}
+
+resource "scaleway_iam_application" "nextcloud_app" {
+ name = "nextcloud_app"
+}
+
+resource "scaleway_iam_policy" "object_read_write_backup" {
+ name = "K8s backup app policy"
+ description = "Gives k8s backup job access to object storage in project"
+ application_id = scaleway_iam_application.backup_app.id
+
+ rule {
+ project_ids = [data.scaleway_account_project.default.id]
+ permission_set_names = [
+ "ObjectStorageObjectsWrite",
+ "ObjectStorageObjectsRead",
+ "ObjectStorageObjectsDelete",
+ "ObjectStorageBucketsRead"
+ ]
+ }
+}
+
+resource "scaleway_iam_policy" "email_full_access" {
+ name = "Email full access policy"
+ description = "Gives access to transactional email in project"
+ application_id = scaleway_iam_application.mail_app.id
+
+ rule {
+ project_ids = [data.scaleway_account_project.default.id]
+ permission_set_names = [
+ "TransactionalEmailFullAccess",
+ ]
+ }
+}
+
+resource "scaleway_iam_policy" "object_read_write_nextcloud" {
+ name = "Nextcloud app policy"
+ description = "Gives nextcloud job access to object storage in project"
+ application_id = scaleway_iam_application.nextcloud_app.id
+
+ rule {
+ project_ids = [data.scaleway_account_project.default.id]
+ permission_set_names = [
+ "ObjectStorageObjectsWrite",
+ "ObjectStorageObjectsRead",
+ "ObjectStorageBucketsRead",
+ "ObjectStorageBucketsWrite",
+ "ObjectStorageObjectsDelete"
+ ]
+ }
+}
+
+resource "scaleway_iam_api_key" "backup_app_api" {
+ application_id = scaleway_iam_application.backup_app.id
+ description = "k8s backup app API key"
+}
+
+resource "scaleway_iam_api_key" "mail_app_api" {
+ application_id = scaleway_iam_application.mail_app.id
+ description = "mail access API key"
+}
+
+resource "scaleway_iam_api_key" "nextcloud_app_api" {
+ application_id = scaleway_iam_application.nextcloud_app.id
+ description = "nextcloud access API key"
+}
diff --git a/terraform/k8sapps/m3uproxy.tf b/terraform/k8sapps/m3uproxy.tf
new file mode 100644
index 0000000..8a1576b
--- /dev/null
+++ b/terraform/k8sapps/m3uproxy.tf
@@ -0,0 +1,446 @@
+locals {
+ m3uproxy_path = "/var/lib/m3uproxy"
+ m3uproxy_cache_path = "${local.m3uproxy_path}/cache"
+ squid_app_path = "${local.m3uproxy_path}/squid"
+ wireguard_address = "192.168.5.3/32"
+ wireguard_dns = "192.168.5.1"
+ wireguard_allowed_ips = [
+ "192.168.5.1/32",
+ "192.168.5.3/32",
+ "0.0.0.0/0"
+ ]
+ wireguard_endpoint = "188.82.170.183"
+ wireguard_endpoint_port = 1195
+
+ m3uproxy_config = templatefile("${path.module}/resources/m3uproxy/m3uproxy.json", {
+ m3uproxy_secret = var.m3uproxy_secret
+ })
+
+}
+
+resource "kubernetes_namespace" "m3uproxy" {
+ metadata {
+ annotations = {
+ name = "m3uproxy"
+ }
+
+ labels = {
+ name = "m3uproxy"
+ }
+
+ name = "m3uproxy"
+ }
+
+}
+
+resource "kubernetes_secret" "m3uproxy_admin_credentials" {
+ metadata {
+ name = "m3uproxy-credentials"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+ data = {
+ username = "tv_admin"
+ password = var.m3uproxy_admin_password
+ }
+ type = "Opaque"
+}
+
+
+resource "kubernetes_service_account" "m3uproxy_service_account" {
+
+ metadata {
+ name = "m3uproxy-app-sa"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+
+ automount_service_account_token = false
+}
+
+resource "kubernetes_config_map" "wireguard_config" {
+ metadata {
+ name = "wireguard-config"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+
+ data = {
+ "wireguard.conf" = templatefile("${path.module}/resources/wireguard/wireguard.conf.tpl", {
+ wireguard_private_key = var.wireguard_private_key
+ wireguard_public_key = var.wireguard_public_key
+ wireguard_address = local.wireguard_address
+ wireguard_dns = local.wireguard_dns
+ wireguard_allowed_ips = join(", ", local.wireguard_allowed_ips)
+ wireguard_endpoint = "${local.wireguard_endpoint}:${local.wireguard_endpoint_port}"
+ })
+ }
+}
+
+resource "kubernetes_config_map" "squid_config" {
+ metadata {
+ name = "squid-config"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+
+ data = {
+ "squid.conf" = file("${path.module}/resources/squid/squid.conf")
+ }
+
+}
+
+resource "kubernetes_config_map" "m3uproxy_config" {
+ metadata {
+ name = "m3uproxy-config"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+
+ data = {
+ "m3uproxy.json" = local.m3uproxy_config
+ "playlist.json" = file("${path.module}/resources/m3uproxy/playlist.json")
+ }
+}
+
+resource "kubernetes_deployment" "proxy_pt" {
+
+ metadata {
+ name = "proxy-pt"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ annotations = {
+ "security.alpha.kubernetes.io/unsafe-sysctls" = "net.ipv4.conf.all.src_valid_mark=1"
+ }
+ }
+
+ spec {
+ replicas = 1
+
+ selector {
+ match_labels = {
+ app = "proxy-pt"
+ }
+ }
+
+ strategy {
+ type = "Recreate"
+ }
+
+ template {
+ metadata {
+ labels = {
+ app = "proxy-pt"
+ }
+ }
+
+ spec {
+ container {
+ name = "wireguard"
+ image = "linuxserver/wireguard:${local.wireguard_version}"
+
+ resources {
+ requests = {
+ memory = "64Mi"
+ }
+ }
+
+ liveness_probe {
+ exec {
+ command = [
+ "/bin/sh",
+ "-c",
+ "wg show | grep -q transfer"
+ ]
+ }
+ initial_delay_seconds = 90
+ period_seconds = 90
+ }
+
+ security_context {
+ privileged = true
+ capabilities {
+ add = ["NET_ADMIN"]
+ }
+ }
+
+ env {
+ name = "PUID"
+ value = "1000"
+ }
+
+ env {
+ name = "PGID"
+ value = "1000"
+ }
+
+ env {
+ name = "TZ"
+ value = "Europe/Berlin"
+ }
+
+ volume_mount {
+ name = "wireguard-config"
+ mount_path = "/config/wg_confs"
+ read_only = true
+ }
+
+ volume_mount {
+ name = "lib-modules"
+ mount_path = "/lib/modules"
+ read_only = true
+ }
+ }
+
+ container {
+ name = "squid"
+ image = "ubuntu/squid:edge"
+
+ port {
+ container_port = 3128
+ name = "proxy"
+ protocol = "TCP"
+ }
+
+ volume_mount {
+ name = "squid-config"
+ mount_path = "/etc/squid"
+ read_only = true
+ }
+
+ volume_mount {
+ name = "squid-data"
+ mount_path = "/var/spol/squid"
+ }
+
+ }
+
+ volume {
+ name = "wireguard-config"
+ config_map {
+ name = kubernetes_config_map.wireguard_config.metadata[0].name
+ }
+ }
+
+ volume {
+ name = "lib-modules"
+ host_path {
+ path = "/lib/modules"
+ }
+ }
+
+ volume {
+ name = "squid-config"
+ config_map {
+ name = kubernetes_config_map.squid_config.metadata[0].name
+ }
+ }
+
+ volume {
+ name = "squid-data"
+ host_path {
+ path = local.squid_app_path
+ }
+ }
+ }
+ }
+ }
+
+ lifecycle {
+ replace_triggered_by = [
+ kubernetes_config_map.wireguard_config,
+ kubernetes_config_map.squid_config
+ ]
+ }
+}
+
+
+# Create the Deployment
+resource "kubernetes_deployment" "m3uproxy" {
+ metadata {
+ name = "m3uproxy"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+
+ spec {
+ replicas = 1
+
+ selector {
+ match_labels = {
+ app = "m3uproxy"
+ }
+ }
+
+ strategy {
+ type = "RollingUpdate"
+ }
+
+ template {
+ metadata {
+ labels = {
+ app = "m3uproxy"
+ }
+ }
+
+ spec {
+ service_account_name = kubernetes_service_account.m3uproxy_service_account.metadata[0].name
+
+
+ container {
+ name = "m3uproxy"
+ image = "a13labs/m3uproxy:${local.m3uproxy_version}"
+ image_pull_policy = "Always"
+
+ resources {
+ requests = {
+ memory = "256Mi"
+ }
+ }
+
+ port {
+ container_port = 8080
+ name = "http"
+ }
+
+ readiness_probe {
+ http_get {
+ path = "/health"
+ port = "http"
+ }
+ initial_delay_seconds = 5
+ period_seconds = 10
+ failure_threshold = 10
+ }
+
+ env {
+ name = "USERNAME"
+ value_from {
+ secret_key_ref {
+ name = kubernetes_secret.m3uproxy_admin_credentials.metadata[0].name
+ key = "username"
+ }
+ }
+ }
+
+ env {
+ name = "PASSWORD"
+ value_from {
+ secret_key_ref {
+ name = kubernetes_secret.m3uproxy_admin_credentials.metadata[0].name
+ key = "password"
+ }
+ }
+ }
+
+ security_context {
+ run_as_user = 1000
+ run_as_group = 1000
+ }
+
+ volume_mount {
+ name = "m3uproxy-config"
+ mount_path = "/app/conf"
+ read_only = true
+ }
+
+ volume_mount {
+ name = "m3uproxy-app-cache"
+ mount_path = "/app/cache"
+ }
+
+ volume_mount {
+ name = "geoip-db"
+ mount_path = "/app/GeoIP"
+ read_only = true
+ }
+ }
+
+ volume {
+ name = "m3uproxy-config"
+ config_map {
+ name = kubernetes_config_map.m3uproxy_config.metadata[0].name
+ }
+ }
+
+ volume {
+ name = "m3uproxy-app-cache"
+ host_path {
+ path = local.m3uproxy_cache_path
+ }
+ }
+
+ volume {
+ name = "geoip-db"
+ host_path {
+ path = "/var/lib/GeoIP"
+ }
+ }
+
+ }
+ }
+ }
+}
+
+resource "kubernetes_service" "m3uproxy" {
+ metadata {
+ name = "http"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+
+ spec {
+ selector = {
+ app = "m3uproxy"
+ }
+ port {
+ port = 8080
+ target_port = "http"
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_service" "proxy_pt" {
+ metadata {
+ name = "proxy-pt"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+ }
+
+ spec {
+ selector = {
+ app = "proxy-pt"
+ }
+ port {
+ port = 3128
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_ingress_v1" "m3uproxy_ingress_http" {
+ metadata {
+ name = "m3uproxy-ingress"
+ namespace = kubernetes_namespace.m3uproxy.metadata[0].name
+
+ annotations = {
+ "kubernetes.io/ingress.class" = "public"
+ "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
+ "kubernetes.io/tls-acme" = "true"
+ "nginx.ingress.kubernetes.io/ssl-redirect" = "true"
+ }
+ }
+ spec {
+ tls {
+ hosts = [local.m3uproxy_fqdn]
+ secret_name = "m3uproxy-tls"
+ }
+ rule {
+ host = local.m3uproxy_fqdn
+ http {
+ path {
+ backend {
+ service {
+ name = "http"
+ port {
+ number = 8080
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/terraform/k8sapps/modules/k8s_mariadb/main.tf b/terraform/k8sapps/modules/k8s_mariadb/main.tf
new file mode 100644
index 0000000..df0fa60
--- /dev/null
+++ b/terraform/k8sapps/modules/k8s_mariadb/main.tf
@@ -0,0 +1,241 @@
+resource "kubernetes_secret" "root_password" {
+ metadata {
+ name = "${var.app_name}-root-credentials"
+ namespace = var.namespace
+ }
+ data = {
+ username = "root"
+ password = var.root_password
+ }
+ type = "Opaque"
+}
+
+resource "kubernetes_service_account" "service_account" {
+
+ metadata {
+ name = "${var.app_name}-mariadb-sa"
+ namespace = var.namespace
+ }
+
+ automount_service_account_token = false
+}
+
+resource "kubernetes_secret" "app_password" {
+ metadata {
+ name = "${var.app_name}-app-credentials"
+ namespace = var.namespace
+ }
+ data = {
+ username = var.app_name
+ password = var.app_password
+ }
+ type = "Opaque"
+}
+
+resource "kubernetes_service" "service" {
+ metadata {
+ name = "mariadb"
+ namespace = var.namespace
+ }
+ spec {
+ port {
+ port = 3306
+ }
+ selector = {
+ app = "mariadb"
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_deployment" "deployment" {
+ metadata {
+ name = "mariadb"
+ namespace = var.namespace
+ }
+ spec {
+ selector {
+ match_labels = {
+ app = "mariadb"
+ }
+ }
+ strategy {
+ type = "Recreate"
+ }
+ template {
+ metadata {
+ labels = {
+ app = "mariadb"
+ }
+ }
+ spec {
+ service_account_name = kubernetes_service_account.service_account.metadata[0].name
+ container {
+ name = "mariadb"
+ image = "mariadb:${var.current_version}"
+
+ resources {
+ requests = {
+ memory = "100Mi"
+ }
+ }
+
+ liveness_probe {
+ exec {
+ command = ["sh", "-c", "mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"]
+ }
+ initial_delay_seconds = 30
+ period_seconds = 10
+ timeout_seconds = 1
+ failure_threshold = 15
+ }
+
+ readiness_probe {
+ exec {
+ command = ["sh", "-c", "mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"]
+ }
+ initial_delay_seconds = 120
+ period_seconds = 10
+ timeout_seconds = 1
+ failure_threshold = 15
+ }
+
+ env {
+ name = "MARIADB_ROOT_PASSWORD"
+ value_from {
+ secret_key_ref {
+ key = "password"
+ name = kubernetes_secret.root_password.metadata[0].name
+ }
+ }
+ }
+ env {
+ name = "MARIADB_ROOT_HOST"
+ value = "10.%"
+ }
+ env {
+ name = "MARIADB_USER"
+ value_from {
+ secret_key_ref {
+ key = "username"
+ name = kubernetes_secret.app_password.metadata[0].name
+ }
+ }
+ }
+ env {
+ name = "MARIADB_PASSWORD"
+ value_from {
+ secret_key_ref {
+ key = "password"
+ name = kubernetes_secret.app_password.metadata[0].name
+ }
+ }
+ }
+ env {
+ name = "MARIADB_DATABASE"
+ value = var.app_name
+ }
+ port {
+ container_port = 3306
+ name = "mariadb"
+ }
+ volume_mount {
+ mount_path = "/var/lib/mysql"
+ name = "persistent-storage"
+ }
+ volume_mount {
+ mount_path = "/docker-entrypoint-initdb.d"
+ name = "initdb-storage"
+ }
+ }
+ volume {
+ name = "persistent-storage"
+ host_path {
+ path = "${var.host_storage_path}/${var.app_name}.data"
+ }
+ }
+ volume {
+ name = "initdb-storage"
+ host_path {
+ path = "${var.host_storage_path}/${var.app_name}.initdb.d"
+ }
+ }
+ }
+ }
+ }
+}
+
+# TODO: This should move out of here
+resource "kubernetes_cron_job_v1" "backup_data_cron" {
+ metadata {
+ name = "mariadb-backup-job"
+ namespace = var.namespace
+ }
+ spec {
+ concurrency_policy = "Forbid"
+ schedule = var.cron_schedule
+ failed_jobs_history_limit = 2
+ successful_jobs_history_limit = 2
+ job_template {
+ metadata {
+ labels = {
+ app = "mariadb"
+ }
+ }
+ spec {
+ backoff_limit = 1
+ parallelism = 1
+ completions = 1
+ active_deadline_seconds = 1800
+ template {
+ metadata {}
+ spec {
+ container {
+ name = "sql-cron-job"
+ image = "mariadb:${var.current_version}"
+ env {
+ name = "MARIADB_USER"
+ value_from {
+ secret_key_ref {
+ key = "username"
+ name = kubernetes_secret.app_password.metadata[0].name
+ }
+ }
+ }
+ env {
+ name = "MARIADB_PASSWORD"
+ value_from {
+ secret_key_ref {
+ key = "password"
+ name = kubernetes_secret.app_password.metadata[0].name
+ }
+ }
+ }
+ env {
+ name = "MARIADB_DATABASE"
+ value = var.app_name
+ }
+ command = ["/bin/sh", "-c"]
+ args = [join(";", [
+ "/usr/bin/mariadb-dump --single-transaction -h mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE | gzip > /backups/${var.app_name}-sql-`date +\"%Y%m%d\"`.sql.gz",
+ "find /backups -type f -name '${var.app_name}-sql-*.sql.gz' -mtime +${var.retention_days} | xargs --no-run-if-empty rm"
+ ])]
+ volume_mount {
+ mount_path = "/backups"
+ name = "backups-storage"
+ }
+ }
+ volume {
+ name = "backups-storage"
+ host_path {
+ path = "${var.host_storage_path}/${var.app_backup_folder}"
+ }
+ }
+ restart_policy = "Never"
+ termination_grace_period_seconds = 30
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/terraform/k8sapps/modules/k8s_mariadb/outputs.tf b/terraform/k8sapps/modules/k8s_mariadb/outputs.tf
new file mode 100644
index 0000000..4940974
--- /dev/null
+++ b/terraform/k8sapps/modules/k8s_mariadb/outputs.tf
@@ -0,0 +1,15 @@
+output "host" {
+ value = "mariadb"
+}
+
+output "port" {
+ value = 3306
+}
+
+output "root_crendentials_name" {
+ value = kubernetes_secret.root_password.metadata[0].name
+}
+
+output "app_crendentials_name" {
+ value = kubernetes_secret.app_password.metadata[0].name
+}
diff --git a/terraform/k8sapps/modules/k8s_mariadb/provider.tf b/terraform/k8sapps/modules/k8s_mariadb/provider.tf
new file mode 100644
index 0000000..d1b0a1a
--- /dev/null
+++ b/terraform/k8sapps/modules/k8s_mariadb/provider.tf
@@ -0,0 +1,11 @@
+terraform {
+ required_version = "~>1.8"
+ required_providers {
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = "~>2.18"
+ }
+ }
+}
+
+
diff --git a/terraform/k8sapps/modules/k8s_mariadb/variables.tf b/terraform/k8sapps/modules/k8s_mariadb/variables.tf
new file mode 100644
index 0000000..b4541ca
--- /dev/null
+++ b/terraform/k8sapps/modules/k8s_mariadb/variables.tf
@@ -0,0 +1,52 @@
+variable "namespace" {
+ description = "namespace"
+ sensitive = false
+ type = string
+}
+
+variable "app_name" {
+ description = "database name"
+ sensitive = false
+ type = string
+}
+
+variable "root_password" {
+ description = "root password"
+ sensitive = true
+ type = string
+}
+
+variable "app_password" {
+ description = "user password"
+ sensitive = true
+ type = string
+}
+
+variable "host_storage_path" {
+ description = "host storage path"
+ default = "/srv"
+ type = string
+}
+
+variable "cron_schedule" {
+ description = "Cronjob schedule for data backups"
+ default = "* 2 * * *"
+ type = string
+}
+
+variable "retention_days" {
+ description = "Backup retention days"
+ default = 14
+ type = number
+}
+
+variable "current_version" {
+ description = "version"
+ default = "10.7.8"
+ type = string
+}
+
+variable "app_backup_folder" {
+ description = "Backup source location"
+ type = string
+}
diff --git a/terraform/k8sapps/modules/scw_backup_job/config.tf b/terraform/k8sapps/modules/scw_backup_job/config.tf
new file mode 100644
index 0000000..48569eb
--- /dev/null
+++ b/terraform/k8sapps/modules/scw_backup_job/config.tf
@@ -0,0 +1,3 @@
+locals {
+ rclone_version = "1.61.1"
+}
\ No newline at end of file
diff --git a/terraform/k8sapps/modules/scw_backup_job/main.tf b/terraform/k8sapps/modules/scw_backup_job/main.tf
new file mode 100644
index 0000000..8ac4a47
--- /dev/null
+++ b/terraform/k8sapps/modules/scw_backup_job/main.tf
@@ -0,0 +1,164 @@
+resource "kubernetes_service_account" "service_account" {
+ metadata {
+ name = "${var.app_name}-backup-sa"
+ namespace = var.namespace
+ }
+
+ automount_service_account_token = false
+}
+
+resource "kubernetes_secret" "scaleway_api_credentials" {
+ metadata {
+ name = "${var.app_name}-scaleway-api-credentials"
+ namespace = var.namespace
+ }
+ data = {
+ access_key = var.access_key
+ secret_key = var.secret_key
+ }
+ type = "Opaque"
+}
+
+resource "kubernetes_config_map" "rclone_config" {
+ metadata {
+ name = "rclone-config"
+ namespace = var.namespace
+ }
+ data = {
+ "rclone.conf" = file("${path.module}/resources/rclone.conf")
+ }
+}
+
+resource "kubernetes_cron_job_v1" "backup_job" {
+ metadata {
+ name = "${var.app_name}-backup"
+ namespace = var.namespace
+ }
+ spec {
+ concurrency_policy = "Forbid"
+ schedule = var.cron_schedule
+ failed_jobs_history_limit = 2
+ successful_jobs_history_limit = 2
+ job_template {
+ metadata {
+ labels = {
+ app = "${var.app_name}-backup"
+ }
+ }
+ spec {
+ backoff_limit = 1
+ parallelism = 1
+ completions = 1
+ active_deadline_seconds = 1800
+ template {
+ metadata {
+ labels = {
+ app = "${var.app_name}-backup"
+ }
+ }
+ spec {
+ service_account_name = kubernetes_service_account.service_account.metadata[0].name
+ security_context {
+ run_as_user = 0
+ }
+ container {
+
+ name = "${var.app_name}-backup"
+ image = "rclone/rclone:${local.rclone_version}"
+ image_pull_policy = "Always"
+
+ resources {
+ requests = {
+ memory = "100Mi"
+ }
+ }
+
+ env {
+ name = "SCW_ACCESS_KEY"
+ value_from {
+ secret_key_ref {
+ key = "access_key"
+ name = kubernetes_secret.scaleway_api_credentials.metadata[0].name
+ }
+ }
+ }
+ env {
+ name = "SCW_SECRET_KEY"
+ value_from {
+ secret_key_ref {
+ key = "secret_key"
+ name = kubernetes_secret.scaleway_api_credentials.metadata[0].name
+ }
+ }
+ }
+ env {
+ name = "SCW_DEFAULT_PROJECT_ID"
+ value = var.scaleway_project_id
+ }
+ env {
+ name = "SCW_DEFAULT_ORGANIZATION_ID"
+ value = var.scaleway_organization_id
+ }
+ env {
+ name = "SCW_DEFAULT_REGION"
+ value = var.scaleway_region
+ }
+ env {
+ name = "SCW_DEFAULT_ZONE"
+ value = var.scaleway_zone
+ }
+ env {
+ name = "BUCKET_NAME"
+ value = var.bucket_name
+ }
+ command = ["/bin/sh", "-c"]
+ args = [join(";", [
+ "apk update",
+ "apk add gettext",
+ "envsubst/etc/rclone.conf",
+ "tar -zcvpf /backup/${var.app_name}-files-`date +\"%Y%m%d\"`.tar.gz /files",
+ "find /backup -type f -name '${var.app_name}-files-*.tar.gz' -mtime +${var.retention_days} | xargs --no-run-if-empty rm",
+ "/usr/local/bin/rclone --config=/etc/rclone.conf sync --progress /backup scaleway:$BUCKET_NAME/${var.app_name}-backup"
+ ])]
+ volume_mount {
+ name = "backup-storage"
+ mount_path = "/backup"
+ }
+ volume_mount {
+ name = "persistent-storage"
+ mount_path = "/files"
+ read_only = true
+ }
+ volume_mount {
+ name = "rclone-conf"
+ mount_path = "/etc/rclone.conf.tmpl"
+ sub_path = "rclone.conf"
+ read_only = true
+ }
+ }
+ volume {
+ name = "backup-storage"
+ host_path {
+ path = "${var.host_storage_path}/${var.app_backup_folder}"
+ }
+ }
+ volume {
+ name = "persistent-storage"
+ host_path {
+ path = "${var.host_storage_path}/${var.app_files_folder}"
+ }
+ }
+ volume {
+ name = "rclone-conf"
+ config_map {
+ name = kubernetes_config_map.rclone_config.metadata[0].name
+ }
+ }
+ restart_policy = "Never"
+ termination_grace_period_seconds = 30
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/terraform/k8sapps/modules/scw_backup_job/provider.tf b/terraform/k8sapps/modules/scw_backup_job/provider.tf
new file mode 100644
index 0000000..d1b0a1a
--- /dev/null
+++ b/terraform/k8sapps/modules/scw_backup_job/provider.tf
@@ -0,0 +1,11 @@
+terraform {
+ required_version = "~>1.8"
+ required_providers {
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = "~>2.18"
+ }
+ }
+}
+
+
diff --git a/terraform/k8sapps/modules/scw_backup_job/resources/rclone.conf b/terraform/k8sapps/modules/scw_backup_job/resources/rclone.conf
new file mode 100644
index 0000000..6ae9387
--- /dev/null
+++ b/terraform/k8sapps/modules/scw_backup_job/resources/rclone.conf
@@ -0,0 +1,10 @@
+[scaleway]
+type = s3
+provider = Scaleway
+env_auth = false
+access_key_id = $SCW_ACCESS_KEY
+secret_access_key = $SCW_SECRET_KEY
+region = $SCW_DEFAULT_REGION
+endpoint = s3.$SCW_DEFAULT_REGION.scw.cloud
+acl = private
+storage_class = STANDARD
\ No newline at end of file
diff --git a/terraform/k8sapps/modules/scw_backup_job/variables.tf b/terraform/k8sapps/modules/scw_backup_job/variables.tf
new file mode 100644
index 0000000..05bb10d
--- /dev/null
+++ b/terraform/k8sapps/modules/scw_backup_job/variables.tf
@@ -0,0 +1,76 @@
+variable "bucket_name" {
+ description = "Backup bucket name"
+ type = string
+}
+
+variable "namespace" {
+ description = "Namespace"
+ type = string
+}
+
+variable "scaleway_project_id" {
+ description = "Scaleway project id"
+ sensitive = true
+ type = string
+}
+
+variable "scaleway_organization_id" {
+ description = "Scaleway organization id"
+ sensitive = true
+ type = string
+}
+
+variable "scaleway_region" {
+ description = "Scaleway region"
+ default = "fr-par"
+ type = string
+}
+
+variable "scaleway_zone" {
+ default = "fr-par-2"
+ type = string
+}
+
+variable "cron_schedule" {
+ description = "Cronjob schedule for data backups"
+ default = "* 2 * * *"
+ type = string
+}
+
+variable "host_storage_path" {
+ description = "Host path"
+ default = "/var/lib"
+ type = string
+}
+
+variable "access_key" {
+ description = "Access key"
+ sensitive = true
+ type = string
+}
+
+variable "secret_key" {
+ description = "Secret key"
+ sensitive = true
+ type = string
+}
+
+variable "app_name" {
+ description = "Name of the app to backup"
+ type = string
+}
+
+variable "app_files_folder" {
+ description = "Name of host folder"
+ type = string
+}
+
+variable "app_backup_folder" {
+ description = "Backup source location"
+ type = string
+}
+
+variable "retention_days" {
+ description = "backup retention days"
+ type = string
+}
diff --git a/terraform/k8sapps/nextcloud.tf.disabled b/terraform/k8sapps/nextcloud.tf.disabled
new file mode 100644
index 0000000..e071b1f
--- /dev/null
+++ b/terraform/k8sapps/nextcloud.tf.disabled
@@ -0,0 +1,492 @@
+locals {
+ nextcloud_env_vars = {
+ # Database configuration
+ MYSQL_DATABASE = "nextcloud"
+ MYSQL_HOST = module.nextcloud_database.host
+
+ # Email configuration
+ SMTP_HOST = local.scaleway_smtp_host
+ SMTP_SECURE = "ssl"
+ SMTP_PORT = 465
+ MAIL_FROM_ADDRESS = "cloud"
+ MAIL_DOMAIN = "alexpires.me"
+ SMTP_AUTHTYPE = "PLAIN"
+
+ # Redis configuration
+ REDIS_HOST = "redis"
+
+ # Object as primary storage
+ OBJECTSTORE_S3_HOST = "s3.${scaleway_object_bucket.nextcloud.region}.scw.cloud"
+ OBJECTSTORE_S3_BUCKET = scaleway_object_bucket.nextcloud.name
+ OBJECTSTORE_S3_PORT = 443
+ OBJECTSTORE_S3_SSL = true
+ OBJECTSTORE_S3_REGION = scaleway_object_bucket.nextcloud.region
+ OBJECTSTORE_S3_AUTOCREATE = false
+
+ # General
+ NEXTCLOUD_TRUSTED_DOMAINS = local.nextcloud_fqdn
+ TRUSTED_PROXIES = join(" ", ["10.1.0.0/16"])
+ OVERWRITEHOST = local.nextcloud_fqdn
+ OVERWRITEPROTOCOL = "https"
+ OVERWRITECLIURL = "https://${local.nextcloud_fqdn}"
+ }
+
+ nextcloud_secrets = {
+
+ OBJECTSTORE_S3_KEY = {
+ key = "access_key"
+ name = kubernetes_secret.nextcloud_object_store_credentials.metadata[0].name
+ }
+
+ OBJECTSTORE_S3_SECRET = {
+ key = "secret_key"
+ name = kubernetes_secret.nextcloud_object_store_credentials.metadata[0].name
+ }
+
+ SMTP_NAME = {
+ key = "username"
+ name = kubernetes_secret.nextcloud_mail_credentials.metadata[0].name
+ }
+
+ SMTP_PASSWORD = {
+ key = "password"
+ name = kubernetes_secret.nextcloud_mail_credentials.metadata[0].name
+ }
+
+ MYSQL_USER = {
+ key = "username"
+ name = module.nextcloud_database.app_crendentials_name
+ }
+
+ MYSQL_PASSWORD = {
+ key = "password"
+ name = module.nextcloud_database.app_crendentials_name
+ }
+
+ NEXTCLOUD_ADMIN_USER = {
+ key = "username"
+ name = kubernetes_secret.nextcloud_admin_credentials.metadata[0].name
+ }
+
+ NEXTCLOUD_ADMIN_PASSWORD = {
+ key = "password"
+ name = kubernetes_secret.nextcloud_admin_credentials.metadata[0].name
+ }
+ }
+
+ nextcloud_path = "/var/lib/nextcloud"
+ nextcloud_mysql_path = "/var/lib/mysql.nextcloud"
+
+}
+
+#
+# Namespace
+#
+resource "kubernetes_namespace" "nextcloud" {
+ metadata {
+ annotations = {
+ name = "nextcloud"
+ }
+
+ labels = {
+ name = "nextcloud"
+ }
+
+ name = "nextcloud"
+ }
+}
+
+#
+# Nextcloud required credentials
+#
+resource "kubernetes_secret" "nextcloud_mail_credentials" {
+ metadata {
+ name = "nextcloud-mail-credentials"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+ data = {
+ username = var.scaleway_project_id
+ password = scaleway_iam_api_key.mail_app_api.secret_key
+ }
+ type = "Opaque"
+}
+
+resource "kubernetes_secret" "nextcloud_object_store_credentials" {
+ metadata {
+ name = "nextcloud-object-store-credentials"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+ data = {
+ access_key = scaleway_iam_api_key.nextcloud_app_api.access_key
+ secret_key = scaleway_iam_api_key.nextcloud_app_api.secret_key
+ }
+ type = "Opaque"
+}
+
+resource "kubernetes_secret" "nextcloud_admin_credentials" {
+ metadata {
+ name = "nextcloud-admin-credentials"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+ data = {
+ username = "admin"
+ password = var.nextcloud_admin_password
+ }
+ type = "Opaque"
+}
+
+#
+# Setup MariaDB database
+#
+module "nextcloud_database" {
+ source = "./modules/k8s_mariadb"
+
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ app_name = "nextcloud"
+ app_backup_folder = "nextcloud.backups"
+
+ app_password = var.nextcloud_mysql_password
+ root_password = var.mysql_root_password
+
+ retention_days = local.local_backup_retention_days
+ cron_schedule = local.nextcloud_sql_backup_cron_schedule
+ host_storage_path = local.nextcloud_mysql_path
+}
+
+#
+# Setup rsync backup job to scaleway bucket
+#
+module "nextcloud_backup" {
+
+ source = "./modules/scw_backup_job"
+
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ app_name = "nextcloud"
+ app_files_folder = "nextcloud"
+ app_backup_folder = "mysql.nextcloud/nextcloud.backups"
+
+ scaleway_organization_id = var.scaleway_organization_id
+ scaleway_project_id = var.scaleway_organization_id
+ access_key = scaleway_iam_api_key.backup_app_api.access_key
+ secret_key = scaleway_iam_api_key.backup_app_api.secret_key
+ bucket_name = scaleway_object_bucket.backup.name
+
+ retention_days = local.local_backup_retention_days
+ cron_schedule = local.nextcloud_files_backup_cron_schedule
+}
+
+#
+# Nextcloud service, ingress, deployment and file backup job
+#
+#
+resource "kubernetes_service_account" "nextcloud_service_account" {
+
+ metadata {
+ name = "nextcloud-app-sa"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+
+ automount_service_account_token = false
+}
+
+resource "kubernetes_service" "nextcloud_redis" {
+ metadata {
+ name = "redis"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+ spec {
+ port {
+ port = 6379
+ name = "redis"
+ }
+ selector = {
+ app = "redis"
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_service" "nextcloud_apache" {
+ metadata {
+ name = "http"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+ spec {
+ port {
+ port = 80
+ target_port = "http"
+ }
+ selector = {
+ app = "nextcloud"
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_ingress_v1" "nextcloud_ingress" {
+ depends_on = [
+ kubernetes_deployment.nextcloud_apache
+ ]
+ metadata {
+ name = "nextcloud-ingress"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+
+ annotations = {
+ "kubernetes.io/ingress.class" = "public"
+ "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
+ "kubernetes.io/tls-acme" = "true"
+ "nginx.ingress.kubernetes.io/ssl-redirect" = "true"
+ "nginx.ingress.kubernetes.io/proxy-body-size" = "4G"
+ "nginx.ingress.kubernetes.io/proxy-request-buffering" = "on"
+ "nginx.ingress.kubernetes.io/proxy-buffer-size" = "32k"
+ "nginx.ingress.kubernetes.io/proxy-buffers-number" = "16"
+ "nginx.ingress.kubernetes.io/server-snippet" = file("${path.module}/resources/nextcloud-server-snippet.conf")
+ }
+ }
+ spec {
+ tls {
+ hosts = [local.nextcloud_fqdn]
+ secret_name = "nextcloud-tls"
+ }
+ rule {
+ host = local.nextcloud_fqdn
+ http {
+ path {
+ backend {
+ service {
+ name = "http"
+ port {
+ number = 80
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+resource "kubernetes_deployment" "nextcloud_redis" {
+ metadata {
+ name = "redis"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+ spec {
+ selector {
+ match_labels = {
+ app = "redis"
+ }
+ }
+ strategy {
+ type = "RollingUpdate"
+ }
+ template {
+ metadata {
+ labels = {
+ app = "redis"
+ }
+ }
+ spec {
+ container {
+ name = "redis"
+ image = "redis:latest"
+
+ resources {
+ requests = {
+ memory = "20Mi"
+ }
+ limits = {
+ memory = "50Mi"
+ }
+ }
+
+ readiness_probe {
+ tcp_socket {
+ port = "redis"
+ }
+ initial_delay_seconds = 10
+ period_seconds = 15
+ success_threshold = 1
+ failure_threshold = 3
+ }
+
+ liveness_probe {
+ tcp_socket {
+ port = "redis"
+ }
+ initial_delay_seconds = 10
+ period_seconds = 15
+ success_threshold = 1
+ failure_threshold = 3
+ }
+
+ port {
+ container_port = 6379
+ name = "redis"
+ protocol = "TCP"
+ }
+ }
+ }
+ }
+ }
+}
+
+resource "kubernetes_deployment" "nextcloud_apache" {
+
+ depends_on = [
+ module.nextcloud_database,
+ kubernetes_deployment.nextcloud_redis
+ ]
+
+ metadata {
+ name = "nextcloud-app"
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ }
+ spec {
+ selector {
+ match_labels = {
+ app = "nextcloud"
+ }
+ }
+ strategy {
+ type = "Recreate"
+ }
+ template {
+ metadata {
+ labels = {
+ app = "nextcloud"
+ }
+ }
+ spec {
+ service_account_name = kubernetes_service_account.nextcloud_service_account.metadata[0].name
+
+ container {
+ name = "nextcloud"
+ image = "nextcloud:${local.nextcloud_version}"
+ image_pull_policy = "Always"
+
+ resources {
+ requests = {
+ memory = "200Mi"
+ }
+ }
+
+ readiness_probe {
+ http_get {
+ port = "http"
+ path = "/status.php"
+ }
+ initial_delay_seconds = 30
+ period_seconds = 10
+ success_threshold = 1
+ failure_threshold = 3
+ }
+
+ liveness_probe {
+ tcp_socket {
+ port = "http"
+ }
+ initial_delay_seconds = 5
+ period_seconds = 10
+ success_threshold = 1
+ failure_threshold = 3
+ }
+
+ dynamic "env" {
+ for_each = local.nextcloud_env_vars
+ content {
+ name = env.key
+ value = env.value
+ }
+ }
+
+ dynamic "env" {
+ for_each = local.nextcloud_secrets
+ content {
+ name = env.key
+ value_from {
+ secret_key_ref {
+ key = env.value.key
+ name = env.value.name
+ }
+ }
+ }
+ }
+
+ port {
+ container_port = 80
+ name = "http"
+ }
+
+ volume_mount {
+ mount_path = "/var/www/html"
+ name = "persistent-storage"
+ }
+ }
+ volume {
+ name = "persistent-storage"
+ host_path {
+ path = local.nextcloud_path
+ }
+ }
+ }
+ }
+ }
+ lifecycle {
+ replace_triggered_by = [
+ kubernetes_deployment.nextcloud_redis
+ ]
+ }
+}
+
+resource "kubernetes_cron_job_v1" "nextcloud_cron_apache" {
+ depends_on = [
+ kubernetes_deployment.nextcloud_apache
+ ]
+ metadata {
+ namespace = kubernetes_namespace.nextcloud.metadata[0].name
+ name = "cronjob-trigger"
+ }
+ spec {
+ schedule = "*/15 * * * *"
+ concurrency_policy = "Forbid"
+ failed_jobs_history_limit = 2
+ successful_jobs_history_limit = 2
+ job_template {
+ metadata {
+ labels = {
+ app = "cronjob-trigger"
+ }
+ }
+ spec {
+ backoff_limit = 1
+ parallelism = 1
+ completions = 1
+ active_deadline_seconds = 300
+ template {
+ metadata {
+ labels = {
+ app = "cronjob-trigger"
+ }
+ }
+ spec {
+ container {
+ name = "nextcloud"
+ image = "nextcloud:${local.nextcloud_version}"
+
+ command = ["curl"]
+ args = ["--fail", "-L", "http://http/cron.php"]
+ }
+ restart_policy = "Never"
+ termination_grace_period_seconds = 30
+ }
+ }
+ }
+ }
+ }
+ lifecycle {
+ replace_triggered_by = [
+ kubernetes_deployment.nextcloud_apache
+ ]
+ }
+}
diff --git a/terraform/k8sapps/operator.tf b/terraform/k8sapps/operator.tf
new file mode 100644
index 0000000..8362673
--- /dev/null
+++ b/terraform/k8sapps/operator.tf
@@ -0,0 +1,38 @@
+resource "kubernetes_service_account" "operator_service_account" {
+ metadata {
+ name = "operator"
+ namespace = "default"
+ }
+}
+
+resource "kubernetes_cluster_role_binding" "operator_admin_access" {
+
+ metadata {
+ name = "operator-admin-access"
+ }
+
+ role_ref {
+ api_group = "rbac.authorization.k8s.io"
+ kind = "ClusterRole"
+ name = "cluster-admin"
+ }
+
+ 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
+}
diff --git a/terraform/k8sapps/provider.tf b/terraform/k8sapps/provider.tf
new file mode 100644
index 0000000..e544d39
--- /dev/null
+++ b/terraform/k8sapps/provider.tf
@@ -0,0 +1,44 @@
+terraform {
+ required_version = "~>1.8"
+ required_providers {
+ random = {
+ source = "hashicorp/random"
+ version = "~>3.4"
+ }
+ null = {
+ source = "hashicorp/null"
+ version = "~>3.2"
+ }
+ external = {
+ source = "hashicorp/external"
+ version = "~>2.2"
+ }
+ time = {
+ source = "hashicorp/time"
+ version = "~>0.9"
+ }
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = "~>2.18"
+ }
+ scaleway = {
+ source = "scaleway/scaleway"
+ version = "~>2.13"
+ }
+ }
+}
+
+provider "kubernetes" {
+ config_path = "~/.kube/config"
+ config_context = "microk8s"
+ insecure = true
+}
+
+provider "scaleway" {
+ zone = "fr-par-2"
+ region = "fr-par"
+ access_key = var.scaleway_access_key
+ secret_key = var.scaleway_secret_key
+ organization_id = var.scaleway_organization_id
+}
+
diff --git a/terraform/k8sapps/resources/apache2/000-default.conf b/terraform/k8sapps/resources/apache2/000-default.conf
new file mode 100644
index 0000000..36e681d
--- /dev/null
+++ b/terraform/k8sapps/resources/apache2/000-default.conf
@@ -0,0 +1,29 @@
+
+ # The ServerName directive sets the request scheme, hostname and port that
+ # the server uses to identify itself. This is used when creating
+ # redirection URLs. In the context of virtual hosts, the ServerName
+ # specifies what hostname must appear in the request's Host: header to
+ # match this virtual host. For the default virtual host (this file) this
+ # value is not decisive as it is used as a last resort host regardless.
+ # However, you must set it for any further virtual host explicitly.
+ #ServerName www.example.com
+
+ ServerAdmin webmaster@localhost
+ DocumentRoot /var/www/html
+
+ # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
+ # error, crit, alert, emerg.
+ # It is also possible to configure the loglevel for particular
+ # modules, e.g.
+ #LogLevel info ssl:warn
+
+ ErrorLog ${APACHE_LOG_DIR}/error.log
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+ # For most configuration files from conf-available/, which are
+ # enabled or disabled at a global level, it is possible to
+ # include a line for only one particular virtual host. For example the
+ # following line enables the CGI configuration for this host only
+ # after it has been globally disabled with "a2disconf".
+ #Include conf-available/serve-cgi-bin.conf
+
diff --git a/terraform/k8sapps/resources/collabora/coolwsd.xml b/terraform/k8sapps/resources/collabora/coolwsd.xml
new file mode 100644
index 0000000..2eb441a
--- /dev/null
+++ b/terraform/k8sapps/resources/collabora/coolwsd.xml
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+
+
+ de_DE en_GB en_US es_ES fr_FR
+ it nl pt_BR pt_PT ru
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ 1
+
+
+ 4
+ 5
+
+
+ false
+ 96
+ 3600
+ 30
+ 300
+ false
+ 0
+ 8000
+ 0
+ 0
+ 100
+ 5
+ 100
+ 500
+ 5000
+
+
+ 10000
+ 60
+ 300
+ 3072
+ 85
+ 120
+
+
+
+
+ true
+ 120
+ 900
+
+
+
+
+
+ true
+
+ warning
+ trace
+ notice
+ fatal
+
+ false
+
+ -INFO-WARN
+
+
+ /var/log/coolwsd.log
+ never
+ timestamp
+ true
+ 10 days
+
+ 10
+ true
+
+ false
+
+
+ false
+ 82589933
+
+ false
+ false
+
+
+
+
+ /var/log/coolwsd.trace.json
+
+
+ false
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ all
+ any
+
+
+
+
+ 192\.168\.[0-9]{1,3}\.[0-9]{1,3}
+
+ ::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3}
+ 127\.0\.0\.1
+ ::ffff:127\.0\.0\.1
+ ::1
+
+ 172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3}
+
+ ::ffff:172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3}
+
+ 172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3}
+
+ ::ffff:172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3}
+
+ 172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3}
+
+ ::ffff:172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3}
+
+ 10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}
+
+ ::ffff:10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}
+
+
+
+
+
+
+
+
+
+
+ true
+
+ false
+ /etc/coolwsd/cert.pem
+ /etc/coolwsd/key.pem
+ /etc/coolwsd/ca-chain.cert.pem
+
+
+ 1000
+
+
+
+
+
+
+ false
+ 31536000
+
+
+
+
+
+ true
+ true
+ 1800
+ false
+ 1
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ default
+ true
+
+
+
+
+
+ 0
+
+ 900
+
+
+
+
+ https://cloud.alexpires.me
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+ true
+
+
+ true
+
+ false
+ admin
+
+ M*7#VayBh7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+ log
+
+
+
+
+
+
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/terraform/k8sapps/resources/m3uproxy/m3uproxy.json b/terraform/k8sapps/resources/m3uproxy/m3uproxy.json
new file mode 100644
index 0000000..f4815e5
--- /dev/null
+++ b/terraform/k8sapps/resources/m3uproxy/m3uproxy.json
@@ -0,0 +1,35 @@
+{
+ "port": 8080,
+ "stream_server": {
+ "playlist": "conf/playlist.json",
+ "epg": "https://m3upt.com/epg",
+ "no_service_image": "assets/no_service_pt.jpg",
+ "default_timeout": 3,
+ "num_workers": 10,
+ "hide_inactive": true,
+ "scan_time": 3600,
+ "kodi": true,
+ "force_https_url": true
+ },
+ "security": {
+ "geoip": {
+ "database": "GeoIP/GeoLite2-Country.mmdb",
+ "whitelist": [
+ "PT",
+ "ST",
+ "BE"
+ ],
+ "internal_networks": [
+ "10.1.0.0/16",
+ "10.152.183.0/24"
+ ]
+ }
+ },
+ "auth": {
+ "provider": "file",
+ "secret_key": "${m3uproxy_secret}",
+ "settings": {
+ "file_path": "cache/auth.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/terraform/k8sapps/resources/m3uproxy/playlist.json b/terraform/k8sapps/resources/m3uproxy/playlist.json
new file mode 100644
index 0000000..eaefe26
--- /dev/null
+++ b/terraform/k8sapps/resources/m3uproxy/playlist.json
@@ -0,0 +1,124 @@
+{
+ "providers": {
+ "m3upt": {
+ "provider": "file",
+ "config": {
+ "source": "https://m3upt.com/iptv"
+ }
+ },
+ "iptv_be": {
+ "provider": "file",
+ "config": {
+ "source": "https://raw.githubusercontent.com/iptv-org/iptv/master/streams/be.m3u"
+ }
+ },
+ "iptv_st": {
+ "provider": "file",
+ "config": {
+ "source": "https://raw.githubusercontent.com/iptv-org/iptv/master/streams/st.m3u"
+ }
+ },
+ "iptv_pt_samsung": {
+ "provider": "file",
+ "config": {
+ "source": "https://raw.githubusercontent.com/iptv-org/iptv/master/streams/pt_samsung.m3u"
+ }
+ },
+ "iptv_uk_rakuten": {
+ "provider": "file",
+ "config": {
+ "source": "https://raw.githubusercontent.com/iptv-org/iptv/master/streams/uk_rakuten.m3u"
+ }
+ },
+ "iptv_uk_samsung": {
+ "provider": "file",
+ "config": {
+ "source": "https://raw.githubusercontent.com/iptv-org/iptv/master/streams/uk_samsung.m3u"
+ }
+ }
+ },
+ "providers_priority": [
+ "m3upt",
+ "iptv_be",
+ "iptv_st",
+ "iptv_pt_samsung",
+ "iptv_uk_rakuten",
+ "iptv_uk_samsung"
+ ],
+ "overrides": {
+ "RTP1.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "RTP2.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "SIC.pt": {
+ "kodi": true,
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "TVI.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "RTP3.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "SICNoticias.pt": {
+ "headers": {
+ "Origin": "https://sicnoticias.pt",
+ "DNT": "1",
+ "Connection": "keep-alive"
+ },
+ "kodi": true,
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "CNNPortugal.pt": {
+ "kodi": true,
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "RTPMemoria.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "RTPAcores.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "RTPMadeira.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "RTPAfrica.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "SICNovelas.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "SICReplay.pt": {
+ "http_proxy": "http://proxy-pt.m3uproxy.svc.cluster.local:3128"
+ },
+ "Rádio Comercial": {
+ "disable_remap": true
+ },
+ "Rádio Comercial Dance": {
+ "disable_remap": true
+ },
+ "Rádio Comercial By Night": {
+ "disable_remap": true
+ },
+ "Rádio Comercial One Hit Wonders": {
+ "disable_remap": true
+ },
+ "Rádio Comercial Kids": {
+ "disable_remap": true
+ },
+ "Rádio Comercial Portugal": {
+ "disable_remap": true
+ },
+ "Rádio Comercial Brasil": {
+ "disable_remap": true
+ },
+ "Rádio Comercial 90s": {
+ "disable_remap": true
+ },
+ "Rádio Comercial 2000s": {
+ "disable_remap": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/terraform/k8sapps/resources/nextcloud-server-snippet.conf b/terraform/k8sapps/resources/nextcloud-server-snippet.conf
new file mode 100644
index 0000000..a58c840
--- /dev/null
+++ b/terraform/k8sapps/resources/nextcloud-server-snippet.conf
@@ -0,0 +1,27 @@
+server_tokens off;
+proxy_hide_header X-Powered-By;
+
+rewrite ^/.well-known/webfinger /index.php/.well-known/webfinger last;
+rewrite ^/.well-known/nodeinfo /index.php/.well-known/nodeinfo last;
+
+location = /.well-known/carddav {
+ return 301 $scheme://$host/remote.php/dav;
+}
+
+location = /.well-known/caldav {
+ return 301 $scheme://$host/remote.php/dav;
+}
+
+location = /robots.txt {
+ allow all;
+ log_not_found off;
+ access_log off;
+}
+
+location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {
+ deny all;
+}
+
+location ~ ^/(?:autotest|occ|issue|indie|db_|console) {
+ deny all;
+}
\ No newline at end of file
diff --git a/terraform/k8sapps/resources/squid/squid.conf b/terraform/k8sapps/resources/squid/squid.conf
new file mode 100644
index 0000000..2dd6b07
--- /dev/null
+++ b/terraform/k8sapps/resources/squid/squid.conf
@@ -0,0 +1,45 @@
+acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
+acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
+acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
+acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
+acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN)
+acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
+acl localnet src fc00::/7 # RFC 4193 local private network range
+acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
+acl SSL_ports port 443
+acl Safe_ports port 80 # http
+acl Safe_ports port 21 # ftp
+acl Safe_ports port 443 # https
+acl Safe_ports port 70 # gopher
+acl Safe_ports port 210 # wais
+acl Safe_ports port 1025-65535 # unregistered ports
+acl Safe_ports port 280 # http-mgmt
+acl Safe_ports port 488 # gss-http
+acl Safe_ports port 591 # filemaker
+acl Safe_ports port 777 # multiling http
+acl CONNECT method CONNECT
+http_access deny !Safe_ports
+http_access deny CONNECT !SSL_ports
+http_access allow localhost manager
+http_access deny manager
+http_access allow localhost
+http_access allow localnet
+http_access deny all
+http_port 3128
+dns_v4_first on
+coredump_dir /var/spool/squid
+refresh_pattern ^ftp: 1440 20% 10080
+refresh_pattern ^gopher: 1440 0% 1440
+refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
+refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
+refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
+refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
+refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
+refresh_pattern . 0 20% 4320
+logfile_rotate 0
+via off
+forwarded_for delete
+follow_x_forwarded_for deny all
+pipeline_prefetch on
+request_header_access From deny all
+request_header_access Server deny all
\ No newline at end of file
diff --git a/terraform/k8sapps/resources/wireguard/wireguard.conf.tpl b/terraform/k8sapps/resources/wireguard/wireguard.conf.tpl
new file mode 100644
index 0000000..5f5d5a1
--- /dev/null
+++ b/terraform/k8sapps/resources/wireguard/wireguard.conf.tpl
@@ -0,0 +1,9 @@
+[Interface]
+PrivateKey = ${wireguard_private_key}
+Address = ${wireguard_address}
+DNS = ${wireguard_dns}
+
+[Peer]
+PublicKey = ${wireguard_public_key}
+AllowedIPs = ${wireguard_allowed_ips}
+Endpoint = ${wireguard_endpoint}
diff --git a/terraform/k8sapps/sectool.env b/terraform/k8sapps/sectool.env
new file mode 100644
index 0000000..b74df49
--- /dev/null
+++ b/terraform/k8sapps/sectool.env
@@ -0,0 +1,16 @@
+AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY
+AWS_SECRET_ACCESS_KEY=$AWS_SECRET_KEY
+TF_VAR_scaleway_access_key=$SCALEWAY_ACCESS_KEY
+TF_VAR_scaleway_secret_key=$SCALEWAY_SECRET_KEY
+TF_VAR_scaleway_project_id=$SCALEWAY_PROJECT_ID
+TF_VAR_scaleway_organization_id=$SCALEWAY_ORGANIZATION_ID
+TF_VAR_mysql_root_password=$MYSQL_ROOT_PASSWORD
+TF_VAR_wordpress_mysql_password=$MYSQL_WORDPRESS_PASSWORD
+TF_VAR_gitea_mysql_password=$MYSQL_GITEA_PASSWORD
+TF_VAR_nextcloud_mysql_password=$MYSQL_NEXTCLOUD_PASSWORD
+TF_VAR_nextcloud_admin_password=$NEXTCLOUD_ADMIN_PASSWORD
+TF_VAR_collabora_admin_password=$NEXTCLOUD_ADMIN_PASSWORD
+TF_VAR_wireguard_private_key=$WIREGUARD_PRIVATE_KEY
+TF_VAR_wireguard_public_key=$WIREGUARD_PUBLIC_KEY
+TF_VAR_m3uproxy_admin_password=$M3UPROXY_ADMIN_PASSWORD
+TF_VAR_m3uproxy_secret=$M3UPROXY_SECRET
\ No newline at end of file
diff --git a/terraform/k8sapps/variables.tf b/terraform/k8sapps/variables.tf
new file mode 100644
index 0000000..e5ee846
--- /dev/null
+++ b/terraform/k8sapps/variables.tf
@@ -0,0 +1,69 @@
+variable "wordpress_mysql_password" {
+ description = "MySQL access password"
+ type = string
+}
+
+variable "mysql_root_password" {
+ description = "MySQL root password"
+ type = string
+}
+
+variable "gitea_mysql_password" {
+ description = "MySQL access password"
+ type = string
+}
+
+variable "scaleway_access_key" {
+ description = "Scaleway Access key"
+ type = string
+}
+
+variable "scaleway_secret_key" {
+ description = "Scaleway Secret key"
+ type = string
+}
+
+variable "scaleway_project_id" {
+ description = "Scaleway Project Id"
+ type = string
+}
+
+variable "scaleway_organization_id" {
+ description = "Scaleway Organization Id"
+ type = string
+}
+
+variable "nextcloud_mysql_password" {
+ description = "MySQL access password"
+ type = string
+}
+
+variable "nextcloud_admin_password" {
+ description = "Nextcloud access password"
+ type = string
+}
+
+variable "collabora_admin_password" {
+ description = "Collabora access password"
+ type = string
+}
+
+variable "wireguard_private_key" {
+ description = "Wireguard public key"
+ type = string
+}
+
+variable "wireguard_public_key" {
+ description = "Wireguard public key"
+ type = string
+}
+
+variable "m3uproxy_admin_password" {
+ description = "M3UProxy access password"
+ type = string
+}
+
+variable "m3uproxy_secret" {
+ description = "M3UProxy HMAC secret"
+ type = string
+}
diff --git a/terraform/k8sapps/wordpress.tf b/terraform/k8sapps/wordpress.tf
new file mode 100644
index 0000000..27b47ed
--- /dev/null
+++ b/terraform/k8sapps/wordpress.tf
@@ -0,0 +1,230 @@
+locals {
+ wordpress_path = "/var/lib/wordpress"
+ wordpress_mysql_path = "/var/lib/mysql.wordpress"
+}
+
+resource "kubernetes_namespace" "wordpress" {
+ metadata {
+ annotations = {
+ name = "wordpress"
+ }
+
+ labels = {
+ name = "wordpress"
+ }
+
+ name = "wordpress"
+ }
+}
+
+
+#
+# Setup MariaDB database
+#
+module "wordpress_database" {
+ source = "./modules/k8s_mariadb"
+
+ namespace = kubernetes_namespace.wordpress.metadata[0].name
+ app_name = "wordpress"
+ app_backup_folder = "wordpress.backups"
+
+ app_password = var.wordpress_mysql_password
+ root_password = var.mysql_root_password
+
+ retention_days = local.local_backup_retention_days
+ cron_schedule = local.wordpress_sql_backup_cron_schedule
+ host_storage_path = local.wordpress_mysql_path
+}
+
+
+#
+# Setup rsync backup job to scaleway bucket
+#
+module "wordpress_backup" {
+ source = "./modules/scw_backup_job"
+
+ namespace = kubernetes_namespace.wordpress.metadata[0].name
+ app_name = "wordpress"
+ app_files_folder = "wordpress"
+ app_backup_folder = "mysql.wordpress/wordpress.backups"
+
+ scaleway_organization_id = var.scaleway_organization_id
+ scaleway_project_id = var.scaleway_organization_id
+ access_key = scaleway_iam_api_key.backup_app_api.access_key
+ secret_key = scaleway_iam_api_key.backup_app_api.secret_key
+ bucket_name = scaleway_object_bucket.backup.name
+
+ retention_days = local.local_backup_retention_days
+ cron_schedule = local.wordpress_files_backup_cron_schedule
+}
+
+#
+# Gitea service, ingress, deployment and file backup job
+#
+resource "kubernetes_service_account" "wordpress_service_account" {
+
+ metadata {
+ name = "wordpress-app-sa"
+ namespace = kubernetes_namespace.wordpress.metadata[0].name
+ }
+
+ automount_service_account_token = false
+}
+
+
+resource "kubernetes_service" "wordpress" {
+ metadata {
+ name = "http"
+ namespace = kubernetes_namespace.wordpress.metadata[0].name
+ }
+ spec {
+ port {
+ port = 80
+ target_port = "http"
+ }
+ selector = {
+ app = "wordpress"
+ }
+ cluster_ip = "None"
+ }
+}
+
+resource "kubernetes_ingress_v1" "wordpress_ingress" {
+ metadata {
+ name = "wordpress-ingress"
+ namespace = kubernetes_namespace.wordpress.metadata[0].name
+
+ annotations = {
+ "kubernetes.io/ingress.class" = "public"
+ "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
+ }
+ }
+ spec {
+ tls {
+ hosts = [local.wordpress_fqdn]
+ secret_name = "wordpress-le-tls"
+ }
+ rule {
+ host = local.wordpress_fqdn
+ http {
+ path {
+ backend {
+ service {
+ name = "http"
+ port {
+ number = 80
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+resource "kubernetes_deployment" "wordpress" {
+
+ depends_on = [module.wordpress_database]
+
+ metadata {
+ name = "wordpress-app"
+ namespace = kubernetes_namespace.wordpress.metadata[0].name
+ }
+ spec {
+ selector {
+ match_labels = {
+ app = "wordpress"
+ }
+ }
+ strategy {
+ type = "RollingUpdate"
+ }
+ template {
+ metadata {
+ labels = {
+ app = "wordpress"
+ }
+ }
+ spec {
+ service_account_name = kubernetes_service_account.wordpress_service_account.metadata[0].name
+
+ container {
+ name = "wordpress"
+ image = "wordpress:${local.wordpress_version}"
+ image_pull_policy = "IfNotPresent"
+
+ resources {
+ requests = {
+ memory = "100Mi"
+ }
+ limits = {
+ memory = "500Mi"
+ }
+ }
+
+ readiness_probe {
+ http_get {
+ path = "/?nocache"
+ port = "http"
+ }
+ initial_delay_seconds = 20
+ period_seconds = 10
+ success_threshold = 1
+ failure_threshold = 5
+ }
+
+ liveness_probe {
+ tcp_socket {
+ port = "http"
+ }
+ initial_delay_seconds = 5
+ period_seconds = 10
+ success_threshold = 1
+ failure_threshold = 3
+ }
+
+ env {
+ name = "WORDPRESS_DB_NAME"
+ value = "wordpress"
+ }
+ env {
+ name = "WORDPRESS_DB_HOST"
+ value = "${module.wordpress_database.host}:${module.wordpress_database.port}"
+ }
+ env {
+ name = "WORDPRESS_DB_USER"
+ value_from {
+ secret_key_ref {
+ key = "username"
+ name = module.wordpress_database.app_crendentials_name
+ }
+ }
+ }
+ env {
+ name = "WORDPRESS_DB_PASSWORD"
+ value_from {
+ secret_key_ref {
+ key = "password"
+ name = module.wordpress_database.app_crendentials_name
+ }
+ }
+ }
+ port {
+ container_port = 80
+ name = "http"
+ }
+ volume_mount {
+ mount_path = "/var/www/html"
+ name = "persistent-storage"
+ }
+ }
+ volume {
+ name = "persistent-storage"
+ host_path {
+ path = local.wordpress_path
+ }
+ }
+ }
+ }
+ }
+}