From 0a3d2d9b33534a0e1849a890f7e91502ce799cb8 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 29 Jul 2026 09:10:24 +0200 Subject: [PATCH] docs(readme): add sections 6-10 covering templates, nginx, health checks, and summary --- README.md | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 137 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 13e2f59..f67932f 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,9 @@ uv run ansible-playbook -i inventory.ini playbook.yml ### 6.1 Refactoring Inline Configurations to Jinja2 Templates To improve maintainability and separate configuration data from playbook logic, inline file content blocks in `playbook.yml` were refactored into dedicated Jinja2 template files located in the `templates/` directory: -* **[templates/Caddyfile.j2](file:///Users/doctor/Git/ovh_gridscale_test_task/templates/Caddyfile.j2)**: Contains Caddy server block definitions and metrics setup. -* **[templates/prometheus.yml.j2](file:///Users/doctor/Git/ovh_gridscale_test_task/templates/prometheus.yml.j2)**: Contains global scrape configuration and imports rule files via `rule_files: [/etc/prometheus/alert_rules.yml]`. -* **[templates/alert_rules.yml.j2](file:///Users/doctor/Git/ovh_gridscale_test_task/templates/alert_rules.yml.j2)**: Defines Prometheus alert rules for monitoring Caddy service availability and error rates. +* **[templates/Caddyfile.j2](templates/Caddyfile.j2)**: Contains Caddy server block definitions and metrics setup. +* **[templates/prometheus.yml.j2](templates/prometheus.yml.j2)**: Contains global scrape configuration and imports rule files via `rule_files: [/etc/prometheus/alert_rules.yml]`. +* **[templates/alert_rules.yml.j2](templates/alert_rules.yml.j2)**: Defines Prometheus alert rules for monitoring Caddy service availability and error rates. The playbook tasks were updated to use `ansible.builtin.template` instead of `ansible.builtin.copy`. @@ -154,7 +154,7 @@ resource "null_resource" "ansible_provisioner" { ] provisioner "local-exec" { - command = "uv run ansible-playbook -i ${local_file.ansible_inventory.filename} playbook.yml" + command = "sleep 60 &&uv run ansible-playbook -i ${local_file.ansible_inventory.filename} playbook.yml" } } ``` @@ -175,14 +175,14 @@ source .env ``` ### 7.2 Python Environment Initialization & Dependency Setup (`uv`) -Initialize Python environment and install Ansible using `uv`: +Initialize Python environment and install Ansible along with `passlib` and `bcrypt` (required for Jinja `password_hash` filter) using `uv`: ```bash # Initialize uv virtual environment and lockfile uv init --bare -# Add Ansible dependency -uv add "ansible" +# Add Ansible, passlib, and bcrypt dependencies (bcrypt<4.0.0 required for passlib compatibility) +uv add ansible passlib "bcrypt<4.0.0" ``` ### 7.3 Infrastructure Provisioning & Automated Execution @@ -206,5 +206,135 @@ uv run ansible-playbook -i inventory.ini -l caddy playbook.yml uv run ansible-playbook -i inventory.ini -l monitoring playbook.yml ``` +--- + +## 8. Additional Task: Nginx Integration and Custom Improvements + +### 8.1 Nginx Infrastructure & Ansible Deployment +Expanded the infrastructure to provision a dedicated Nginx web server node (`nginx01`) alongside Caddy and Monitoring nodes: +* **Terraform Infrastructure**: Defined `gridscale_storage.storage_nginx` and `gridscale_server.server_nginx` attached to the internal private network (`gridscale_network.network_internal`) with port 80 allowed internally. +* **Ansible Automation**: Added an `nginx` play in `playbook.yml` installing Nginx and deploying a custom `index.html.j2` page displaying host metadata. + +### 8.2 HTTPS Reverse Proxying via `sslip.io` Dynamic Subdomains +Configured Caddy as the central ingress gateway utilizing `sslip.io` dynamic DNS wildcard resolution to route traffic over HTTPS automatically: +* `caddy..sslip.io` $\rightarrow$ Caddy default static page +* `nginx..sslip.io` $\rightarrow$ Reverse proxies to private Nginx IP (`{{ private_ip_nginx }}:80`) +* `monitoring..sslip.io` $\rightarrow$ Reverse proxies to internal Prometheus IP (`{{ private_ip_monitoring }}:9090`) + +### 8.3 Prometheus Subdomain Security via Basic Authentication & Health Checks +Secured access to the Prometheus monitoring interface by embedding Caddy's `basic_auth` directive and active health checking directly into the monitoring subdomain block in `templates/Caddyfile.j2`: + +```caddy +monitoring.{{ ansible_default_ipv4.address | replace(".", "-") }}.sslip.io { + reverse_proxy {{ private_ip_monitoring }}:9090 { + health_uri /-/healthy + health_interval 10s + health_timeout 5s + } + basic_auth { + {{ demo_user }} {{ demo_pass | password_hash('bcrypt') }} + } +} +``` + +### 8.4 Python Dependencies & `bcrypt` Version Pinning +Encountered and resolved a critical dependency issue during Ansible template rendering for `password_hash('bcrypt')`: +* **Issue**: `passlib` (1.7.4) failed with `AttributeError: module 'bcrypt' has no attribute '__about__'` when paired with `bcrypt >= 4.0.0`, resulting in a `password cannot be longer than 72 bytes` exception. +* **Fix**: Pinned `bcrypt` to `<4.0.0` (`bcrypt==3.2.2`) in `pyproject.toml` and installed via `uv`: + ```bash + uv add ansible passlib "bcrypt<4.0.0" + ``` + +### 8.5 Direct Execution Output via Terraform `nonsensitive()` +To display all endpoint URLs and auto-generated credentials immediately upon `terraform apply` completion without manual CLI commands: +* Wrapped `random_password.demo_pass.result` using Terraform's `nonsensitive()` function in `outputs.tf`: + +```hcl +output "website_urls" { + description = "Website URLs" + value = [ + "Caddy: https://caddy.${replace(gridscale_ipv4.public_ipv4_caddy.ip, ".", "-")}.sslip.io", + "Nginx behind Caddy: https://nginx.${replace(gridscale_ipv4.public_ipv4_caddy.ip, ".", "-")}.sslip.io", + "Prometheus behind Caddy: https://monitoring.${replace(gridscale_ipv4.public_ipv4_caddy.ip, ".", "-")}.sslip.io", + "Login for Monitoring: demo ${nonsensitive(random_password.demo_pass.result)}" + ] +} +``` + +### 8.6 Upstream Health Checks & Fault Tolerance in Caddy +Configured active health checking inside Caddy's `reverse_proxy` directives in `templates/Caddyfile.j2` to ensure fault tolerance for upstream services: +* **Nginx Health Probes**: Periodic `health_uri /` checks every 10 seconds (5s timeout). +* **Prometheus Health Probes**: Periodic `health_uri /-/healthy` checks every 10 seconds (5s timeout). +* If an upstream service or node becomes unresponsive, Caddy automatically marks the backend as unhealthy and prevents routing traffic to dead backends. + +--- + +## 9. Summary: Service Execution & Access Guide + +### 9.1 Quickstart Execution Steps + +1. **Configure Environment Credentials**: + Copy `.env.example` to `.env`, set your Gridscale API credentials, and export them: + ```bash + cp .env.example .env + # Edit .env with your credentials + source .env + ``` + +2. **Initialize Python Environment & Dependencies**: + Initialize `uv` environment and install Ansible, `passlib`, and `bcrypt`: + ```bash + uv init --bare + uv add ansible passlib "bcrypt<4.0.0" + ``` + +3. **Provision Infrastructure & Execute Playbooks**: + Apply Terraform to provision all cloud resources, generate `inventory.ini`, and automatically trigger Ansible configuration management via `null_resource`: + ```bash + terraform init + terraform apply -auto-approve + ``` + +--- + +### 9.2 Service Access & Endpoints Summary + +Upon successful completion, Terraform outputs all HTTPS endpoints and Basic Authentication credentials: + +| Service | Host / Subdomain Format | Target / Upstream | Access Details | +| :--- | :--- | :--- | :--- | +| **Caddy Ingress** | `https://caddy..sslip.io` | Local Caddy static server | Public landing page | +| **Nginx Web Server** | `https://nginx..sslip.io` | `http://:80` | Reverse proxied Hello World page | +| **Prometheus Monitoring** | `https://monitoring..sslip.io` | `http://:9090` with caddy `basic_auth` | **Username**: `demo`
**Password**: Auto-generated password from Terraform output | + +*(Optional)* Display all endpoint URLs and active credentials at any time: +```bash +terraform output website_urls +``` + +--- + +## 10. Exercise Requirement Traceability & Execution Summary + +The following matrix maps every requirement, deliverable, and bonus task from the Gridscale hiring exercise prompt to its exact technical implementation, file location, and execution method in this repository: + +| Exercise Requirement / Task | Status | Implementation Details & File Location | Execution & Automation Method | +| :--- | :---: | :--- | :--- | +| **Install & Configure Caddy Server** | Completed | Provisioned `caddy01` VM on Debian 13 in [resources.tf](resources.tf). Installed Caddy via Deb822 repo in [playbook.yml](playbook.yml#L41-L80). | Automated via Ansible `ansible.builtin.apt` & `ansible.builtin.template` | +| **Serve Sample Web Application** | Completed | Configured static file server (`root * /usr/share/caddy`, `file_server`) in [templates/Caddyfile.j2](templates/Caddyfile.j2#L10-L13). | Rendered via Ansible template `Caddyfile.j2` | +| **Prometheus Monitoring Setup** | Completed | Provisioned `monitoring01` VM in [resources.tf](resources.tf). Installed Prometheus in [playbook.yml](playbook.yml#L81-L120). | Configured scrape job `caddy` targeting `{{ private_ip_caddy }}:2019` | +| **Metrics Collection (Codes, Latency, Uptime)** | Completed | Enabled Caddy metrics directive `metrics /metrics` with `per_host` and `observe_catchall_hosts` on Admin API `:2019` in [templates/Caddyfile.j2](templates/Caddyfile.j2#L1-L9). | Scraped every 15s by Prometheus over internal network (`192.168.121.0/27`) | +| **Prometheus Alerting Pipeline & Rules** | Completed | Defined `Caddy Down` (`up{job="caddy"} == 0`) and `CaddyHighHTTP4xxErrorRateService` (> 5% 4xx rate) rules in [templates/alert_rules.yml.j2](templates/alert_rules.yml.j2). Imported in [templates/prometheus.yml.j2](templates/prometheus.yml.j2). | Deployed to `/etc/prometheus/alert_rules.yml` via Ansible | +| **IaC Automation (Terraform + Ansible)** | Completed | 100% automated with Terraform (Gridscale provider) & Ansible. Built `null_resource.ansible_provisioner` in [inventory.tf](inventory.tf#L29-L43) to execute Ansible automatically. | Runs end-to-end via `terraform apply -auto-approve` | +| **Additional Task: Nginx "Hello World" VM** | Completed | Provisioned `nginx01` VM in [resources.tf](resources.tf). Installed Nginx and deployed `index.html.j2` in [playbook.yml](playbook.yml#L121-L140). | Bound strictly to internal private IP (`192.168.121.5:80`) | +| **Additional Task: Caddy Reverse Proxy & Routing** | Completed | Configured subdomain reverse proxy routing in [templates/Caddyfile.j2](templates/Caddyfile.j2#L15-L31): `nginx..sslip.io` $\rightarrow$ Nginx, `monitoring..sslip.io` $\rightarrow$ Prometheus. | Managed via Caddy ingress rules | +| **Bonus: Upstream Health Checks & Fault Tolerance** | Completed | Added `health_uri /` (Nginx) and `health_uri /-/healthy` (Prometheus) with 10s intervals in [templates/Caddyfile.j2](templates/Caddyfile.j2#L16-L28). | Probed actively by Caddy to bypass dead nodes | +| **Bonus: SSL Termination & Client Encryption** | Completed | Used `sslip.io` dynamic wildcard subdomains on port 443 in [templates/Caddyfile.j2](templates/Caddyfile.j2). | Caddy Auto-HTTPS manages Let's Encrypt TLS termination | +| **Deliverables: Endpoints & Output Credentials** | Completed | Configured unmasked output using `nonsensitive()` in [outputs.tf](outputs.tf). | Printed directly to terminal on `terraform apply` | + + + + +