Example: Multi-Tier Application

Multi-Tier Application

Compose roles from individual cookbooks to provision a complete web + application + database stack.

Directory Structure

cookbooks/
  nginx/
    default.rb
  ruby-app/
    default.rb
  postgresql/
    default.rb
  docker/
    default.rb
  users/
    default.rb
  monitoring/
    default.rb
  security/
    default.rb
roles/
  base.rb
  web.rb
  app.rb
  db.rb
nodes/
  web01.json
  app01.json
  db01.json

Role Recipes

roles/base.rb

Common configuration applied to every server: security hardening, user management, and monitoring.

# roles/base.rb

include_recipe '../cookbooks/security/default.rb'
include_recipe '../cookbooks/users/default.rb'
include_recipe '../cookbooks/monitoring/default.rb'

roles/web.rb

Web tier: base configuration plus Nginx.

# roles/web.rb

include_recipe './base.rb'
include_recipe '../cookbooks/nginx/default.rb'

roles/app.rb

Application tier: base configuration plus the Ruby app deployment.

# roles/app.rb

include_recipe './base.rb'
include_recipe '../cookbooks/ruby-app/default.rb'

roles/db.rb

Database tier: base configuration plus PostgreSQL.

# roles/db.rb

include_recipe './base.rb'
include_recipe '../cookbooks/postgresql/default.rb'

Node Attributes

Each tier gets its own node JSON that merges base settings with tier-specific ones.

nodes/web01.json

{
  "security": {
    "ssh_port": 2222,
    "permit_root_login": "no",
    "password_authentication": "no",
    "allowed_users": ["alice", "deploy"],
    "ufw_allowed_ports": [2222, 80, 443],
    "fail2ban_maxretry": 3,
    "fail2ban_bantime": 3600,
    "fail2ban_findtime": 600
  },
  "users": {
    "admins": [
      { "name": "alice", "uid": 2001, "shell": "/bin/bash", "ssh_key": "ssh-ed25519 AAAA..." }
    ],
    "deployers": [{ "name": "deploy", "uid": 3001, "shell": "/bin/bash" }],
    "admin_group_gid": 2000,
    "deploy_group_gid": 3000
  },
  "monitoring": {
    "node_exporter_version": "1.7.0",
    "node_exporter_port": 9100,
    "node_exporter_user": "node_exporter",
    "node_exporter_uid": 9100,
    "textfile_dir": "/var/lib/node_exporter/textfile",
    "health_check_url": "https://health.example.com/ping",
    "alert_email": "ops@example.com"
  },
  "nginx": {
    "worker_processes": 4,
    "worker_connections": 1024,
    "server_name": "app.example.com",
    "root": "/var/www/app/current/public",
    "upstream_port": 3000,
    "ssl_certificate": "/etc/ssl/certs/app.pem",
    "ssl_certificate_key": "/etc/ssl/private/app.key"
  }
}

nodes/app01.json

{
  "security": { "...": "same base security settings" },
  "users": { "...": "same base user settings" },
  "monitoring": { "...": "same base monitoring settings" },
  "app": {
    "name": "myapp",
    "user": "deploy",
    "uid": 1001,
    "deploy_to": "/var/www/myapp",
    "repository": "https://github.com/example/myapp.git",
    "revision": "main",
    "ruby_version": "3.2.2",
    "puma_workers": 2,
    "puma_threads_min": 1,
    "puma_threads_max": 5,
    "puma_port": 3000,
    "environment": "production",
    "secret_key_base": "abc123..."
  }
}

nodes/db01.json

{
  "security": { "...": "same base security settings" },
  "users": { "...": "same base user settings" },
  "monitoring": { "...": "same base monitoring settings" },
  "postgresql": {
    "version": "15",
    "data_dir": "/var/lib/postgresql/15/main",
    "max_connections": 200,
    "shared_buffers": "256MB",
    "effective_cache_size": "1GB",
    "db_name": "myapp_production",
    "db_user": "myapp",
    "db_password": "secret",
    "backup_dir": "/var/backups/postgresql",
    "backup_retention_days": 7
  }
}

Running

Provision each tier by pointing Itamae at the role recipe and the appropriate node file:

# Web tier
itamae ssh -j nodes/web01.json -h web01.example.com roles/web.rb

# App tier
itamae ssh -j nodes/app01.json -h app01.example.com roles/app.rb

# Database tier
itamae ssh -j nodes/db01.json -h db01.example.com roles/db.rb

Scaling

To add more servers, create additional node JSON files and run the corresponding role:

# Add a second web server
itamae ssh -j nodes/web02.json -h web02.example.com roles/web.rb

# Add a second app server
itamae ssh -j nodes/app02.json -h app02.example.com roles/app.rb

The key benefit of role-based composition is that each cookbook is maintained independently while roles combine them for specific server functions.