Files
2025-11-29 18:00:24 +08:00

476 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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!