476 lines
9.8 KiB
Markdown
476 lines
9.8 KiB
Markdown
# 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!
|