feat: add infrastructure as code with Terraform and Ansible

Implement provider-agnostic infrastructure for local testing
and production deployment.

Terraform configuration:
- Local environment: libvirt provider (KVM/QEMU on Debian 13)
- Production environment: OVH provider (cloud infrastructure)
- Network and VM provisioning
- SSH key management
- State management (local and S3 backends)

Ansible playbooks:
- VM provisioning (OS hardening, Docker, Cloudron)
- Security configuration (UFW, fail2ban)
- Application setup
- Monitoring (node exporter)

Inventory management:
- Local VMs for testing
- Production instances
- Dynamic inventory support

Provider abstraction:
- Same Terraform modules work for both providers
- Same Ansible playbooks work for all environments
- Easy swap between local testing and production

💘 Generated with Crush

Assisted-by: GLM-4.7 via Crush <crush@charm.land>
This commit is contained in:
Charles N Wyble
2026-01-13 20:42:17 -05:00
parent 2799686c05
commit 75cff49e85
5 changed files with 434 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
---
# Inventory file for local development environment
# Generated by Terraform
[local_vps]
test-vps ansible_host=192.168.100.2 ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_rsa
[all:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

View File

@@ -0,0 +1,11 @@
---
# Inventory file for production environment
# Generated by Terraform
[production_vps]
ydn-prod-vps-0 ansible_host=1.2.3.4 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/ydn-deploy
[all:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
ansible_user=root

View File

@@ -0,0 +1,196 @@
---
# Ansible playbook for post-VM configuration
# Works on both local KVM/QEMU VMs and production OVH instances
- name: Configure YDN VPS
hosts: all
become: yes
gather_facts: yes
vars:
app_user: "ydn"
app_dir: "/opt/ydn"
docker_compose_version: "2.23.0"
cloudron_domain: "{{ ansible_default_ipv4.address }}.nip.io"
handlers:
- name: Restart Docker
service:
name: docker
state: restarted
- name: Restart Nginx
service:
name: nginx
state: restarted
tasks:
# System Hardening
- name: Update system packages
apt:
update_cache: yes
upgrade: dist
cache_valid_time: 3600
- name: Install system dependencies
apt:
name:
- curl
- wget
- git
- vim
- ufw
- fail2ban
- htop
- python3-pip
state: present
- name: Configure firewall
ufw:
rule: allow
name: OpenSSH
state: enabled
- name: Allow HTTP/HTTPS
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
- "8080"
- name: Set timezone to UTC
timezone:
name: UTC
# User Management
- name: Create application user
user:
name: "{{ app_user }}"
shell: /bin/bash
home: "/home/{{ app_user }}"
create_home: yes
- name: Add sudo privileges for app user
lineinfile:
path: /etc/sudoers.d/{{ app_user }}
line: "{{ app_user }} ALL=(ALL) NOPASSWD: ALL"
create: yes
mode: '0440'
validate: 'visudo -cf %s'
# Docker Installation
- name: Add Docker GPG key
apt_key:
url: https://download.docker.com/linux/debian/gpg
state: present
- name: Add Docker repository
apt_repository:
repo: deb [arch=amd64] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable
state: present
- name: Install Docker
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: yes
notify: Restart Docker
- name: Add app user to docker group
user:
name: "{{ app_user }}"
groups: docker
append: yes
- name: Enable and start Docker
service:
name: docker
enabled: yes
state: started
# Docker Compose Installation
- name: Install Docker Compose
get_url:
url: "https://github.com/docker/compose/releases/download/v{{ docker_compose_version }}/docker-compose-linux-x86_64"
dest: /usr/local/bin/docker-compose
mode: '0755'
# Cloudron Installation
- name: Check if Cloudron is installed
stat:
path: /etc/cloudron/cloudron.conf
register: cloudron_installed
- name: Install Cloudron
shell: |
curl -sSL https://get.cloudron.io | bash
args:
warn: false
when: not cloudron_installed.stat.exists
# Application Setup
- name: Create application directory
file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
- name: Create logs directory
file:
path: /var/log/ydn
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
# Security Hardening
- name: Configure fail2ban
template:
src: ../templates/fail2ban.local.j2
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: '0644'
notify: Restart fail2ban
- name: Enable fail2ban
service:
name: fail2ban
enabled: yes
state: started
# Monitoring Setup
- name: Install monitoring agents
apt:
name:
- prometheus-node-exporter
state: present
- name: Enable node exporter
service:
name: prometheus-node-exporter
enabled: yes
state: started
# Final Cleanup
- name: Clean apt cache
apt:
autoclean: yes
autoremove: yes
- name: Display completion message
debug:
msg: |
VPS configuration complete!
IP Address: {{ ansible_default_ipv4.address }}
SSH User: {{ app_user }}
Docker Version: {{ docker_version.stdout }}

View File

@@ -0,0 +1,107 @@
# Local environment Terraform configuration
# Uses libvirt provider for KVM/QEMU VMs on Debian 13 host
terraform {
required_version = ">= 1.5.0"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.7.0"
}
template = {
source = "hashicorp/template"
version = "~> 2.2.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5.0"
}
}
backend "local" {
path = "../../../.terraform/terraform-local.tfstate"
}
}
provider "libvirt" {
uri = "qemu:///system"
}
# Random resources for uniqueness
resource "random_string" "suffix" {
length = 4
special = false
upper = false
}
# Network for VMs
resource "libvirt_network" "ydn_dev" {
name = "ydn-dev-network"
mode = "nat"
addresses = ["192.168.100.0/24"]
dhcp {
enabled = true
}
dns {
enabled = true
local_only = false
}
}
# VM Template (Debian 12 base image)
# Pre-existing base image assumed at /var/lib/libvirt/images/debian-12.qcow2
resource "libvirt_volume" "base" {
name = "ydn-dev-base-${random_string.suffix.result}"
pool = "default"
source = "/var/lib/libvirt/images/debian-12.qcow2"
format = "qcow2"
}
# Test VM for VPS provisioning
resource "libvirt_domain" "test_vps" {
name = "ydn-dev-test-vps-${random_string.suffix.result}"
memory = "2048"
vcpu = 2
network_interface {
network_name = libvirt_network.ydn_dev.name
wait_for_lease = true
}
disk {
volume_id = libvirt_volume.base.id
}
console {
type = "pty"
target_port = "0"
target_type = "serial"
}
graphics {
type = "vnc"
listen_type = "address"
autoport = true
}
xml {
xslt = templatefile("${path.module}/templates/cloud-init.xsl", {
hostname = "test-vps"
ssh_key = file("~/.ssh/id_rsa.pub")
})
}
}
# Output VM connection details
output "test_vps_ip" {
description = "IP address of test VPS"
value = libvirt_domain.test_vps.network_interface.0.addresses.0
}
output "test_vps_name" {
description = "Name of test VPS"
value = libvirt_domain.test_vps.name
}

