feat: Enhance website deployment and build process

- Added rsync installation in setup-env action for file synchronization.
- Excluded playbook_websites.yml from ansible-apply-playbook workflow.
- Created a new workflow for building and releasing the Hugo website.
- Added submodule for Hugo theme in .gitmodules.
- Updated host_vars for production to include websites configuration.
- Introduced playbook_websites.yml for managing static files deployment.
- Implemented hugo_build script for building and archiving Hugo sites.
- Updated inventory to include a new web group for website deployment.
- Refactored Terraform configuration to support new website structure.
- Removed deprecated WordPress Terraform configuration.
- Added .gitignore files for websites and specific Hugo site.
- Created archetype and configuration files for the Hugo site.
- Added static assets including CV and images for the Hugo site.
- Integrated Hugo profile theme as a submodule for the website.
This commit is contained in:
2025-04-27 14:03:56 +02:00
parent 2fbe4dd1cc
commit 4e5346e2e0
24 changed files with 850 additions and 258 deletions
+12 -11
View File
@@ -1,25 +1,26 @@
locals {
# general
domain_name = "alexpires.me"
# storage
persistent_folder = "/mnt/microk8s_persistent"
database_folder = "/var/lib"
# apps
wordpress_version = "6.7.2"
wordpress_fqdns = ["blog.${local.domain_name}", "www.${local.domain_name}", "${local.domain_name}"]
wordpress_sql_backup_cron_schedule = "0 2 * * *"
wordpress_files_backup_cron_schedule = "10 2 * * *"
# domains
primary_domain = "alexpires.me"
websites = {
"alexpires.me" = {
domain_name = "alexpires.me"
fqdn = ["blog.alexpires.me", "www.alexpires.me", "alexpires.me"]
}
}
gitea_version = "1.23.4"
gitea_fqdn = "code.${local.domain_name}"
gitea_fqdn = "code.${local.primary_domain}"
gitea_sql_backup_cron_schedule = "20 2 * * *"
gitea_files_backup_cron_schedule = "30 2 * * *"
nextcloud_version = "30.0.0-apache"
nextcloud_fqdn = "cloud.${local.domain_name}"
nextcloud_fqdn = "cloud.${local.primary_domain}"
nextcloud_sql_backup_cron_schedule = "40 2 * * *"
nextcloud_files_backup_cron_schedule = "50 2 * * *"
@@ -34,7 +35,7 @@ locals {
m3uproxy_version = "latest"
wireguard_version = "1.0.20210914-r4-ls54"
squid_version = "6.1-23.10_edge"
m3uproxy_fqdn = "tv.${local.domain_name}"
m3uproxy_fqdn = "tv.${local.primary_domain}"
geoip_image = "maxmindinc/geoipupdate"
geoip_version = "latest"
@@ -47,7 +48,7 @@ locals {
# rustdesk
rustdesk_version = "latest"
rustdesk_fqdn = "rustdesk.${local.domain_name}"
rustdesk_fqdn = "rustdesk.${local.primary_domain}"
# backup
local_backup_retention_days = 2
+4 -4
View File
@@ -25,10 +25,10 @@ locals {
# Email configuration
SMTP_HOST = local.scaleway_smtp_host
SMTP_PORT = 465
MY_PRIMARY_DOMAIN = local.domain_name
MY_PRIMARY_DOMAIN = local.primary_domain
MY_DESTINATION = join(",", [for account in local.email_accounts : account.domain])
MY_NETWORKS = "127.0.0.0/8"
MAILNAME = "smtp.${local.domain_name}"
MAILNAME = "smtp.${local.primary_domain}"
DOVECOT_AUTH_ADDRESS = "dovecot-auth.mail.svc.cluster.local"
DOVECOT_AUTH_PORT = 31000
DOVECOT_LMTP_ADDRESS = "dovecot-lmtp.mail.svc.cluster.local"
@@ -201,8 +201,8 @@ resource "kubernetes_manifest" "mail_certificate" {
name = "letsencrypt-prod"
kind = "ClusterIssuer"
}
commonName = "imap.${local.domain_name}"
dnsNames = ["imap.${local.domain_name}", "smtp.${local.domain_name}"]
commonName = "imap.${local.primary_domain}"
dnsNames = flatten([for account in local.email_accounts : ["imap.${account.domain}", "smtp.${account.domain}"]])
}
}
}
@@ -0,0 +1,35 @@
server {
listen 8080;
server_name localhost;
gzip on;
gzip_comp_level 9;
gzip_types
text/html
text/plain
text/css
text/js
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
root /tmp/site;
location / {
index index.html index.htm;
}
location ~* ^/([^/]+) {
index index.html index.htm;
error_page 404 = @error;
}
error_page 404 /404.html;
location @error {
try_files /$1/404.html /404.html =404;
}
}
@@ -0,0 +1,34 @@
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
proxy_temp_path /tmp/proxy_temp;
client_body_temp_path /tmp/client_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}
+201
View File
@@ -0,0 +1,201 @@
locals {
websites_path = "${local.persistent_folder}/web"
}
resource "kubernetes_namespace" "website" {
metadata {
annotations = {
name = "website"
}
labels = {
name = "website"
}
name = "website"
}
}
resource "kubernetes_service_account" "website_service_account" {
metadata {
name = "website-app-sa"
namespace = kubernetes_namespace.website.metadata[0].name
}
automount_service_account_token = false
}
resource "kubernetes_service" "website" {
for_each = local.websites
metadata {
name = format("website-%s", replace(each.key, ".", "-"))
namespace = kubernetes_namespace.website.metadata[0].name
}
spec {
port {
port = 80
target_port = "http"
}
selector = {
app = format("website-%s", replace(each.key, ".", "-"))
}
cluster_ip = "None"
}
}
resource "kubernetes_config_map" "nginx_config" {
metadata {
name = "nginx-config"
namespace = kubernetes_namespace.website.metadata[0].name
}
data = {
"nginx.conf" = file("${path.module}/resources/nginx/nginx.conf")
"default.conf" = file("${path.module}/resources/nginx/conf.d/default.conf")
}
}
resource "kubernetes_ingress_v1" "website_ingress" {
for_each = local.websites
metadata {
name = "website-ingress"
namespace = kubernetes_namespace.website.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "public"
"cert-manager.io/cluster-issuer" = "letsencrypt-prod"
}
}
spec {
tls {
hosts = each.value.fqdn
secret_name = format("website-%s-tls", replace(each.key, ".", "-"))
}
dynamic "rule" {
for_each = each.value.fqdn
content {
host = rule.value
http {
path {
backend {
service {
name = format("website-%s", replace(each.key, ".", "-"))
port {
number = 80
}
}
}
}
}
}
}
}
}
resource "kubernetes_deployment" "website" {
for_each = local.websites
metadata {
name = "website-app"
namespace = kubernetes_namespace.website.metadata[0].name
}
spec {
selector {
match_labels = {
app = format("website-%s", replace(each.key, ".", "-"))
}
}
strategy {
type = "RollingUpdate"
}
template {
metadata {
labels = {
app = format("website-%s", replace(each.key, ".", "-"))
}
}
spec {
service_account_name = kubernetes_service_account.website_service_account.metadata[0].name
security_context {
fs_group = 1000
run_as_user = 1000
}
container {
name = "website"
image = "nginx:stable-alpine"
image_pull_policy = "Always"
resources {
requests = {
memory = "100Mi"
}
limits = {
memory = "500Mi"
}
}
readiness_probe {
http_get {
path = "/"
port = 8080
}
initial_delay_seconds = 5
period_seconds = 10
}
liveness_probe {
http_get {
path = "/"
port = 8080
}
initial_delay_seconds = 5
period_seconds = 10
}
security_context {
allow_privilege_escalation = false
}
port {
container_port = 8080
name = "http"
}
volume_mount {
name = "nginx-config"
mount_path = "/etc/nginx/nginx.conf"
sub_path = "nginx.conf"
read_only = true
}
volume_mount {
name = "nginx-config"
mount_path = "/etc/nginx/conf.d/default.conf"
sub_path = "default.conf"
read_only = true
}
volume_mount {
name = "website"
mount_path = "/tmp/site"
read_only = true
}
}
volume {
name = "nginx-config"
config_map {
name = kubernetes_config_map.nginx_config.metadata[0].name
}
}
volume {
name = "website"
host_path {
path = format("%s/%s", local.websites_path, each.key)
}
}
}
}
}
}
-239
View File
@@ -1,239 +0,0 @@
locals {
wordpress_path = "${local.persistent_folder}/wordpress"
wordpress_db_path = "${local.database_folder}/mysql.wordpress"
wordpress_backup_path = "${local.persistent_folder}/backup.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_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
database_folder = local.wordpress_db_path
backup_folder = local.wordpress_backup_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"
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
source_folder = local.wordpress_path
target_folder = local.wordpress_backup_path
}
#
# 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_fqdns
secret_name = "wordpress-le-tls"
}
dynamic "rule" {
for_each = local.wordpress_fqdns
content {
host = rule.value
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 = 30
# period_seconds = 60
# success_threshold = 1
# failure_threshold = 10
# }
security_context {
allow_privilege_escalation = false
}
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
}
}
}
}
}
}