Initial IaC setup: Nomad, Consul, Terraform, Ansible

- Ansible roles for Nomad and Consul installation
- Nomad job specs for Traefik and monitoring stack
- Terraform configuration for Nomad job management
- Enterprise naming convention inventory
- HashiCorp stack deployment playbook
This commit is contained in:
2026-02-04 08:52:08 -06:00
commit 3d3c1af237
15 changed files with 998 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
# Hobbs.farm Infrastructure as Code
Enterprise-grade infrastructure management using HashiCorp stack + Ansible.
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ HOBBS.FARM IaC │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ TERRAFORM │────▶│ ANSIBLE │────▶│ NOMAD │ │
│ │ │ │ │ │ │ │
│ │ • Infra │ │ • Config │ │ • Deploy │ │
│ │ • State │ │ • Install │ │ • Scale │ │
│ │ • DNS │ │ • Secure │ │ • Monitor │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ └────────────────┴────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────┐ │
│ │ HASHICORP STACK │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ NOMAD │ │ CONSUL │ │ VAULT │ │ │
│ │ │ orchest │◄─┤ service │◄─┤ secrets │ │ │
│ │ │ ration │ │ mesh │ │ mgmt │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ HOSTS │ │
│ │ hf-pdocker-01 (primary) hf-pdocker-02 (secondary) │
│ │ 192.168.86.192 192.168.86.45 │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Directory Structure
```
hobbs-iac/
├── terraform/ # Infrastructure provisioning
│ ├── environments/ # Per-environment configs
│ │ └── prod/
│ ├── modules/ # Reusable modules
│ └── nomad-jobs/ # Nomad job definitions (HCL)
├── ansible/ # Configuration management
│ ├── inventory/ # Host inventory
│ ├── playbooks/ # Automation playbooks
│ ├── roles/ # Ansible roles
│ └── group_vars/ # Variables
├── nomad/ # Nomad-specific configs
│ ├── jobs/ # Job specifications
│ ├── policies/ # ACL policies
│ └── templates/ # Job templates
├── consul/ # Consul configs
│ ├── config/ # Consul configuration
│ └── policies/ # ACL policies
├── vault/ # Vault configs
│ ├── policies/ # Access policies
│ └── secrets/ # Secret definitions (encrypted)
└── scripts/ # Utility scripts
```
## Quick Start
### 1. Bootstrap Hosts (Ansible)
```bash
cd ansible
ansible-playbook -i inventory/hosts.yml playbooks/bootstrap.yml
```
### 2. Install HashiCorp Stack
```bash
ansible-playbook -i inventory/hosts.yml playbooks/hashicorp-stack.yml
```
### 3. Initialize Terraform
```bash
cd terraform/environments/prod
terraform init
terraform plan
terraform apply
```
### 4. Deploy Services
```bash
# Deploy all Nomad jobs
nomad job run ../nomad-jobs/traefik.nomad
nomad job run ../nomad-jobs/monitoring.nomad
# ... etc
```
## Naming Convention
See [Naming Convention Standard](docs/naming-convention.md)
| Type | Format | Example |
|------|--------|---------|
| Physical | `hf-p<function>-##` | `hf-pdocker-01` |
| Virtual | `hf-v<function>-##` | `hf-vapp-01` |
| Container | `hf-c<function>-##` | `hf-cproxy-01` |
| Network | `hf-n<function>-##` | `hf-nfw-01` |
| Storage | `hf-s<function>-##` | `hf-snas-01` |
## Prerequisites
- Ansible 2.15+
- Terraform 1.5+
- Nomad 1.6+
- Consul 1.16+
- Vault 1.15+
## License
Private - Hobbs.farm internal use only
+109
View File
@@ -0,0 +1,109 @@
---
# Hobbs.farm Infrastructure Inventory
# Naming: <location>-<type><function>-<sequence>
all:
vars:
ansible_python_interpreter: /usr/bin/python3
domain: hobbs.farm
timezone: America/Chicago
# HashiCorp versions
nomad_version: "1.7.3"
consul_version: "1.17.2"
vault_version: "1.15.4"
# Cluster settings
datacenter: hf-dc1
# Data paths
nomad_data_dir: /opt/nomad
consul_data_dir: /opt/consul
vault_data_dir: /opt/vault
children:
# =========================================
# Nomad Cluster
# =========================================
nomad_servers:
hosts:
hf-pdocker-01:
nomad_server: true
nomad_client: true
nomad_bootstrap_expect: 1
nomad_clients:
hosts:
hf-pdocker-01:
nomad_client: true
nomad_node_class: "primary"
hf-pdocker-02:
nomad_client: true
nomad_node_class: "secondary"
# =========================================
# Consul Cluster
# =========================================
consul_servers:
hosts:
hf-pdocker-01:
consul_server: true
consul_bootstrap_expect: 1
consul_clients:
hosts:
hf-pdocker-01:
hf-pdocker-02:
# =========================================
# Vault Cluster
# =========================================
vault_servers:
hosts:
hf-pdocker-01:
vault_server: true
# =========================================
# Physical Hosts
# =========================================
physical:
children:
docker_hosts:
hosts:
hf-pdocker-01:
ansible_host: 192.168.86.192
ansible_user: glyptic
ansible_become_password: "{{ vault_sudo_password }}"
legacy_name: hobbs
role: primary
hf-pdocker-02:
ansible_host: 192.168.86.45
ansible_user: glyptic
ansible_ssh_private_key_file: ~/.ssh/id_docker_server
legacy_name: peggy
role: secondary
status: degraded
nvr_hosts:
hosts:
hf-pnvr-01:
ansible_host: 192.168.86.181
ansible_user: glyptic
ansible_ssh_private_key_file: ~/.ssh/id_docker_server
legacy_name: bart
# =========================================
# Network & Storage (reference only)
# =========================================
network:
hosts:
hf-nfw-01:
ansible_host: 192.168.86.1
legacy_name: firewalla
storage:
hosts:
hf-snas-01:
ansible_host: 192.168.86.30
legacy_name: nas
+66
View File
@@ -0,0 +1,66 @@
---
# Install HashiCorp Stack (Nomad, Consul, Vault)
# Run: ansible-playbook -i inventory/hosts.yml playbooks/hashicorp-stack.yml
- name: Install Consul
hosts: consul_servers:consul_clients
become: yes
roles:
- consul
tags: [consul]
- name: Install Vault
hosts: vault_servers
become: yes
roles:
- vault
tags: [vault]
- name: Install Nomad
hosts: nomad_servers:nomad_clients
become: yes
roles:
- nomad
tags: [nomad]
- name: Bootstrap Nomad cluster
hosts: nomad_servers[0]
become: yes
tasks:
- name: Wait for Nomad to be ready
ansible.builtin.uri:
url: "http://{{ ansible_host }}:4646/v1/status/leader"
method: GET
status_code: 200
register: nomad_leader
until: nomad_leader.status == 200
retries: 30
delay: 5
- name: Display Nomad leader
ansible.builtin.debug:
msg: "Nomad leader: {{ nomad_leader.json }}"
tags: [nomad, bootstrap]
- name: Verify cluster health
hosts: nomad_servers[0]
become: yes
tasks:
- name: Check Nomad node status
ansible.builtin.command: nomad node status
register: nomad_nodes
changed_when: false
- name: Display nodes
ansible.builtin.debug:
var: nomad_nodes.stdout_lines
- name: Check Consul members
ansible.builtin.command: consul members
register: consul_members
changed_when: false
- name: Display Consul members
ansible.builtin.debug:
var: consul_members.stdout_lines
tags: [verify]
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart Consul
ansible.builtin.systemd:
name: consul
state: restarted
daemon_reload: yes
+56
View File
@@ -0,0 +1,56 @@
---
# Consul Installation Role
- name: Add HashiCorp GPG key
ansible.builtin.apt_key:
url: https://apt.releases.hashicorp.com/gpg
state: present
- name: Add HashiCorp repository
ansible.builtin.apt_repository:
repo: "deb https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main"
state: present
filename: hashicorp
- name: Install Consul
ansible.builtin.apt:
name: "consul={{ consul_version }}-1"
state: present
update_cache: yes
- name: Create Consul directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: consul
group: consul
mode: '0755'
loop:
- "{{ consul_data_dir }}"
- "{{ consul_data_dir }}/data"
- /etc/consul.d
- name: Configure Consul
ansible.builtin.template:
src: consul.hcl.j2
dest: /etc/consul.d/consul.hcl
owner: consul
group: consul
mode: '0640'
notify: Restart Consul
- name: Configure Consul server (if server)
ansible.builtin.template:
src: server.hcl.j2
dest: /etc/consul.d/server.hcl
owner: consul
group: consul
mode: '0640'
when: consul_server | default(false)
notify: Restart Consul
- name: Enable and start Consul
ansible.builtin.systemd:
name: consul
enabled: yes
state: started
@@ -0,0 +1,34 @@
# Consul Configuration
# Managed by Ansible
datacenter = "{{ datacenter }}"
data_dir = "{{ consul_data_dir }}/data"
node_name = "{{ inventory_hostname }}"
bind_addr = "{{ ansible_host }}"
client_addr = "0.0.0.0"
ui_config {
enabled = true
}
# DNS settings
dns_config {
allow_stale = true
}
# Service mesh
connect {
enabled = true
}
# Telemetry
telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
# Performance
performance {
raft_multiplier = 1
}
@@ -0,0 +1,15 @@
# Consul Server Configuration
server = true
bootstrap_expect = {{ consul_bootstrap_expect | default(1) }}
# Raft settings
raft_protocol = 3
# Autopilot
autopilot {
cleanup_dead_servers = true
last_contact_threshold = "200ms"
max_trailing_logs = 250
server_stabilization_time = "10s"
}
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart Nomad
ansible.builtin.systemd:
name: nomad
state: restarted
daemon_reload: yes
+76
View File
@@ -0,0 +1,76 @@
---
# Nomad Installation Role
- name: Install dependencies
ansible.builtin.apt:
name:
- curl
- gnupg
- lsb-release
- apt-transport-https
state: present
update_cache: yes
- name: Add HashiCorp GPG key
ansible.builtin.apt_key:
url: https://apt.releases.hashicorp.com/gpg
state: present
- name: Add HashiCorp repository
ansible.builtin.apt_repository:
repo: "deb https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main"
state: present
filename: hashicorp
- name: Install Nomad
ansible.builtin.apt:
name: "nomad={{ nomad_version }}-1"
state: present
update_cache: yes
- name: Create Nomad directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: nomad
group: nomad
mode: '0755'
loop:
- "{{ nomad_data_dir }}"
- "{{ nomad_data_dir }}/data"
- /etc/nomad.d
- name: Configure Nomad
ansible.builtin.template:
src: nomad.hcl.j2
dest: /etc/nomad.d/nomad.hcl
owner: nomad
group: nomad
mode: '0640'
notify: Restart Nomad
- name: Configure Nomad server (if server)
ansible.builtin.template:
src: server.hcl.j2
dest: /etc/nomad.d/server.hcl
owner: nomad
group: nomad
mode: '0640'
when: nomad_server | default(false)
notify: Restart Nomad
- name: Configure Nomad client
ansible.builtin.template:
src: client.hcl.j2
dest: /etc/nomad.d/client.hcl
owner: nomad
group: nomad
mode: '0640'
when: nomad_client | default(false)
notify: Restart Nomad
- name: Enable and start Nomad
ansible.builtin.systemd:
name: nomad
enabled: yes
state: started
@@ -0,0 +1,34 @@
# Nomad Client Configuration
client {
enabled = true
node_class = "{{ nomad_node_class | default('default') }}"
# Resource reservations
reserved {
cpu = 500
memory = 512
}
# Host volumes for persistent data
host_volume "docker-data" {
path = "/mnt/nas/docker"
read_only = false
}
host_volume "media" {
path = "/mnt/nas/media"
read_only = false
}
# Meta information
meta {
"rack" = "{{ rack | default('rack-1') }}"
"datacenter" = "{{ datacenter }}"
"legacy_name" = "{{ legacy_name | default('unknown') }}"
}
# Network interface
network_interface = "{{ ansible_default_ipv4.interface | default('eth0') }}"
}
@@ -0,0 +1,53 @@
# Nomad Configuration
# Managed by Ansible - Do not edit manually
datacenter = "{{ datacenter }}"
data_dir = "{{ nomad_data_dir }}/data"
bind_addr = "0.0.0.0"
advertise {
http = "{{ ansible_host }}"
rpc = "{{ ansible_host }}"
serf = "{{ ansible_host }}"
}
# Enable ACLs (after bootstrap)
# acl {
# enabled = true
# }
# Consul integration
consul {
address = "127.0.0.1:8500"
server_service_name = "nomad"
client_service_name = "nomad-client"
auto_advertise = true
server_auto_join = true
client_auto_join = true
}
# Vault integration (after Vault setup)
# vault {
# enabled = true
# address = "http://127.0.0.1:8200"
# }
telemetry {
collection_interval = "10s"
disable_hostname = true
prometheus_metrics = true
publish_allocation_metrics = true
publish_node_metrics = true
}
plugin "docker" {
config {
allow_privileged = true
volumes {
enabled = true
}
extra_labels = ["job_name", "task_group_name", "task_name", "namespace", "node_name"]
}
}
@@ -0,0 +1,14 @@
# Nomad Server Configuration
server {
enabled = true
bootstrap_expect = {{ nomad_bootstrap_expect | default(1) }}
# Raft settings
raft_protocol = 3
# Server join (for multi-server setup)
# server_join {
# retry_join = ["hf-pdocker-01.hobbs.farm"]
# }
}
+186
View File
@@ -0,0 +1,186 @@
job "monitoring" {
datacenters = ["hf-dc1"]
type = "service"
group "prometheus" {
count = 1
network {
port "prometheus" {
static = 9095
}
}
service {
name = "prometheus"
port = "prometheus"
tags = [
"traefik.enable=true",
"traefik.http.routers.prometheus.rule=Host(`prometheus.hobbs.farm`)",
]
check {
type = "http"
path = "/-/healthy"
interval = "10s"
timeout = "2s"
}
}
task "prometheus" {
driver = "docker"
config {
image = "prom/prometheus:v2.48.0"
ports = ["prometheus"]
args = [
"--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.path=/prometheus",
"--web.listen-address=:9095",
"--web.enable-lifecycle",
]
}
template {
data = <<-EOF
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9095']
- job_name: 'nomad'
consul_sd_configs:
- server: '{{ env "NOMAD_IP_prometheus" }}:8500'
services: ['nomad-client', 'nomad']
relabel_configs:
- source_labels: ['__meta_consul_tags']
regex: '.*,metrics,.*'
action: keep
- job_name: 'consul'
static_configs:
- targets: ['{{ env "NOMAD_IP_prometheus" }}:8500']
EOF
destination = "local/prometheus.yml"
}
resources {
cpu = 200
memory = 512
}
volume_mount {
volume = "docker-data"
destination = "/prometheus"
read_only = false
}
}
volume "docker-data" {
type = "host"
source = "docker-data"
read_only = false
}
}
group "grafana" {
count = 1
network {
port "grafana" {
static = 3001
}
}
service {
name = "grafana"
port = "grafana"
tags = [
"traefik.enable=true",
"traefik.http.routers.grafana.rule=Host(`grafana.hobbs.farm`)",
]
check {
type = "http"
path = "/api/health"
interval = "10s"
timeout = "2s"
}
}
task "grafana" {
driver = "docker"
config {
image = "grafana/grafana:10.2.2"
ports = ["grafana"]
}
env {
GF_SERVER_HTTP_PORT = "3001"
GF_SECURITY_ADMIN_PASSWORD = "admin" # TODO: Use Vault
}
resources {
cpu = 200
memory = 256
}
volume_mount {
volume = "docker-data"
destination = "/var/lib/grafana"
read_only = false
}
}
volume "docker-data" {
type = "host"
source = "docker-data"
read_only = false
}
}
group "alertmanager" {
count = 1
network {
port "alertmanager" {
static = 9093
}
}
service {
name = "alertmanager"
port = "alertmanager"
check {
type = "http"
path = "/-/healthy"
interval = "10s"
timeout = "2s"
}
}
task "alertmanager" {
driver = "docker"
config {
image = "prom/alertmanager:v0.26.0"
ports = ["alertmanager"]
}
resources {
cpu = 100
memory = 128
}
}
}
}
+129
View File
@@ -0,0 +1,129 @@
job "traefik" {
datacenters = ["hf-dc1"]
type = "service"
# Run on primary node
constraint {
attribute = "${node.class}"
value = "primary"
}
group "traefik" {
count = 1
network {
port "http" {
static = 80
}
port "https" {
static = 443
}
port "api" {
static = 8080
}
}
service {
name = "traefik"
port = "https"
tags = [
"traefik.enable=true",
"traefik.http.routers.api.rule=Host(`traefik.hobbs.farm`)",
"traefik.http.routers.api.service=api@internal",
]
check {
type = "http"
path = "/ping"
port = "api"
interval = "10s"
timeout = "2s"
}
}
task "traefik" {
driver = "docker"
config {
image = "traefik:v3.0"
ports = ["http", "https", "api"]
volumes = [
"local/traefik.yml:/etc/traefik/traefik.yml:ro",
"local/dynamic:/etc/traefik/dynamic:ro",
]
# Mount Docker socket for provider
mount {
type = "bind"
source = "/var/run/docker.sock"
target = "/var/run/docker.sock"
}
}
template {
data = <<-EOF
api:
dashboard: true
insecure: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
consulCatalog:
prefix: traefik
exposedByDefault: false
endpoint:
address: "{{ env "NOMAD_IP_http" }}:8500"
file:
directory: /etc/traefik/dynamic
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: jeremy@hobbs.farm
storage: /data/acme.json
dnsChallenge:
provider: cloudflare
ping:
entryPoint: api
log:
level: INFO
EOF
destination = "local/traefik.yml"
}
resources {
cpu = 200
memory = 256
}
volume_mount {
volume = "docker-data"
destination = "/data"
read_only = false
}
}
volume "docker-data" {
type = "host"
source = "docker-data"
read_only = false
}
}
}
+97
View File
@@ -0,0 +1,97 @@
# Hobbs.farm Production Environment
# Terraform configuration for Nomad job management
terraform {
required_version = ">= 1.5.0"
required_providers {
nomad = {
source = "hashicorp/nomad"
version = "~> 2.0"
}
consul = {
source = "hashicorp/consul"
version = "~> 2.18"
}
}
# Backend configuration for state storage
# Uncomment when ready to use remote state
# backend "consul" {
# address = "hf-pdocker-01.hobbs.farm:8500"
# scheme = "http"
# path = "terraform/hobbs-iac/prod"
# }
}
# Provider configurations
provider "nomad" {
address = "http://hf-pdocker-01.hobbs.farm:4646"
# secret_id = var.nomad_token # Uncomment after ACL setup
}
provider "consul" {
address = "hf-pdocker-01.hobbs.farm:8500"
datacenter = "hf-dc1"
}
# Variables
variable "environment" {
default = "prod"
}
variable "datacenter" {
default = "hf-dc1"
}
# Deploy Nomad jobs
resource "nomad_job" "traefik" {
jobspec = file("${path.module}/../../nomad-jobs/traefik.nomad")
hcl2 {
enabled = true
}
}
resource "nomad_job" "monitoring" {
jobspec = file("${path.module}/../../nomad-jobs/monitoring.nomad")
hcl2 {
enabled = true
}
depends_on = [nomad_job.traefik]
}
# Consul KV for configuration
resource "consul_keys" "config" {
datacenter = var.datacenter
key {
path = "hobbs/config/environment"
value = var.environment
}
key {
path = "hobbs/config/datacenter"
value = var.datacenter
}
key {
path = "hobbs/config/domain"
value = "hobbs.farm"
}
}
# Outputs
output "nomad_address" {
value = "http://hf-pdocker-01.hobbs.farm:4646"
}
output "consul_address" {
value = "http://hf-pdocker-01.hobbs.farm:8500"
}
output "traefik_job_id" {
value = nomad_job.traefik.id
}