Compare commits

..
27 changed files with 727 additions and 49 deletions
+8 -4
View File
@@ -1,3 +1,8 @@
# syntax=docker/dockerfile:1
FROM docker
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
RUN docker buildx version
FROM python:3.14-slim AS builder FROM python:3.14-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:0.11.26 /uv /uvx /bin/ COPY --from=ghcr.io/astral-sh/uv:0.11.26 /uv /uvx /bin/
@@ -8,10 +13,8 @@ ENV UV_COMPILE_BYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \ COPY uv.lock pyproject.toml /app/
--mount=type=bind,source=uv.lock,target=uv.lock \ RUN uv sync --locked --no-install-project --no-dev
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-dev --no-build
# Le projet lui-meme n'est pas installe (pas de second `uv sync`) : il tourne depuis /app, le # Le projet lui-meme n'est pas installe (pas de second `uv sync`) : il tourne depuis /app, le
# repertoire de travail, et rien ne lit ses metadonnees. L'installer imposerait de le construire # repertoire de travail, et rien ne lit ses metadonnees. L'installer imposerait de le construire
@@ -19,6 +22,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# l'installation des dependances n'execute aucun script de build (regle Sonar docker:S8541). # l'installation des dependances n'execute aucun script de build (regle Sonar docker:S8541).
COPY . /app COPY . /app
RUN uv sync --locked --no-dev
FROM python:3.14-slim AS runtime FROM python:3.14-slim AS runtime
-3
View File
@@ -16,9 +16,6 @@ EN_TETES: Final[dict[str, str]] = {
"X-Content-Type-Options": "nosniff", "X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY", "X-Frame-Options": "DENY",
"Referrer-Policy": "no-referrer", "Referrer-Policy": "no-referrer",
# same-origin : aucun client ne charge l'API en no-cors depuis une autre origine
# (proxy.conf.json en dev, reverse proxy nginx ensuite, cf. docs/architecture/20-backend.md).
"Cross-Origin-Resource-Policy": "same-origin",
} }
PREFIXE_AUTHENTIFICATION: Final = "/auth" PREFIXE_AUTHENTIFICATION: Final = "/auth"
+1 -2
View File
@@ -23,9 +23,8 @@ async def interroge(
("x-content-type-options", "nosniff"), ("x-content-type-options", "nosniff"),
("x-frame-options", "DENY"), ("x-frame-options", "DENY"),
("referrer-policy", "no-referrer"), ("referrer-policy", "no-referrer"),
("cross-origin-resource-policy", "same-origin"),
], ],
ids=["nosniff", "anti_iframe", "referrer", "corp"], ids=["nosniff", "anti_iframe", "referrer"],
) )
async def test_every_response_carries_the_security_headers( async def test_every_response_carries_the_security_headers(
client: AsyncClient, entete: str, valeur: str client: AsyncClient, entete: str, valeur: str
+12 -4
View File
@@ -26,20 +26,28 @@ RUN npm run build
FROM nginx:1.31-alpine AS runner FROM nginx:1.31-alpine AS runner
# Copie de la configuration de nginx # Copie de la configuration de nginx
COPY --chown=root:root --chmod=755 nginx.conf /etc/nginx/nginx.conf COPY --chown=root:root nginx.conf /etc/nginx/nginx.conf
RUN chmod 755 /etc/nginx/nginx.conf
# Copy the static build output from the build stage to Nginx's default HTML serving directory # Copy the static build output from the build stage to Nginx's default HTML serving directory
COPY --chown=root:root --chmod=755 --from=builder /app/dist/*/browser /usr/share/nginx/html COPY --chown=root:root --from=builder /app/dist/frontend/browser /usr/share/nginx/html
RUN chmod 755 /usr/share/nginx/html
# Create necessary directories with proper permissions for nginx # Create necessary directories with proper permissions for nginx
RUN mkdir -p /var/log/nginx /var/cache/nginx && \ RUN mkdir -p /var/log/nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx /var/cache/nginx /usr/share/nginx/html chown -R nginx:nginx /var/log/nginx /var/cache/nginx /usr/share/nginx/html &&\
chmod 755 /var/log/nginx /var/log
# Permissions appropriées
RUN find /usr/share/nginx/html -type f -exec chmod 644 {} \; && \
find /usr/share/nginx/html -type d -exec chmod 755 {} \; && \
chown -R nginx:nginx /var/log/nginx /var/cache/nginx
# Use a non-root user for security best practices # Use a non-root user for security best practices
USER nginx USER nginx
# Frontend : port 3000 # Frontend : port 3000
# Backend : port 8000
EXPOSE 3000 EXPOSE 3000
# Start Nginx directly with custom config # Start Nginx directly with custom config
+3 -4
View File
@@ -1,5 +1,5 @@
worker_processes auto; worker_processes auto;
error_log /var/log/nginx/error.log warn; error_log /dev/null warn; # ← Envoyer les logs vers /dev/null
pid /tmp/nginx.pid; pid /tmp/nginx.pid;
events { events {
@@ -9,15 +9,14 @@ events {
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
access_log /dev/null; # ← Idem pour access logs
sendfile on; sendfile on;
keepalive_timeout 65; keepalive_timeout 65;
server { server {
listen 3000; listen 3000;
server_name _; server_name _;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
@@ -29,4 +28,4 @@ http {
deny all; deny all;
} }
} }
} }
+2 -2
View File
@@ -4,10 +4,10 @@
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "node node_modules/@angular/cli/bin/ng.js build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"test:ci": "ng test --watch=false" "test:ci": "ng test --watch=false"
}, },
"private": true, "private": true,
"packageManager": "npm@11.19.0", "packageManager": "npm@11.19.0",
+2 -7
View File
@@ -423,13 +423,8 @@ Le reste, par ordre de surface :
écriture des journaux. C'est la troisième ligne de défense : la première est de ne rien passer écriture des journaux. C'est la troisième ligne de défense : la première est de ne rien passer
de secret au logger, la deuxième de ne jamais mettre un jeton dans une URL. de secret au logger, la deuxième de ne jamais mettre un jeton dans une URL.
- En-têtes posés par l'application : `X-Content-Type-Options`, `X-Frame-Options`, - En-têtes posés par l'application : `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, `Cross-Origin-Resource-Policy: same-origin`, plus `Cache-Control: no-store` `Referrer-Policy`, plus `Cache-Control: no-store` sur `/auth/*`. HSTS et CSP appartiennent au
sur `/auth/*`. Le CORP est fixé à `same-origin` parce qu'aucun client légitime ne charge l'API terminateur TLS, que l'application ne connaît pas : le reverse proxy les pose
en `no-cors` (image, script, média) depuis une autre origine : le frontend l'appelle en relatif
(`/api/v1`), sur sa propre origine, via `proxy.conf.json` en dev et le reverse proxy nginx
(`infra/proxy/conf.d/enervision.conf`) en recette et en production. Les appels `HttpClient`, en
mode `cors`, n'y sont de toute façon pas soumis. HSTS et CSP appartiennent au terminateur TLS, que
l'application ne connaît pas : le reverse proxy les pose
([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)).
- Le conteneur tourne en utilisateur non-root, avec un `HEALTHCHECK` sur `/api/v1/health/live`. - Le conteneur tourne en utilisateur non-root, avec un `HEALTHCHECK` sur `/api/v1/health/live`.
- TLS, limitation de débit au frontal et journal d'accès sont portés par le reverse proxy. - TLS, limitation de débit au frontal et journal d'accès sont portés par le reverse proxy.
+1 -1
View File
@@ -39,7 +39,7 @@ lecture seule ; plusieurs lignes resteront à compléter une fois les endpoints
| Cinq gardes de configuration qui refusent le démarrage plutôt que de dégrader silencieusement | `app/core/config.py` | A05 | | Cinq gardes de configuration qui refusent le démarrage plutôt que de dégrader silencieusement | `app/core/config.py` | A05 |
| Documentation interactive fermée hors développement, `/metrics` derrière un jeton, sonde qui ne publie plus de version | `app/main.py`, `app/api/security.py` | A05 | | Documentation interactive fermée hors développement, `/metrics` derrière un jeton, sonde qui ne publie plus de version | `app/main.py`, `app/api/security.py` | A05 |
| Scan dynamique OWASP ZAP de l'API authentifiée (compte `lecteur` jetable), non bloquant, configuration par défaut du backend uniquement (ni TLS ni en-têtes du reverse proxy) | `.github/workflows/dast.yml`, `scripts/dast-token.sh` | A05, API8 Security Misconfiguration | | Scan dynamique OWASP ZAP de l'API authentifiée (compte `lecteur` jetable), non bloquant, configuration par défaut du backend uniquement (ni TLS ni en-têtes du reverse proxy) | `.github/workflows/dast.yml`, `scripts/dast-token.sh` | A05, API8 Security Misconfiguration |
| En-têtes `nosniff`, `DENY`, `no-referrer`, `Cross-Origin-Resource-Policy: same-origin`, et `no-store` sur les routes d'authentification | `app/api/middleware.py` | A05 | | En-têtes `nosniff`, `DENY`, `no-referrer`, et `no-store` sur les routes d'authentification | `app/api/middleware.py` | A05 |
| Refus de rétrograder ou désactiver le dernier administrateur actif | `app/services/user.py` | A04 Insecure Design | | Refus de rétrograder ou désactiver le dernier administrateur actif | `app/services/user.py` | A04 Insecure Design |
| Amorçage du premier administrateur hors dépôt, mot de passe jamais dans `argv` ni dans Git | `app/cli.py` | A02, A05 | | Amorçage du premier administrateur hors dépôt, mot de passe jamais dans `argv` ni dans Git | `app/cli.py` | A02, A05 |
| Réponse de l'API Mock bornée avant écriture : timeout, plafond de sites et de mesures, bornes physiques par grandeur, recopie des seuls champs attendus | `app/etl/mock_api_import.py` | API10 Unsafe Consumption of APIs | | Réponse de l'API Mock bornée avant écriture : timeout, plafond de sites et de mesures, bornes physiques par grandeur, recopie des seuls champs attendus | `app/etl/mock_api_import.py` | API10 Unsafe Consumption of APIs |
+44 -4
View File
@@ -1,11 +1,31 @@
# This file is maintained automatically by "terraform init". # This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/null" { provider "registry.terraform.io/hashicorp/archive" {
version = "3.3.2" version = "2.8.1"
constraints = "~> 3.2" constraints = "~> 2.4"
hashes = [ hashes = [
"h1:IQ1qrkht1sC1nibUR+AJ3ulryyhVDHfCHZhoJi0sg2Y=", "h1:eehhIUcuegkswQDKArYBAhVV9wQmRVMhyYGaD7kHIj0=",
"zh:03de290604114a89fcd45c2e5bc7787d5a1ebfc5f964fb5989306bea7a4c79ec",
"zh:0a7d69dc9fbbc48960bc2f04588c8fb1bd92c78a8f306566b7fb17fc4a4f2058",
"zh:4df1f3981379c35f1757da957470f7f7724496b57219da3485177cf1647bdf59",
"zh:50f0e72ba53bfe6e11b03b7fc899e1f72536354381d302a743a274ae2a3f45f4",
"zh:5c4e15a04c98e2a8cafb1cd9632b48ad318051e8462d105b1153006277985c35",
"zh:66069e604bcf5c4af0278e15997d9e6bd755c54fb3801d78885838b889729c5f",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:979765db3f42601870ab377104ba70befb029547b278337ec4ada980e3582de6",
"zh:b165254da774f49945a73fbccc3ef1b63d70ea00a98fa9e14716665ba80ecaad",
"zh:c0bb2697b525da9fec4511f569ed2bd2b42f25d5c1f9fa00f8f3645b50cfc2e9",
"zh:c48f6695d12df0d0fa5231f7c1b8d518a4feae91a733fdd35a5804af3a303831",
"zh:d589954c93f075180c9f4e2ce91780d5edcb56dfd0d3cda8ba08e13c10b52249",
"zh:f5792ed06da65d0daf7ca3711f5399ff78c7cb4400afe53fbce9a926fbde4477",
]
}
provider "registry.terraform.io/hashicorp/null" {
version = "3.3.2"
hashes = [
"h1:7rn0+p+fbrHfJxNVgTnlKp6C3yu0jENMXffz9Xb9XjQ=",
"zh:10ec43b8b7b18d5639238c7fb9e111f6a4b038523dd66c7a426bf27b25fa4c08", "zh:10ec43b8b7b18d5639238c7fb9e111f6a4b038523dd66c7a426bf27b25fa4c08",
"zh:60beb9cc2ad5b871c710860cee75b42850cc6acd43db0d77cb5e00fda7288b55", "zh:60beb9cc2ad5b871c710860cee75b42850cc6acd43db0d77cb5e00fda7288b55",
"zh:62538582d0a4a2f10ad8a8d9a6c3cd3f05af6c6d91c6641ffc78d4f0e8e69b27", "zh:62538582d0a4a2f10ad8a8d9a6c3cd3f05af6c6d91c6641ffc78d4f0e8e69b27",
@@ -21,3 +41,23 @@ provider "registry.terraform.io/hashicorp/null" {
"zh:faa01928c25d2a6ecd9c7eb8b88134cb08de55a6b11ca6c703ac0092845344ba", "zh:faa01928c25d2a6ecd9c7eb8b88134cb08de55a6b11ca6c703ac0092845344ba",
] ]
} }
provider "registry.terraform.io/kreuzwerker/docker" {
version = "4.5.0"
constraints = "~> 4.5.0"
hashes = [
"h1:eXPMxNcZjz5gC7Nc1oEPJldpI6B5btbvldjLOgJQNbI=",
"zh:0ee4e9121632158a9ae1049bdd53c71fd9e06357de915dd193df2c1c55ae8814",
"zh:12c6991012b5a548a406e1960c323484f95a9538c2522af15ec82f47fdb832d1",
"zh:19c86ae59ed1e062e8402a49ea842f957fcda2f9689e06ecbd9480cc9bd1e41a",
"zh:3ac8bc9805acd20c40c0ed474c9f4605aa73b61cf52809e1131e335fcbfe8e49",
"zh:5a0e70d143759712fdda109f024a41d59d4758b5615378aa21ee6b959f65a416",
"zh:5d3599285f71cc53c88a5802fa6e2f8ba87b9deea03abe5fe8bb2639e9d95654",
"zh:a42f573a2526cddc9b8a93637a2c5478bc6e903ff29556ab8b2f1467681d5bde",
"zh:c8de50d902cc8457e565a5d959f1e3e49064d31c2f14f7adf5f3ec632cfe3695",
"zh:d40543d51eb4e902125d2c5a8a0f425a60edcec6c1dc07c4a89d92335ec05541",
"zh:dd3105018ffddc8114083dcf84798c32fdecca7ef708eceaee49eb4b306fc061",
"zh:e51e0199700698b962085683af4551d1f982dce37f3b6950da062c65e0145505",
"zh:ffe415d3d3deffffdbd3a7173e3a9f90b74601cb6d8acafec1ab701a5b91dbff",
]
}
@@ -0,0 +1,78 @@
locals {
backend_dir = "../../../../apps/backend"
backend_image_name = "enervision-back"
backend_image_tag = "latest"
backend_container_name = "enervision-backend"
}
resource "docker_image" "backend" {
name = "${local.backend_image_name}:${local.backend_image_tag}"
build {
context = local.backend_dir
dockerfile = "Dockerfile"
}
triggers = {
build_hash = sha256(join("", [
for file in fileset(local.backend_dir, "**") :
filesha256("${local.backend_dir}/${file}")
]))
}
}
resource "docker_container" "backend" {
image = docker_image.backend.image_id
name = local.backend_container_name
ports {
internal = 8000
external = var.backend_port
}
volumes {
host_path = "/var/log/enervision"
container_path = "/var/log/enervision"
}
depends_on = [docker_image.backend]
}
resource "null_resource" "deploy_backend_to_server" {
depends_on = [docker_image.backend]
triggers = {
image_id = docker_image.backend.image_id
}
connection {
type = "ssh"
host = var.ssh_host
port = var.ssh_port
user = var.ssh_user
password = var.ssh_password
private_key = try(file(pathexpand(var.ssh_private_key_path)), null)
timeout = "5m"
}
provisioner "file" {
source = "${local.backend_dir}/Dockerfile"
destination = "/tmp/Dockerfile"
}
provisioner "file" {
source = "${local.backend_dir}/"
destination = "/tmp/backend/"
}
provisioner "remote-exec" {
inline = [
"cd /tmp/backend",
"sudo docker stop ${local.backend_container_name} 2>/dev/null || true",
"sudo docker rm ${local.backend_container_name} 2>/dev/null || true",
"sudo docker build -t ${local.backend_image_name}:${local.backend_image_tag} .",
"sudo docker run -d --name ${local.backend_container_name} -p ${var.backend_port}:8000 --restart always -v /var/log/enervision:/var/log/enervision ${local.backend_image_name}:${local.backend_image_tag}"
]
}
}
@@ -0,0 +1,145 @@
locals {
frontend_environments = {
dev = {
source_dir = "${path.root}/../../../../apps/frontend/dist/frontend/browser"
domain = "dev.enervision"
}
}
frontend_dir = "../../../../apps/frontend"
image_name = "enervision-front"
image_tag = "latest"
container_name = "enervision-frontend"
selected_frontend = local.frontend_environments[var.deployment_environment]
}
provider "docker" {
host = var.docker_host
}
# Build le front
resource "null_resource" "build_frontend" {
triggers = {
frontend_files = sha256(join("", [
for file in fileset(local.frontend_dir, "src/**,package.json") :
filesha256("${local.frontend_dir}/${file}")
]))
}
provisioner "local-exec" {
command = "cd ${local.frontend_dir} && npm ci && chmod -R +x node_modules/@angular/cli/bin && chmod -R +x node_modules/.bin && npm exec ng build"
}
}
# Build l'image docker du front
resource "docker_image" "frontend" {
name = "${local.image_name}:${local.image_tag}"
build {
context = local.frontend_dir
dockerfile = "Dockerfile"
}
triggers = {
build_hash = sha256(join("", [
for file in fileset(local.frontend_dir, "**") :
filesha256("${local.frontend_dir}/${file}")
]))
}
depends_on = [null_resource.build_frontend]
}
# Création du container Docker
resource "docker_container" "frontend" {
image = docker_image.frontend.image_id
name = local.container_name
ports {
internal = 3000
external = var.frontend_port
}
# Volume pour les logs
volumes {
host_path = "/var/log/enervision"
container_path = "/var/log/nginx"
}
depends_on = [docker_image.frontend]
}
resource "null_resource" "create_network" {
provisioner "remote-exec" {
connection {
type = "ssh"
host = var.ssh_host
port = var.ssh_port
user = var.ssh_user
password = var.ssh_password
private_key = try(file(pathexpand(var.ssh_private_key_path)), null)
timeout = "5m"
}
inline = [
"sudo docker network create enervision_default 2>/dev/null || true"
]
}
}
# Déploiement du container sur le serveur via SSH
resource "null_resource" "deploy_to_server" {
depends_on = [docker_image.frontend]
triggers = {
image_id = docker_image.frontend.image_id
}
connection {
type = "ssh"
host = var.ssh_host
port = var.ssh_port
user = var.ssh_user
password = var.ssh_password
private_key = try(file(pathexpand(var.ssh_private_key_path)), null)
timeout = "5m"
}
# Copie le Dockerfile et nginx.conf
provisioner "file" {
source = "${local.frontend_dir}/Dockerfile"
destination = "/tmp/Dockerfile"
}
provisioner "file" {
source = "${local.frontend_dir}/nginx.conf"
destination = "/tmp/nginx.conf"
}
# Copie les sources pour le build
provisioner "file" {
source = "${local.frontend_dir}/"
destination = "/tmp/frontend/"
}
# Build et lance le container sur le serveur
provisioner "remote-exec" {
inline = [
"cd /tmp/frontend",
"sudo docker stop ${local.container_name} 2>/dev/null || true",
"sudo docker rm ${local.container_name} 2>/dev/null || true",
"sudo docker build -t ${local.image_name}:${local.image_tag} .",
"sudo docker run -d \\",
" --name ${local.container_name} \\",
" --network enervision_default \\",
" --network-alias frontend \\",
" -p ${var.frontend_port}:3000 \\",
" --restart always \\",
" -v /var/log/enervision:/var/log/nginx \\",
" ${local.image_name}:${local.image_tag}"
]
}
}
@@ -5,7 +5,24 @@ module "k3s" {
ssh_port = var.ssh_port ssh_port = var.ssh_port
ssh_user = var.ssh_user ssh_user = var.ssh_user
ssh_private_key_path = var.ssh_private_key_path ssh_private_key_path = var.ssh_private_key_path
ssh_password = var.ssh_password
k3s_version = var.k3s_version k3s_version = var.k3s_version
k3s_disable_components = var.k3s_disable_components k3s_disable_components = var.k3s_disable_components
kubeconfig_output_path = var.kubeconfig_output_path kubeconfig_output_path = var.kubeconfig_output_path
} }
provider "vault" {
# Adresse du serveur Vault (dev)
address = "https://10.101.200.37:8200"
# Auth par userpass
auth_login {
path = "auth/userpass/login/${var.vault_username}"
parameters = {
login = var.vault_username
password = var.vault_password
}
}
# Skip verify pour dev uniquement (pas en prod!)
# skip_client_verification = true
}
@@ -21,6 +21,35 @@ variable "ssh_private_key_path" {
sensitive = true sensitive = true
} }
variable "ssh_password" {
type = string
sensitive = true
}
variable "remote_path" {
type = string
description = "Chemin distant sur le serveur pour le deploiement."
default = "/var/www/enervision"
}
variable "docker_host" {
type = string
default = "npipe:////.//pipe//docker_engine"
description = "Host Docker (local ou distant)"
}
variable "frontend_port" {
type = number
default = 3000
description = "Port externe du frontend"
}
variable "backend_port" {
type = number
default = 8000
description = "Port externe du backend"
}
variable "k3s_version" { variable "k3s_version" {
type = string type = string
description = "Version k3s a epingler pour un deploiement reproductible (ex: v1.31.5+k3s1). Voir https://github.com/k3s-io/k3s/releases." description = "Version k3s a epingler pour un deploiement reproductible (ex: v1.31.5+k3s1). Voir https://github.com/k3s-io/k3s/releases."
@@ -37,3 +66,25 @@ variable "kubeconfig_output_path" {
description = "Chemin local ou ecrire le kubeconfig recupere apres installation." description = "Chemin local ou ecrire le kubeconfig recupere apres installation."
default = "./kubeconfig" default = "./kubeconfig"
} }
variable "deployment_environment" {
type = string
default = "dev"
validation {
condition = contains(["dev", "rec", "prod"], var.deployment_environment)
error_message = "L'environnement doit être dev, rec ou prod."
}
}
variable "vault_username" {
description = "Username Vault"
type = string
sensitive = false
}
variable "vault_password" {
description = "Password Vault"
type = string
sensitive = true
}
@@ -0,0 +1,125 @@
locals {
frontend_environments = {
prod = {
source_dir = "${path.root}/../../../apps/frontend/dist/frontend/browser"
domain = "enervision"
}
}
frontend_dir = "../../../../apps/frontend"
image_name = "enervision-front"
image_tag = "latest"
container_name = "enervision-frontend"
selected_frontend = local.frontend_environments[var.deployment_environment]
}
provider "docker" {
host = var.docker_host
}
# Build le front
resource "null_resource" "build_frontend" {
triggers = {
frontend_files = sha256(join("", [
for file in fileset(local.frontend_dir, "src/**,package.json") :
filesha256("${local.frontend_dir}/${file}")
]))
}
provisioner "local-exec" {
command = "cd ${local.frontend_dir} && npm ci && chmod -R +x node_modules/@angular/cli/bin && chmod -R +x node_modules/.bin && npm exec ng build"
}
}
# Build l'image docker du front
resource "docker_image" "frontend" {
name = "${local.image_name}:${local.image_tag}"
build {
context = local.frontend_dir
dockerfile = "Dockerfile"
}
triggers = {
build_hash = sha256(join("", [
for file in fileset(local.frontend_dir, "**") :
filesha256("${local.frontend_dir}/${file}")
]))
}
depends_on = [null_resource.build_frontend]
}
# Création du container Docker
resource "docker_container" "frontend" {
image = docker_image.frontend.image_id
name = local.container_name
ports {
internal = 3000
external = var.frontend_port
}
# Volume pour les logs
volumes {
host_path = "/var/log/enervision"
container_path = "/var/log/nginx"
}
depends_on = [docker_image.frontend]
}
# Déploiement du container sur le serveur via SSH
resource "null_resource" "deploy_to_server" {
depends_on = [docker_image.frontend]
triggers = {
image_id = docker_image.frontend.image_id
}
connection {
type = "ssh"
host = var.ssh_host
port = var.ssh_port
user = var.ssh_user
password = var.ssh_password
private_key = try(file(pathexpand(var.ssh_private_key_path)), null)
timeout = "5m"
}
# Copie le Dockerfile et nginx.conf
provisioner "file" {
source = "${local.frontend_dir}/Dockerfile"
destination = "/tmp/Dockerfile"
}
provisioner "file" {
source = "${local.frontend_dir}/nginx.conf"
destination = "/tmp/nginx.conf"
}
# Copie les sources pour le build
provisioner "file" {
source = "${local.frontend_dir}/"
destination = "/tmp/frontend/"
}
# Build et lance le container sur le serveur
provisioner "remote-exec" {
inline = [
"cd /tmp/frontend",
"sudo docker stop ${local.container_name} 2>/dev/null || true",
"sudo docker rm ${local.container_name} 2>/dev/null || true",
"sudo docker build -t ${local.image_name}:${local.image_tag} .",
"sudo docker run -d \\",
" --name ${local.container_name} \\",
" --network enervision_default \\",
" --network-alias frontend \\",
" -p ${var.frontend_port}:3000 \\",
" --restart always \\",
" -v /var/log/enervision:/var/log/nginx \\",
" ${local.image_name}:${local.image_tag}"
]
}
}
+11
View File
@@ -0,0 +1,11 @@
module "k3s" {
source = "../../modules/k3s"
ssh_host = var.ssh_host
ssh_port = var.ssh_port
ssh_user = var.ssh_user
ssh_private_key_path = var.ssh_private_key_path
k3s_version = var.k3s_version
k3s_disable_components = var.k3s_disable_components
kubeconfig_output_path = var.kubeconfig_output_path
}
@@ -0,0 +1,9 @@
output "kubeconfig_path" {
description = "Chemin local du kubeconfig recupere apres installation."
value = module.k3s.kubeconfig_path
}
output "node_host" {
description = "Adresse du serveur sur lequel k3s est installe."
value = module.k3s.node_host
}
@@ -0,0 +1,39 @@
variable "ssh_host" {
type = string
description = "Adresse IP ou nom d'hote du serveur on-premise de l'ecole."
}
variable "ssh_port" {
type = number
description = "Port SSH du serveur."
default = 22
}
variable "ssh_user" {
type = string
description = "Utilisateur SSH utilise pour l'installation."
default = "root"
}
variable "ssh_private_key_path" {
type = string
description = "Chemin local vers la cle privee SSH."
sensitive = true
}
variable "k3s_version" {
type = string
description = "Version k3s a epingler pour un deploiement reproductible (ex: v1.31.5+k3s1). Voir https://github.com/k3s-io/k3s/releases."
}
variable "k3s_disable_components" {
type = list(string)
description = "Composants embarques k3s a desactiver."
default = ["traefik"]
}
variable "kubeconfig_output_path" {
type = string
description = "Chemin local ou ecrire le kubeconfig recupere apres installation."
default = "./kubeconfig"
}
@@ -0,0 +1,14 @@
terraform {
required_version = ">= 1.7"
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
backend "local" {
path = "terraform.tfstate"
}
}
@@ -0,0 +1,57 @@
locals {
frontend_environments = {
rec = {
source_dir = "${path.root}/../../../apps/frontend/dist/frontend-rec/browser"
domain = "rec.enervision"
}
}
selected_frontend = local.frontend_environments[var.deployment_environment]
}
resource "null_resource" "frontend" {
depends_on = [module.k3s]
triggers = {
environment = var.deployment_environment
build_hash = sha256(join("", [
for file in fileset("${path.root}/../../../apps/frontend/dist/frontend/browser", "**") :
filesha256("${path.root}/../../../apps/frontend/dist/frontend/browser/${file}")
]))
}
// Variables pour la connexion SSH
connection {
type = "ssh"
host = var.ssh_host
port = var.ssh_port
user = var.ssh_user
private_key = file(pathexpand(var.ssh_private_key_path))
}
// Lancement de script en SSH avec remote-exec
// Installation de Nginx et initialisation du répertoire du frontend
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
"sudo mkdir -p /var/www/enervision",
"sudo rm -rf /var/www/enervision/*"
]
}
// Copie des fichiers vers le serveur
provisioner "file" {
source = "${path.root}/../../../apps/frontend/dist/frontend/browser/"
destination = "/tmp/enervision-frontend"
}
// Déplacement des fichiers
provisioner "remote-exec" {
inline = [
"sudo cp -r /tmp/enervision-frontend/* /var/www/enervision/",
"sudo chown -R www-data:www-data /var/www/enervision"
]
}
}
+11
View File
@@ -0,0 +1,11 @@
module "k3s" {
source = "../../modules/k3s"
ssh_host = var.ssh_host
ssh_port = var.ssh_port
ssh_user = var.ssh_user
ssh_private_key_path = var.ssh_private_key_path
k3s_version = var.k3s_version
k3s_disable_components = var.k3s_disable_components
kubeconfig_output_path = var.kubeconfig_output_path
}
@@ -0,0 +1,9 @@
output "kubeconfig_path" {
description = "Chemin local du kubeconfig recupere apres installation."
value = module.k3s.kubeconfig_path
}
output "node_host" {
description = "Adresse du serveur sur lequel k3s est installe."
value = module.k3s.node_host
}
@@ -0,0 +1,39 @@
variable "ssh_host" {
type = string
description = "Adresse IP ou nom d'hote du serveur on-premise de l'ecole."
}
variable "ssh_port" {
type = number
description = "Port SSH du serveur."
default = 22
}
variable "ssh_user" {
type = string
description = "Utilisateur SSH utilise pour l'installation."
default = "root"
}
variable "ssh_private_key_path" {
type = string
description = "Chemin local vers la cle privee SSH."
sensitive = true
}
variable "k3s_version" {
type = string
description = "Version k3s a epingler pour un deploiement reproductible (ex: v1.31.5+k3s1). Voir https://github.com/k3s-io/k3s/releases."
}
variable "k3s_disable_components" {
type = list(string)
description = "Composants embarques k3s a desactiver."
default = ["traefik"]
}
variable "kubeconfig_output_path" {
type = string
description = "Chemin local ou ecrire le kubeconfig recupere apres installation."
default = "./kubeconfig"
}
@@ -0,0 +1,14 @@
terraform {
required_version = ">= 1.7"
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
backend "local" {
path = "terraform.tfstate"
}
}
@@ -2,13 +2,15 @@ terraform {
required_version = ">= 1.7" required_version = ">= 1.7"
required_providers { required_providers {
null = { archive = {
source = "hashicorp/null" source = "hashicorp/archive"
version = "~> 3.2" version = "~> 2.4"
}
docker = {
source = "kreuzwerker/docker"
version = "~> 4.5.0"
} }
} }
backend "local" {
path = "terraform.tfstate"
}
} }
+19 -9
View File
@@ -10,6 +10,13 @@ locals {
# l'adresse, le port, l'utilisateur et le chemin de la cle : jamais la cle ni un mot de passe. # l'adresse, le port, l'utilisateur et le chemin de la cle : jamais la cle ni un mot de passe.
resource "null_resource" "k3s_install" { resource "null_resource" "k3s_install" {
triggers = { triggers = {
ssh_host = var.ssh_host
ssh_port = tostring(var.ssh_port)
ssh_user = var.ssh_user
ssh_password = var.ssh_password
k3s_version = var.k3s_version
disable_components = join(",", var.k3s_disable_components)
sudo_prefix = local.sudo_prefix
ssh_host = var.ssh_host ssh_host = var.ssh_host
ssh_port = tostring(var.ssh_port) ssh_port = tostring(var.ssh_port)
ssh_user = var.ssh_user ssh_user = var.ssh_user
@@ -20,11 +27,11 @@ resource "null_resource" "k3s_install" {
} }
connection { connection {
type = "ssh" type = "ssh"
host = self.triggers.ssh_host host = self.triggers.ssh_host
port = tonumber(self.triggers.ssh_port) port = self.triggers.ssh_port
user = self.triggers.ssh_user user = self.triggers.ssh_user
private_key = file(pathexpand(self.triggers.ssh_key_path)) password = self.triggers.ssh_password
} }
provisioner "remote-exec" { provisioner "remote-exec" {
@@ -53,10 +60,13 @@ resource "null_resource" "fetch_kubeconfig" {
} }
provisioner "local-exec" { provisioner "local-exec" {
interpreter = ["bash", "-c"] interpreter = ["PowerShell", "-NoProfile", "-Command"]
command = <<-EOT
ssh -i "${var.ssh_private_key_path}" -p ${var.ssh_port} -o StrictHostKeyChecking=accept-new ${var.ssh_user}@${var.ssh_host} '${local.kubeconfig_cmd}' \ command = <<-EOT
| sed 's/127.0.0.1/${var.ssh_host}/' > "${var.kubeconfig_output_path}" $content = ssh -i "${pathexpand(var.ssh_private_key_path)}" -p ${var.ssh_port} -o StrictHostKeyChecking=accept-new ${var.ssh_user}@${var.ssh_host} "cat /etc/rancher/k3s/k3s.yaml"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$content -replace "127.0.0.1", "${var.ssh_host}" |
Set-Content -Path "${var.kubeconfig_output_path}" -Encoding UTF8
EOT EOT
} }
} }
+5
View File
@@ -21,6 +21,11 @@ variable "ssh_private_key_path" {
sensitive = true sensitive = true
} }
variable "ssh_password" {
type = string
sensitive = true
}
variable "k3s_version" { variable "k3s_version" {
type = string type = string
description = "Version k3s a epingler pour un deploiement reproductible (ex: v1.31.5+k3s1). Voir https://github.com/k3s-io/k3s/releases." description = "Version k3s a epingler pour un deploiement reproductible (ex: v1.31.5+k3s1). Voir https://github.com/k3s-io/k3s/releases."
+3 -3
View File
@@ -2,9 +2,9 @@ terraform {
required_version = ">= 1.7" required_version = ">= 1.7"
required_providers { required_providers {
null = { provider = {
source = "hashicorp/null" source = "hashicorp/archive"
version = "~> 3.2" version = "~> 2.4"
} }
} }
} }