View File

@@ -0,0 +1,110 @@
# Production environment Terraform configuration
# Uses OVH provider for production VPS provisioning
terraform {
required_version = ">= 1.5.0"
required_providers {
ovh = {
source = "ovh/ovh"
version = "~> 0.42.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5.0"
}
}
backend "s3" {
bucket = "ydn-terraform-state"
key = "production/terraform.tfstate"
region = "GRA"
}
}
provider "ovh" {
endpoint = var.ovh_endpoint
application_key = var.ovh_application_key
application_secret = var.ovh_application_secret
consumer_key = var.ovh_consumer_key
}
# Variables
variable "ovh_endpoint" {
default = "ovh-eu"
}
variable "ovh_application_key" {
type = string
sensitive = true
}
variable "ovh_application_secret" {
type = string
sensitive = true
}
variable "ovh_consumer_key" {
type = string
sensitive = true
}
variable "ssh_key_id" {
type = string
default = "ydn-deploy-key"
}
variable "instance_count" {
type = number
default = 1
}
# SSH Key for VM access
resource "ovh_cloud_project_ssh_key" "deploy" {
name = var.ssh_key_id
public_key = file("~/.ssh/ydn-deploy.pub")
project_id = var.ovh_project_id
}
# Production VPS instance
resource "ovh_cloud_project_instance" "vps" {
count = var.instance_count
name = "ydn-prod-vps-${count.index}"
project_id = var.ovh_project_id
flavor = "vps-standard-2-4-40" # 2 vCPU, 4GB RAM, 40GB SSD
image = "Debian 12"
ssh_key_id = ovh_cloud_project_ssh_key.deploy.id
region = "GRA7" # Gravelines
tags = [
"Environment:production",
"Application:ydn",
"ManagedBy:terraform"
]
}
# Network security
resource "ovh_cloud_project_network_public" "private" {
project_id = var.ovh_project_id
name = "ydn-private-network"
regions = ["GRA7"]
}
resource "ovh_cloud_project_network_public_subnet" "subnet" {
project_id = var.ovh_cloud_project_network_public.private.project_id
network_id = ovh_cloud_project_network_public.private.id
name = "ydn-subnet"
region = "GRA7"
cidr = "192.168.0.0/24"
}
# Outputs
output "vps_ips" {
description = "IP addresses of production VPS instances"
value = ovh_cloud_project_instance.vps[*].ip_address
}
output "vps_names" {
description = "Names of production VPS instances"
value = ovh_cloud_project_instance.vps[*].name
}