Add Nextcloud deployment and configuration

- Introduced Nextcloud module with Kubernetes resources including deployment, service, ingress, and cron job.
- Configured Apache settings for Nextcloud with SSL support and compression.
- Created secrets for mail and admin credentials.
- Added necessary environment variables for Nextcloud configuration.
- Removed unused Scaleway access and secret keys from secrets and variables.
- Updated CoreDNS module to support custom host overrides.
- Implemented backup bucket policies and lifecycle rules for Scaleway object storage.
- Enhanced IAM policies and applications for backup and email services.
- Added outputs for backup bucket and API keys.
This commit is contained in:
2025-06-09 23:49:43 +02:00
parent cac269f160
commit 826959c903
41 changed files with 1186 additions and 219 deletions
+2
View File
@@ -2,6 +2,8 @@
locals {
configfile = templatefile("${path.module}/resources/Corefile.tpl", {
zones = var.zones
override_hosts = length(var.override_hosts) > 0
hosts = var.override_hosts
forward_dns_servers = join(" ", var.forward_dns_servers)
})
configfile_checksum = md5(local.configfile)
@@ -1,4 +1,12 @@
.:53 {
%{ if override_hosts ~}
hosts {
%{ for h in hosts ~}
${h.ip} ${h.name}
%{ endfor ~}
fallthrough
}
%{ endif ~}
errors
health {
lameduck 5s
@@ -10,6 +10,15 @@ variable "forward_dns_servers" {
default = ["8.8.8.8", "8.8.4.4"]
}
variable "override_hosts" {
description = "Override hosts"
type = list(object({
ip = string
name = string
}))
default = []
}
variable "zones" {
description = "Zones to be managed by coredns"
type = list(object({
@@ -0,0 +1,90 @@
locals {
persistent_folder = var.persistent_folder
database_credentials = {
MYSQL_USER = {
key = "username"
name = var.app_config.db_user
}
MYSQL_PASSWORD = {
key = "password"
name = var.app_config.db_password
}
}
nextcloud_env_vars = merge({
# Database configuration
MYSQL_DATABASE = var.app_config.db_name
MYSQL_HOST = var.app_config.db_host
# Email configuration
SMTP_HOST = var.app_config.smtp_host
SMTP_SECURE = var.app_config.smtp_secure
SMTP_PORT = var.app_config.smtp_port
MAIL_FROM_ADDRESS = var.app_config.mail_from
MAIL_DOMAIN = var.app_config.mail_domain
SMTP_AUTHTYPE = "PLAIN"
# Redis configuration
REDIS_HOST = "redis"
# General
NEXTCLOUD_TRUSTED_DOMAINS = var.fqdn
TRUSTED_PROXIES = join(" ", ["10.1.0.0/16"])
OVERWRITEHOST = var.fqdn
OVERWRITEPROTOCOL = "https"
OVERWRITECLIURL = "https://${var.fqdn}"
# PHP
PHP_MEMORY_LIMIT = "512M"
}, var.db_credentials_are_secrets ? {} : local.database_credentials)
nextcloud_secrets = merge({
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
}
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
}
}, var.db_credentials_are_secrets ? local.database_credentials : {})
nextcloud_data = "${local.persistent_folder}/nextcloud"
apache_confs = {
for f in fileset("${path.module}/resources/apache2/conf-enabled/", "*.conf") :
"${f}" => file("${path.module}/resources/apache2/conf-enabled//${f}")
}
apache_mods_load = {
for f in fileset("${path.module}/resources/apache2/mods-enabled/", "*.cload") :
"${f}" => file("${path.module}/resources/apache2/mods-enabled//${f}")
}
apache_mods_conf = {
for f in fileset("${path.module}/resources/apache2/mods-enabled/", "*.conf") :
"${f}" => file("${path.module}/resources/apache2/mods-enabled//${f}")
}
apache_sites = {
for f in fileset("${path.module}/resources/apache2/sites-enabled/", "*.conf") :
"${f}" => file("${path.module}/resources/apache2/sites-enabled//${f}")
}
apache_confs_checksum = sha256(jsonencode(local.apache_confs))
apache_sites_checksum = sha256(jsonencode(local.apache_sites))
apache_mods_load_checksum = sha256(jsonencode(local.apache_mods_load))
apache_mods_conf_checksum = sha256(jsonencode(local.apache_mods_conf))
}
+54
View File
@@ -0,0 +1,54 @@
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:${var.tag}"
command = ["curl"]
args = ["--fail", "-L", "http://http/cron.php"]
security_context {
allow_privilege_escalation = false
}
}
restart_policy = "Never"
termination_grace_period_seconds = 30
}
}
}
}
}
lifecycle {
replace_triggered_by = [
kubernetes_deployment.nextcloud_apache
]
}
}
@@ -0,0 +1,67 @@
resource "kubernetes_service" "nextcloud_apache" {
metadata {
name = "http"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
spec {
port {
port = var.tls_enabled ? 443 : 80
target_port = var.tls_enabled ? 443 : 80
}
selector = {
app = "nextcloud"
}
cluster_ip = "None"
}
}
resource "kubernetes_ingress_v1" "nextcloud_ingress" {
depends_on = [
kubernetes_deployment.nextcloud_apache
]
metadata {
name = "ingress"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
annotations = merge({
"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")
},
var.tls_enabled ? {
"nginx.org/ssl-services" = "http"
"nginx.org/ssl-redirect" = "true"
"nginx.org/redirect-to-https" = "true"
"nginx.ingress.kubernetes.io/backend-protocol" = "HTTPS"
} : {}
)
}
spec {
tls {
hosts = [var.fqdn]
secret_name = "nextcloud-tls"
}
rule {
host = var.fqdn
http {
path {
backend {
service {
name = "http"
port {
number = var.tls_enabled ? 443 : 80
}
}
}
}
}
}
}
}
+285
View File
@@ -0,0 +1,285 @@
resource "kubernetes_namespace" "nextcloud" {
metadata {
annotations = {
name = "nextcloud"
}
labels = {
name = "nextcloud"
}
name = "nextcloud"
}
}
resource "kubernetes_manifest" "internal_cert" {
count = var.tls_enabled ? 1 : 0
manifest = {
apiVersion = "cert-manager.io/v1"
kind = "Certificate"
metadata = {
name = "internal-cert"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
spec = {
secretName = "internal-cert"
issuerRef = {
name = var.issuer
kind = "ClusterIssuer"
}
commonName = var.fqdn
dnsNames = [var.fqdn]
}
}
}
resource "kubernetes_config_map" "apache_confs" {
metadata {
name = "nextcloud-apache-confs"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
data = local.apache_confs
}
resource "kubernetes_config_map" "apache_mods_load" {
metadata {
name = "nextcloud-apache-mods-load"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
data = local.apache_mods_load
}
resource "kubernetes_config_map" "apache_mods_conf" {
metadata {
name = "nextcloud-apache-mods-enabled"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
data = local.apache_mods_conf
}
resource "kubernetes_config_map" "apache_sites" {
metadata {
name = "nextcloud-apache-sites"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
data = local.apache_sites
}
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_deployment" "nextcloud_apache" {
depends_on = [
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"
}
annotations = {
"checksum/apache_confs" = local.apache_confs_checksum
"checksum/apache_sites" = local.apache_sites_checksum
"checksum/apache_mods_load" = local.apache_mods_load_checksum
"checksum/apache_mods_conf" = local.apache_mods_conf_checksum
}
}
spec {
service_account_name = kubernetes_service_account.nextcloud_service_account.metadata[0].name
container {
name = "nextcloud"
image = "nextcloud:${var.tag}"
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
}
}
security_context {
allow_privilege_escalation = false
}
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"
}
dynamic "port" {
for_each = var.tls_enabled ? [1] : []
content {
container_port = 443
name = "https"
}
}
volume_mount {
mount_path = "/var/www/html"
name = "html-data"
}
dynamic "volume_mount" {
for_each = local.apache_confs
content {
mount_path = "/etc/apache2/conf-enabled/${volume_mount.key}"
name = "apache-confs"
sub_path = volume_mount.key
read_only = true
}
}
dynamic "volume_mount" {
for_each = local.apache_sites
content {
mount_path = "/etc/apache2/sites-enabled/${volume_mount.key}"
name = "apache-sites"
sub_path = volume_mount.key
read_only = true
}
}
dynamic "volume_mount" {
for_each = local.apache_mods_load
content {
mount_path = "/etc/apache2/mods-enabled/${volume_mount.key}"
name = "apache-mods-load"
sub_path = volume_mount.key
read_only = true
}
}
dynamic "volume_mount" {
for_each = local.apache_mods_conf
content {
mount_path = "/etc/apache2/mods-enabled/${volume_mount.key}"
name = "apache-mods-conf"
sub_path = volume_mount.key
read_only = true
}
}
dynamic "volume_mount" {
for_each = var.tls_enabled ? [1] : []
content {
mount_path = "/etc/ssl/certs/private"
name = "internal-cert"
read_only = true
}
}
}
volume {
name = "html-data"
host_path {
path = local.nextcloud_data
}
}
volume {
name = "apache-confs"
config_map {
name = kubernetes_config_map.apache_confs.metadata[0].name
}
}
volume {
name = "apache-sites"
config_map {
name = kubernetes_config_map.apache_sites.metadata[0].name
}
}
volume {
name = "apache-mods-load"
config_map {
name = kubernetes_config_map.apache_mods_load.metadata[0].name
}
}
volume {
name = "apache-mods-conf"
config_map {
name = kubernetes_config_map.apache_mods_conf.metadata[0].name
}
}
dynamic "volume" {
for_each = var.tls_enabled ? [1] : []
content {
name = "internal-cert"
secret {
secret_name = kubernetes_manifest.internal_cert[0].manifest["metadata"]["name"]
items {
key = "tls.crt"
path = "tls.crt"
}
items {
key = "tls.key"
path = "tls.key"
}
}
}
}
}
}
}
lifecycle {
replace_triggered_by = [
kubernetes_deployment.nextcloud_redis
]
}
}
@@ -0,0 +1,4 @@
output "namespace" {
description = "value of namespace"
value = kubernetes_namespace.nextcloud.metadata[0].name
}
@@ -0,0 +1,9 @@
terraform {
required_version = "~>1.8"
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "2.36.0"
}
}
}
+76
View File
@@ -0,0 +1,76 @@
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_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"
security_context {
allow_privilege_escalation = false
}
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"
}
}
}
}
}
}
@@ -0,0 +1,14 @@
<IfModule mod_deflate.c>
# Enable compression for the following content types
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE text/xml
</IfModule>
# Do not compress images
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|woff|woff2)$ no-gzip
@@ -0,0 +1 @@
MaxRequestWorkers 10
@@ -0,0 +1,86 @@
LoadModule socache_shmcb_module /usr/lib/apache2/modules/mod_socache_shmcb.so
LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
# Pseudo Random Number Generator (PRNG):
# Configure one or more sources to seed the PRNG of the SSL library.
# The seed data should be of good random quality.
# WARNING! On some platforms /dev/random blocks if not enough entropy
# is available. This means you then cannot use the /dev/random device
# because it would lead to very long connection times (as long as
# it requires to make more entropy available). But usually those
# platforms additionally provide a /dev/urandom device which doesn't
# block. So, if available, use this one instead. Read the mod_ssl User
# Manual for more details.
#
SSLRandomSeed startup builtin
SSLRandomSeed startup file:/dev/urandom 512
SSLRandomSeed connect builtin
SSLRandomSeed connect file:/dev/urandom 512
##
## SSL Global Context
##
## All SSL configuration in this context applies both to
## the main server and all SSL-enabled virtual hosts.
##
#
# Some MIME-types for downloading Certificates and CRLs
#
AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl .crl
# Pass Phrase Dialog:
# Configure the pass phrase gathering process.
# The filtering dialog program (`builtin' is a internal
# terminal dialog) has to provide the pass phrase on stdout.
SSLPassPhraseDialog exec:/usr/share/apache2/ask-for-passphrase
# Inter-Process Session Cache:
# Configure the SSL Session Cache: First the mechanism
# to use and second the expiring timeout (in seconds).
# (The mechanism dbm has known memory leaks and should not be used).
#SSLSessionCache dbm:${APACHE_RUN_DIR}/ssl_scache
SSLSessionCache shmcb:${APACHE_RUN_DIR}/ssl_scache(512000)
SSLSessionCacheTimeout 300
# Semaphore:
# Configure the path to the mutual exclusion semaphore the
# SSL engine uses internally for inter-process synchronization.
# (Disabled by default, the global Mutex directive consolidates by default
# this)
#Mutex file:${APACHE_LOCK_DIR}/ssl_mutex ssl-cache
# SSL Cipher Suite:
# List the ciphers that the client is permitted to negotiate. See the
# ciphers(1) man page from the openssl package for list of all available
# options.
# Enable only secure ciphers:
SSLCipherSuite HIGH:!aNULL
# SSL server cipher order preference:
# Use server priorities for cipher algorithm choice.
# Clients may prefer lower grade encryption. You should enable this
# option if you want to enforce stronger encryption, and can afford
# the CPU cost, and did not override SSLCipherSuite in a way that puts
# insecure ciphers first.
# Default: Off
#SSLHonorCipherOrder on
# The protocols to enable.
# Available values: all, SSLv3, TLSv1, TLSv1.1, TLSv1.2
# SSL v2 is no longer supported
SSLProtocol all -SSLv3
# Allow insecure renegotiation with clients which do not yet support the
# secure renegotiation protocol. Default: Off
#SSLInsecureRenegotiation on
# Whether to forbid non-SNI clients to access name based virtual hosts.
# Default: Off
#SSLStrictSNIVHostCheck On
# Warning: Session Tickets require regular reloading of the server!
# Make sure you do this (e.g. via logrotate) before changing this setting!
SSLSessionTickets off
@@ -0,0 +1,11 @@
<VirtualHost *:443>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLEngine on
SSLCertificateFile /etc/ssl/certs/private/tls.crt
SSLCertificateKeyFile /etc/ssl/certs/private/tls.key
</VirtualHost>
@@ -0,0 +1,32 @@
server_tokens off;
proxy_hide_header X-Powered-By;
location = /.well-known/webfinger {
return 301 $scheme://$host/index.php/.well-known/webfinger;
}
location = /.well-known/nodeinfo {
return 301 $scheme://$host/index.php/.well-known/nodeinfo;
}
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;
}
@@ -0,0 +1,24 @@
resource "kubernetes_secret" "nextcloud_mail_credentials" {
metadata {
name = "nextcloud-mail-credentials"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
data = {
username = var.app_config.smtp_user
password = var.app_config.smtp_password
}
type = "Opaque"
}
resource "kubernetes_secret" "nextcloud_admin_credentials" {
metadata {
name = "nextcloud-admin-credentials"
namespace = kubernetes_namespace.nextcloud.metadata[0].name
}
data = {
username = var.app_config.admin_username
password = var.app_config.admin_password
}
type = "Opaque"
}
@@ -0,0 +1,52 @@
variable "persistent_folder" {
description = "The path to the persistent folder"
type = string
}
variable "tag" {
description = "The version of the websites to install"
type = string
default = "30-apache"
}
variable "fqdn" {
description = "The fully qualified domain name for the Rustdesk server"
type = string
}
variable "tls_enabled" {
description = "Enable TLS for the SFTPGo server"
type = bool
default = true
}
variable "issuer" {
description = "The issuer for the certificate"
type = string
default = "internal-ca"
}
variable "db_credentials_are_secrets" {
description = "set to true if the database credentials are secrets"
type = bool
default = false
}
variable "app_config" {
description = "The configuration for the SFTPGo server"
type = object({
mail_from = string
mail_domain = string
smtp_user = string
smtp_password = string
smtp_host = string
admin_password = string
db_host = string
db_name = string
db_user = string
db_password = string
smtp_port = optional(number, 465)
smtp_secure = optional(string, "ssl")
admin_username = optional(string, "admin")
})
}
@@ -136,12 +136,14 @@ resource "kubernetes_ingress_v1" "reverse-proxy_ingress" {
"kubernetes.io/ingress.class" = "public"
"cert-manager.io/cluster-issuer" = "letsencrypt-prod"
},
each.value.custom_ingress_annotations,
each.value.tls ? {
"nginx.org/ssl-services" = format("reverse-proxy-%s", replace(each.key, ".", "-"))
"nginx.org/ssl-redirect" = "true"
"nginx.org/redirect-to-https" = "true"
"nginx.ingress.kubernetes.io/backend-protocol" = "HTTPS"
} : {})
} : {}
)
}
spec {
tls {
@@ -29,13 +29,15 @@ variable "issuer_name" {
variable "services" {
description = "A list of services to be deployed"
type = map(object({
fqdn = list(string)
upstream = optional(string, "")
resolver = optional(string, "")
default_headers = optional(map(string), {}),
http_port = optional(number, 80)
https_port = optional(number, 443)
tls = optional(bool, false)
fqdn = list(string)
custom_ingress_annotations = optional(map(string), {})
annotations = optional(map(string), {})
upstream = optional(string, "")
resolver = optional(string, "")
default_headers = optional(map(string), {}),
http_port = optional(number, 80)
https_port = optional(number, 443)
tls = optional(bool, false)
locations = optional(list(object({
path = string,
headers = optional(map(string), {}),
+4
View File
@@ -13,3 +13,7 @@ output "root_crendentials_name" {
output "app_crendentials_name" {
value = kubernetes_secret.app_password.metadata[0].name
}
output "db_name" {
value = var.app_name
}