Initial commit
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
# Docker Deployment with Infisical Secrets
|
||||
|
||||
**Learning objective:** See best practices in action - secrets management, error handling, and idempotency.
|
||||
|
||||
## What This Example Demonstrates
|
||||
|
||||
This playbook showcases **production-ready Ansible patterns** from Virgo-Core:
|
||||
|
||||
✅ **Secrets Management:**
|
||||
|
||||
- Infisical integration using reusable task
|
||||
- Fallback to environment variables
|
||||
- `no_log: true` on sensitive tasks
|
||||
|
||||
✅ **Error Handling:**
|
||||
|
||||
- Pre-flight checks with `assert`
|
||||
- `changed_when` for idempotency
|
||||
- `failed_when` for graceful failures
|
||||
- Block/rescue for rollback
|
||||
|
||||
✅ **Best Practices:**
|
||||
|
||||
- Fully qualified module names (FQCN)
|
||||
- Task organization with blocks
|
||||
- Handlers for service restarts
|
||||
- Verification steps
|
||||
|
||||
✅ **Docker Operations:**
|
||||
|
||||
- Idempotent container management
|
||||
- Health checks with retries
|
||||
- Proper logging on failures
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Infisical Setup
|
||||
|
||||
**Universal Auth credentials:**
|
||||
|
||||
```bash
|
||||
export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID="ua-abc123"
|
||||
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET="secret-xyz789"
|
||||
```
|
||||
|
||||
**OR fallback environment variables:**
|
||||
|
||||
```bash
|
||||
export DB_PASSWORD="fallback-db-password"
|
||||
export API_KEY="fallback-api-key"
|
||||
export REDIS_PASSWORD="fallback-redis-password"
|
||||
```
|
||||
|
||||
### 2. Ansible Collections
|
||||
|
||||
```bash
|
||||
# Install required collections
|
||||
cd ../../.. # Back to ansible directory
|
||||
uv run ansible-galaxy collection install -r requirements.yml
|
||||
```
|
||||
|
||||
### 3. Target Hosts
|
||||
|
||||
Update inventory with Docker hosts:
|
||||
|
||||
```ini
|
||||
# inventory/hosts
|
||||
[docker_hosts]
|
||||
docker-01-nexus.spaceships.work
|
||||
```
|
||||
|
||||
### 4. Templates (create these)
|
||||
|
||||
The playbook references templates you need to create:
|
||||
|
||||
**`templates/app-config.yml.j2`:**
|
||||
|
||||
```yaml
|
||||
database:
|
||||
host: db.spaceships.work
|
||||
password: "{{ db_password }}"
|
||||
|
||||
api:
|
||||
key: "{{ api_key }}"
|
||||
|
||||
redis:
|
||||
host: redis.spaceships.work
|
||||
password: "{{ redis_password }}"
|
||||
```
|
||||
|
||||
**`templates/docker-compose.yml.j2`:**
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: your-app:latest
|
||||
environment:
|
||||
- CONFIG_FILE=/config/config.yml
|
||||
volumes:
|
||||
- {{ app_dir }}/config.yml:/config/config.yml:ro
|
||||
ports:
|
||||
- "8080:8080"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Validate Playbook
|
||||
|
||||
**Syntax check:**
|
||||
|
||||
```bash
|
||||
ansible-playbook docker-deployment.yml --syntax-check
|
||||
```
|
||||
|
||||
**Lint check:**
|
||||
|
||||
```bash
|
||||
ansible-lint docker-deployment.yml
|
||||
```
|
||||
|
||||
**Dry run:**
|
||||
|
||||
```bash
|
||||
ansible-playbook docker-deployment.yml --check
|
||||
```
|
||||
|
||||
### 2. Run Playbook
|
||||
|
||||
```bash
|
||||
# Full deployment
|
||||
ansible-playbook -i ../../inventory/hosts docker-deployment.yml
|
||||
|
||||
# Specific tags
|
||||
ansible-playbook -i ../../inventory/hosts docker-deployment.yml --tags secrets
|
||||
ansible-playbook -i ../../inventory/hosts docker-deployment.yml --tags deploy
|
||||
ansible-playbook -i ../../inventory/hosts docker-deployment.yml --tags verify
|
||||
```
|
||||
|
||||
### 3. Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check application health
|
||||
curl http://docker-01-nexus.spaceships.work:8080/health
|
||||
|
||||
# Check Docker containers
|
||||
ssh ansible@docker-01-nexus.spaceships.work "docker ps"
|
||||
```
|
||||
|
||||
## Understanding the Patterns
|
||||
|
||||
### Pattern 1: Infisical Secret Lookup
|
||||
|
||||
**The Pattern:**
|
||||
|
||||
```yaml
|
||||
- name: Retrieve database password from Infisical
|
||||
ansible.builtin.include_tasks: ../../tasks/infisical-secret-lookup.yml
|
||||
vars:
|
||||
secret_name: 'DB_PASSWORD'
|
||||
secret_var_name: 'db_password'
|
||||
fallback_env_var: 'DB_PASSWORD'
|
||||
```
|
||||
|
||||
**Why it works:**
|
||||
|
||||
- Reusable task (DRY principle)
|
||||
- Validates authentication before retrieving
|
||||
- Fallback to environment for local dev
|
||||
- No secrets in logs
|
||||
- Clear error messages
|
||||
|
||||
**Learn more:** [../../patterns/secrets-management.md](../../patterns/secrets-management.md)
|
||||
|
||||
### Pattern 2: Pre-flight Validation
|
||||
|
||||
**The Pattern:**
|
||||
|
||||
```yaml
|
||||
pre_tasks:
|
||||
- name: Validate required variables
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- app_name is defined
|
||||
fail_msg: "Required variables not set"
|
||||
|
||||
- name: Check if Docker is installed
|
||||
ansible.builtin.command: which docker
|
||||
register: docker_check
|
||||
changed_when: false # Check doesn't change state
|
||||
failed_when: false # Don't fail yet
|
||||
```
|
||||
|
||||
**Why it works:**
|
||||
|
||||
- Fails fast with clear messages
|
||||
- Prevents partial deployments
|
||||
- Uses `changed_when: false` for checks
|
||||
- Uses `failed_when: false` to check result later
|
||||
|
||||
### Pattern 3: Idempotent Docker Operations
|
||||
|
||||
**The Pattern:**
|
||||
|
||||
```yaml
|
||||
- name: Check if container is already running
|
||||
ansible.builtin.command: docker ps --filter name={{ app_name }}
|
||||
register: container_check
|
||||
changed_when: false
|
||||
|
||||
- name: Start Docker containers
|
||||
ansible.builtin.command: docker-compose up -d
|
||||
register: compose_up
|
||||
changed_when: "'Creating' in compose_up.stderr or 'Starting' in compose_up.stderr"
|
||||
when: container_check.stdout != app_name
|
||||
```
|
||||
|
||||
**Why it works:**
|
||||
|
||||
- Check first, then create
|
||||
- Only reports "changed" if actually started something
|
||||
- Conditional execution with `when:`
|
||||
- True idempotency
|
||||
|
||||
### Pattern 4: Block/Rescue Error Handling
|
||||
|
||||
**The Pattern:**
|
||||
|
||||
```yaml
|
||||
- name: Docker Management Block
|
||||
block:
|
||||
- name: Pull images
|
||||
# ... tasks ...
|
||||
|
||||
rescue:
|
||||
- name: Show container logs on failure
|
||||
ansible.builtin.command: docker-compose logs --tail=50
|
||||
register: container_logs
|
||||
|
||||
- name: Report failure
|
||||
ansible.builtin.fail:
|
||||
msg: "Deployment failed: {{ container_logs.stdout }}"
|
||||
```
|
||||
|
||||
**Why it works:**
|
||||
|
||||
- Groups related tasks
|
||||
- Automatic rollback on failure
|
||||
- Provides debugging info
|
||||
- Clean error reporting
|
||||
|
||||
**Learn more:** [../../patterns/error-handling.md](../../patterns/error-handling.md)
|
||||
|
||||
### Pattern 5: Health Checks with Retries
|
||||
|
||||
**The Pattern:**
|
||||
|
||||
```yaml
|
||||
- name: Wait for application to be healthy
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:8080/health"
|
||||
status_code: 200
|
||||
register: health_check
|
||||
until: health_check.status == 200
|
||||
retries: 30
|
||||
delay: 10
|
||||
```
|
||||
|
||||
**Why it works:**
|
||||
|
||||
- Automatic retries for transient failures
|
||||
- Configurable timeout (30 × 10s = 5 minutes)
|
||||
- Fails clearly if never becomes healthy
|
||||
|
||||
## Common Mistakes Avoided
|
||||
|
||||
This playbook avoids common anti-patterns:
|
||||
|
||||
### ❌ Anti-pattern 1: Hard-coded Secrets
|
||||
|
||||
```yaml
|
||||
# DON'T DO THIS!
|
||||
- name: Deploy config
|
||||
ansible.builtin.template:
|
||||
src: config.j2
|
||||
dest: /etc/app/config.yml
|
||||
vars:
|
||||
db_password: "MyPassword123" # NEVER!
|
||||
```
|
||||
|
||||
✅ **This playbook:** Uses Infisical with fallback to environment
|
||||
|
||||
### ❌ Anti-pattern 2: Missing changed_when
|
||||
|
||||
```yaml
|
||||
# DON'T DO THIS!
|
||||
- name: Start container
|
||||
ansible.builtin.command: docker start myapp
|
||||
# Always reports "changed" even if already running
|
||||
```
|
||||
|
||||
✅ **This playbook:** Checks first, uses `changed_when` to detect actual changes
|
||||
|
||||
### ❌ Anti-pattern 3: No Error Handling
|
||||
|
||||
```yaml
|
||||
# DON'T DO THIS!
|
||||
- name: Deploy app
|
||||
ansible.builtin.command: deploy.sh
|
||||
# No check if it worked, no cleanup on failure
|
||||
```
|
||||
|
||||
✅ **This playbook:** Uses block/rescue, verifies success
|
||||
|
||||
### ❌ Anti-pattern 4: Secrets in Logs
|
||||
|
||||
```yaml
|
||||
# DON'T DO THIS!
|
||||
- name: Set password
|
||||
ansible.builtin.command: set-password {{ password }}
|
||||
# Password visible in Ansible output!
|
||||
```
|
||||
|
||||
✅ **This playbook:** Uses `no_log: true` on sensitive tasks
|
||||
|
||||
## Customization
|
||||
|
||||
### Different Application
|
||||
|
||||
Change variables:
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
app_name: "my-other-app"
|
||||
app_dir: "/opt/my-other-app"
|
||||
```
|
||||
|
||||
### Different Secrets
|
||||
|
||||
Add more secret retrievals:
|
||||
|
||||
```yaml
|
||||
- name: Retrieve JWT secret
|
||||
ansible.builtin.include_tasks: ../../tasks/infisical-secret-lookup.yml
|
||||
vars:
|
||||
secret_name: 'JWT_SECRET'
|
||||
secret_var_name: 'jwt_secret'
|
||||
```
|
||||
|
||||
### Skip Health Check
|
||||
|
||||
```bash
|
||||
ansible-playbook docker-deployment.yml --skip-tags verify
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Infisical Authentication Failed
|
||||
|
||||
**Error:** `Missing Infisical authentication credentials`
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Check environment variables
|
||||
echo $INFISICAL_UNIVERSAL_AUTH_CLIENT_ID
|
||||
echo $INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET
|
||||
|
||||
# OR use fallback
|
||||
export DB_PASSWORD="fallback-password"
|
||||
```
|
||||
|
||||
### Docker Not Installed
|
||||
|
||||
**Error:** `Docker is not installed`
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Install Docker on target host
|
||||
ssh ansible@docker-host
|
||||
sudo apt update
|
||||
sudo apt install docker.io docker-compose
|
||||
```
|
||||
|
||||
### Container Won't Start
|
||||
|
||||
**Error:** `Docker deployment failed`
|
||||
|
||||
**Solution:** Playbook shows logs automatically in rescue block. Review output for errors.
|
||||
|
||||
**Manual check:**
|
||||
|
||||
```bash
|
||||
ssh ansible@docker-host
|
||||
cd /opt/my-application
|
||||
docker-compose logs
|
||||
```
|
||||
|
||||
### Health Check Timeout
|
||||
|
||||
**Error:** `Wait for application to be healthy` times out
|
||||
|
||||
**Solution:**
|
||||
|
||||
```yaml
|
||||
# Increase retries/delay
|
||||
retries: 60 # 10 minutes
|
||||
delay: 10
|
||||
```
|
||||
|
||||
## Testing the Playbook
|
||||
|
||||
### Check Idempotency
|
||||
|
||||
```bash
|
||||
# Run twice - second run should show no changes
|
||||
ansible-playbook docker-deployment.yml
|
||||
ansible-playbook docker-deployment.yml # Should be all "ok", no "changed"
|
||||
```
|
||||
|
||||
### Run Linters
|
||||
|
||||
```bash
|
||||
# Ansible lint
|
||||
ansible-lint docker-deployment.yml
|
||||
|
||||
# Custom idempotency check
|
||||
../../tools/check_idempotency.py docker-deployment.yml
|
||||
|
||||
# Full lint suite
|
||||
../../tools/lint-all.sh
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Learn More Patterns
|
||||
|
||||
- **Error Handling:** [../../patterns/error-handling.md](../../patterns/error-handling.md)
|
||||
- **Secrets Management:** [../../patterns/secrets-management.md](../../patterns/secrets-management.md)
|
||||
- **Common Mistakes:** [../../anti-patterns/common-mistakes.md](../../anti-patterns/common-mistakes.md)
|
||||
|
||||
### Additional Examples
|
||||
|
||||
- **Basic Playbook:** `../01-basic-playbook/` - Simpler starting point
|
||||
- **Repository Playbooks:** `../../../ansible/playbooks/` - Real production playbooks
|
||||
|
||||
### Best Practices
|
||||
|
||||
Review the main skill:
|
||||
|
||||
- [../../SKILL.md](../../SKILL.md) - Complete best practices guide
|
||||
|
||||
## Why These Patterns Matter
|
||||
|
||||
**In Production:**
|
||||
|
||||
- ✅ Secrets never in version control
|
||||
- ✅ Playbooks are truly idempotent
|
||||
- ✅ Clear error messages for troubleshooting
|
||||
- ✅ Audit trail for all operations
|
||||
- ✅ Rollback on failures
|
||||
|
||||
**For Teams:**
|
||||
|
||||
- ✅ Consistent patterns across playbooks
|
||||
- ✅ Easy to understand and maintain
|
||||
- ✅ Self-documenting code
|
||||
- ✅ Reduced bus factor
|
||||
|
||||
**For You:**
|
||||
|
||||
- ✅ Confidence in deployments
|
||||
- ✅ Less time debugging
|
||||
- ✅ Better sleep at night!
|
||||
@@ -0,0 +1,211 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Docker Deployment with Infisical Secrets
|
||||
# =============================================================================
|
||||
# This playbook demonstrates best practices from Virgo-Core:
|
||||
# - Infisical secrets management (using reusable task)
|
||||
# - Proper error handling with changed_when/failed_when
|
||||
# - Idempotent command execution
|
||||
# - No secrets in logs (no_log: true)
|
||||
# - Fully qualified module names (FQCN)
|
||||
# - Task organization with blocks
|
||||
|
||||
- name: Deploy Docker application with secrets from Infisical
|
||||
hosts: docker_hosts
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
app_name: "my-application"
|
||||
app_dir: "/opt/{{ app_name }}"
|
||||
infisical_project_id: "7b832220-24c0-45bc-a5f1-ce9794a31259"
|
||||
infisical_env: "prod"
|
||||
infisical_path: "/doggos-cluster"
|
||||
|
||||
# ==========================================================================
|
||||
# Pre-flight Checks
|
||||
# ==========================================================================
|
||||
|
||||
pre_tasks:
|
||||
- name: Validate required variables
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- app_name is defined and app_name | length > 0
|
||||
- app_dir is defined
|
||||
- infisical_project_id is defined
|
||||
fail_msg: "Required variables not set"
|
||||
success_msg: "All required variables present"
|
||||
tags: [always]
|
||||
|
||||
- name: Check if Docker is installed
|
||||
ansible.builtin.command: which docker
|
||||
register: docker_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
tags: [always]
|
||||
|
||||
- name: Fail if Docker not installed
|
||||
ansible.builtin.fail:
|
||||
msg: |
|
||||
Docker is not installed on {{ inventory_hostname }}
|
||||
Please install Docker first: sudo apt install docker.io
|
||||
when: docker_check.rc != 0
|
||||
tags: [always]
|
||||
|
||||
# ==========================================================================
|
||||
# Main Tasks
|
||||
# ==========================================================================
|
||||
|
||||
tasks:
|
||||
# ========================================================================
|
||||
# Retrieve Secrets from Infisical
|
||||
# ========================================================================
|
||||
|
||||
- name: Secrets Management Block
|
||||
block:
|
||||
- name: Retrieve database password from Infisical
|
||||
ansible.builtin.include_tasks: ../../tasks/infisical-secret-lookup.yml
|
||||
vars:
|
||||
secret_name: 'DB_PASSWORD'
|
||||
secret_var_name: 'db_password'
|
||||
fallback_env_var: 'DB_PASSWORD' # Optional fallback
|
||||
|
||||
- name: Retrieve API key from Infisical
|
||||
ansible.builtin.include_tasks: ../../tasks/infisical-secret-lookup.yml
|
||||
vars:
|
||||
secret_name: 'API_KEY'
|
||||
secret_var_name: 'api_key'
|
||||
fallback_env_var: 'API_KEY'
|
||||
|
||||
- name: Retrieve Redis password from Infisical
|
||||
ansible.builtin.include_tasks: ../../tasks/infisical-secret-lookup.yml
|
||||
vars:
|
||||
secret_name: 'REDIS_PASSWORD'
|
||||
secret_var_name: 'redis_password'
|
||||
fallback_env_var: 'REDIS_PASSWORD'
|
||||
|
||||
tags: [secrets, config]
|
||||
|
||||
# ========================================================================
|
||||
# Application Setup
|
||||
# ========================================================================
|
||||
|
||||
- name: Application Deployment Block
|
||||
block:
|
||||
- name: Create application directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ app_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: Deploy application configuration
|
||||
ansible.builtin.template:
|
||||
src: app-config.yml.j2
|
||||
dest: "{{ app_dir }}/config.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600' # Secure permissions for config with secrets
|
||||
notify: Restart application
|
||||
no_log: true # Config contains secrets
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ app_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
rescue:
|
||||
- name: Report deployment failure
|
||||
ansible.builtin.fail:
|
||||
msg: "Failed to deploy application configuration"
|
||||
|
||||
tags: [deploy, config]
|
||||
|
||||
# ========================================================================
|
||||
# Docker Operations (with proper idempotency)
|
||||
# ========================================================================
|
||||
|
||||
- name: Docker Management Block
|
||||
block:
|
||||
- name: Check if container is already running
|
||||
ansible.builtin.command: docker ps --filter name={{ app_name }} --format "{{ '{{' }}.Names{{ '}}' }}"
|
||||
register: container_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Pull Docker images
|
||||
ansible.builtin.command: docker-compose -f {{ app_dir }}/docker-compose.yml pull
|
||||
args:
|
||||
chdir: "{{ app_dir }}"
|
||||
register: pull_result
|
||||
changed_when: "'Downloaded newer image' in pull_result.stdout"
|
||||
when: container_check.stdout != app_name
|
||||
|
||||
- name: Start Docker containers
|
||||
ansible.builtin.command: docker-compose -f {{ app_dir }}/docker-compose.yml up -d
|
||||
args:
|
||||
chdir: "{{ app_dir }}"
|
||||
register: compose_up
|
||||
changed_when: "'Creating' in compose_up.stderr or 'Starting' in compose_up.stderr"
|
||||
when: container_check.stdout != app_name
|
||||
|
||||
- name: Wait for application to be healthy
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:8080/health"
|
||||
status_code: 200
|
||||
register: health_check
|
||||
until: health_check.status == 200
|
||||
retries: 30
|
||||
delay: 10
|
||||
changed_when: false
|
||||
|
||||
rescue:
|
||||
- name: Show container logs on failure
|
||||
ansible.builtin.command: docker-compose -f {{ app_dir }}/docker-compose.yml logs --tail=50
|
||||
args:
|
||||
chdir: "{{ app_dir }}"
|
||||
register: container_logs
|
||||
changed_when: false
|
||||
|
||||
- name: Report Docker failure
|
||||
ansible.builtin.fail:
|
||||
msg: |
|
||||
Docker deployment failed
|
||||
Logs: {{ container_logs.stdout }}
|
||||
|
||||
tags: [deploy, docker]
|
||||
|
||||
# ========================================================================
|
||||
# Verification
|
||||
# ========================================================================
|
||||
|
||||
- name: Verify application is running
|
||||
ansible.builtin.command: docker ps --filter name={{ app_name }} --filter status=running --format "{{ '{{' }}.Status{{ '}}' }}"
|
||||
register: running_check
|
||||
changed_when: false
|
||||
failed_when: "'Up' not in running_check.stdout"
|
||||
tags: [verify]
|
||||
|
||||
- name: Report deployment success
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
✓ Application deployed successfully
|
||||
Container: {{ app_name }}
|
||||
Status: {{ running_check.stdout }}
|
||||
Health endpoint: http://{{ inventory_hostname }}:8080/health
|
||||
tags: [verify]
|
||||
|
||||
# ==========================================================================
|
||||
# Handlers
|
||||
# ==========================================================================
|
||||
|
||||
handlers:
|
||||
- name: Restart application
|
||||
ansible.builtin.command: docker-compose -f {{ app_dir }}/docker-compose.yml restart
|
||||
args:
|
||||
chdir: "{{ app_dir }}"
|
||||
changed_when: true
|
||||
Reference in New Issue
Block a user