341 lines
18 KiB
Markdown
341 lines
18 KiB
Markdown
# Technical Writeup: OVH Gridscale, Caddy & Prometheus Infrastructure
|
|
|
|
## Overview
|
|
|
|
This document provides a comprehensive technical writeup of the infrastructure deployment and configuration management workflow. The project provisions cloud resources on **OVH / Gridscale** using **Terraform** and automates host configuration using **Ansible**.
|
|
|
|
---
|
|
|
|
## 1. Initial Infrastructure Setup & Technical Caveats
|
|
|
|
### 1.1 Terraform Provider & Resource Foundation
|
|
The initial configuration (`main.tf`, `variables.tf`, `resources.tf`) established authentication with the official Gridscale provider (`gridscale/gridscale`). Infrastructure resources were defined using the **Debian 13** image template and SSH key authentication.
|
|
|
|
### 1.2 Key Failures & Solutions
|
|
|
|
During initial testing and deployment, several technical hurdles were encountered and resolved:
|
|
|
|
1. **SSH Connection Timeouts (Public Network Requirement)**:
|
|
* **Issue**: Initial attempts to connect via SSH to the server's public IPv4 address timed out (`ssh: connect to host ... port 22: Operation timed out`).
|
|
* **Root Cause & Fix**: Gridscale instances require explicit attachment to the public network. Because no dynamic network UUID data lookup was provided in the task specification, the public network UUID (`5557a73b-31ee-4b1f-aa15-7789ad6ae04c`) was hardcoded directly into the server's network block in `resources.tf`.
|
|
|
|
2. **Unpowered Server Instances**:
|
|
* **Issue**: Newly created instances remained offline and unreachable after `terraform apply`.
|
|
* **Root Cause & Fix**: Gridscale creates servers in an unpowered state by default. Setting `power = true` explicitly in `gridscale_server` resources ensured servers automatically boot upon creation.
|
|
|
|
3. **Ansible SSH Host Key Verification Failures**:
|
|
* **Issue**: Ansible fact-gathering failed with `Host key verification failed`.
|
|
* **Root Cause & Fix**: Cloud IPs change dynamically on fresh deployments. Added `ansible_ssh_extra_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'` to the `[all:vars]` block in `inventory.tf` to bypass interactive host key verification prompts seamlessly.
|
|
|
|
---
|
|
|
|
## 2. Network Topology & Firewall Security
|
|
|
|
### 2.1 Multi-Server Topology
|
|
The architecture consists of two dedicated nodes:
|
|
* **`caddy01`**: Web server hosting Caddy.
|
|
* **`monitoring01`**: Telemetry node hosting Prometheus.
|
|
|
|
### 2.2 Dual-Interface Network Isolation & Inbound Firewall Rules
|
|
To secure management endpoints and telemetry data, instances were configured with dual network interfaces:
|
|
|
|
1. **Public Network**:
|
|
* Attached to Gridscale public network UUID `5557a73b-31ee-4b1f-aa15-7789ad6ae04c`.
|
|
* **Firewall Rules (`rules_v4_in`, `rules_v6_in`)**:
|
|
* Allow TCP port `22` (SSH).
|
|
* Allow TCP port `80` (HTTP).
|
|
* Block all other inbound public traffic.
|
|
|
|
2. **Private Network (`gridscale_network.network_internal`)**:
|
|
* Internal DHCP subnet: `192.168.121.0/27` (Gateway: `192.168.121.1`, DNS: `192.168.121.2`, Reserved: `192.168.121.0/31`).
|
|
* **Firewall Rules**:
|
|
* On `caddy01`, allows inbound TCP port `2019` (Caddy Admin API / Metrics) **only** from the internal CIDR `192.168.121.0/27`. Public access to port 2019 is strictly denied at the firewall layer.
|
|
|
|
---
|
|
|
|
## 3. Dynamic Ansible Inventory Generation
|
|
|
|
### 3.1 Automated Inventory Build (`inventory.tf`)
|
|
To eliminate manual IP tracking, Terraform's `local_file` resource generates `inventory.ini` dynamically upon `terraform apply`.
|
|
|
|
### 3.2 Dynamic IP Interpolation
|
|
- Extracts auto-assigned private IP addresses from resource state: `${gridscale_server.server_caddy.network[1].auto_assigned_ip}`.
|
|
- Automatically exports host groups (`[caddy]`, `[monitoring]`) and internal IP variables (`privat_ip_caddy`, `privat_ip_monitoring`) for Ansible playbooks.
|
|
- Exposes outputs `caddy_public_ip`, `monitoring_public_ip`, `caddy_private_ip`, and `monitoring_private_ip` in `outputs.tf`.
|
|
|
|
---
|
|
|
|
## 4. Ansible Playbook Orchestration (`playbook.yml`)
|
|
|
|
### 4.1 Base System Maintenance & Idempotent Reboot (`all`)
|
|
* **Package Updates**: Executes `apt update` and `upgrade: dist`.
|
|
* **Idempotent Reboot Check**: Uses `ansible.builtin.stat` to check for `/var/run/reboot-required`. The reboot task triggers *only* when required (`when: reboot_required_file.stat.exists == true`), avoiding unnecessary reboot delays during repeat runs.
|
|
* **Prerequisites**: Installs `debian-keyring`, `debian-archive-keyring`, `apt-transport-https`, and `curl`.
|
|
|
|
### 4.2 Caddy Web Server Configuration (`caddy`)
|
|
* **Official Deb822 Repository**: Per Caddy official documentation, configured Caddy's Debian repository using `ansible.builtin.deb822_repository` (`https://dl.cloudsmith.io/public/caddy/stable/deb/debian` with official GPG key verification).
|
|
* **Metrics & Admin API Security**:
|
|
* Configured `Caddyfile` with `/metrics` and bound the Admin API to `:2019`.
|
|
* Set `origins http://{{ privat_ip_caddy }}:2019` so Caddy validates origin headers and only processes authorized internal requests.
|
|
* **Service Handlers**: Uses Ansible handlers to restart the `caddy` service automatically upon `Caddyfile` updates.
|
|
|
|
### 4.3 Prometheus Monitoring Setup (`monitoring`)
|
|
* **Installation**: Installs the `prometheus` package on `monitoring01`.
|
|
* **Private Scrape Target**: Configured `prometheus.yml` to scrape Caddy metrics target `{{ privat_ip_caddy }}:2019` strictly over the internal private network (`192.168.121.0/27`).
|
|
* **Service Handlers**: Triggers `Restart Prometheus service` automatically when `prometheus.yml` changes.
|
|
|
|
---
|
|
|
|
## 5. Execution Guide
|
|
|
|
### 5.1 Provision Infrastructure (Terraform)
|
|
```bash
|
|
# Initialize Terraform providers
|
|
terraform init
|
|
|
|
# Apply infrastructure changes
|
|
terraform apply -auto-approve
|
|
```
|
|
|
|
### 5.2 Run Ansible Provisioning
|
|
|
|
```bash
|
|
# Provision Caddy Web Server
|
|
uv run ansible-playbook -i inventory.ini -l caddy playbook.yml
|
|
|
|
# Provision Monitoring Server
|
|
uv run ansible-playbook -i inventory.ini -l monitoring playbook.yml
|
|
|
|
# Provision All Servers
|
|
uv run ansible-playbook -i inventory.ini playbook.yml
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Modular Jinja2 Templating & Prometheus Alerting Rules
|
|
|
|
### 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](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`.
|
|
|
|
### 6.2 Variable Naming Refactoring
|
|
Standardized variable naming from `privat_ip_*` to `private_ip_*` across:
|
|
* `inventory.tf` (Terraform dynamic inventory builder)
|
|
* `inventory.ini` (Generated inventory)
|
|
* `playbook.yml` & Jinja2 templates (`{{ private_ip_caddy }}`)
|
|
|
|
### 6.3 Prometheus Alert Rules & Raw Jinja Escaping
|
|
Configured alerting rules in `templates/alert_rules.yml.j2` using Jinja `{% raw %}` ... `{% endraw %}` blocks to prevent Ansible from misinterpreting Prometheus template variables (such as `{{ $labels.instance }}` and `{{ $value }}`):
|
|
|
|
1. **`Caddy Down`**: Triggers a critical alert if `up{job="caddy"} == 0` for 1 minute.
|
|
2. **`CaddyHighHTTP4xxErrorRateService`**: Triggers a critical alert if the 4xx HTTP error rate exceeds 5% over a 3-minute window (with total request count > 0).
|
|
* *Source*: Rule specification adapted from [Awesome Prometheus Alerts (Caddy Rules)](https://samber.github.io/awesome-prometheus-alerts/rules/proxies-load-balancers-and-service-meshes/caddy/).
|
|
|
|
### 6.4 Verification
|
|
Verified playbook execution and rule rendering:
|
|
```bash
|
|
uv run ansible-playbook -i inventory.ini -l monitoring playbook.yml
|
|
```
|
|
|
|
### 6.5 Automated Playbook Execution via Terraform (`null_resource`)
|
|
To fully automate the end-to-end deployment, a `null_resource` provisioner was integrated into `inventory.tf`. It executes automatically as the final step of `terraform apply` after server creation and inventory file generation:
|
|
|
|
```hcl
|
|
resource "null_resource" "ansible_provisioner" {
|
|
depends_on = [
|
|
local_file.ansible_inventory,
|
|
gridscale_server.server_caddy,
|
|
gridscale_server.server_monitoring
|
|
]
|
|
|
|
provisioner "local-exec" {
|
|
command = "sleep 60 &&uv run ansible-playbook -i ${local_file.ansible_inventory.filename} playbook.yml"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. End-to-End Environment Setup & Deployment Workflow
|
|
|
|
### 7.1 Environment Prerequisites & Credentials (`.env`)
|
|
Copy the `.env.example` template and export your Gridscale API credentials into your shell session:
|
|
|
|
```bash
|
|
# Copy template environment file
|
|
cp .env.example .env
|
|
|
|
# Edit .env with your credentials, then export variables
|
|
source .env
|
|
```
|
|
|
|
### 7.2 Python Environment Initialization & Dependency Setup (`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, 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
|
|
Initialize Terraform and apply the configuration. Terraform will provision the servers, dynamically construct `inventory.ini`, and automatically trigger the Ansible playbook via the `null_resource`:
|
|
|
|
```bash
|
|
# Initialize Terraform providers
|
|
terraform init
|
|
|
|
# Apply infrastructure changes (automatically provisions servers & runs Ansible)
|
|
terraform apply -auto-approve
|
|
```
|
|
|
|
*(Optional)* Execute Ansible playbooks manually via `uv`:
|
|
```bash
|
|
# Run Ansible playbook for all hosts
|
|
uv run ansible-playbook -i inventory.ini playbook.yml
|
|
|
|
# Target specific host groups
|
|
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.<ip>.sslip.io` $\rightarrow$ Caddy default static page
|
|
* `nginx.<ip>.sslip.io` $\rightarrow$ Reverse proxies to private Nginx IP (`{{ private_ip_nginx }}:80`)
|
|
* `monitoring.<ip>.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.<caddy-ip-with-dashes>.sslip.io` | Local Caddy static server | Public landing page |
|
|
| **Nginx Web Server** | `https://nginx.<caddy-ip-with-dashes>.sslip.io` | `http://<nginx-ip-private>:80` | Reverse proxied Hello World page |
|
|
| **Prometheus Monitoring** | `https://monitoring.<caddy-ip-with-dashes>.sslip.io` | `http://<monitoring-ip-private>:9090` with caddy `basic_auth` | **Username**: `demo`<br>**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.<ip>.sslip.io` $\rightarrow$ Nginx, `monitoring.<ip>.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` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|