Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:00:24 +08:00
commit 4768fb755a
22 changed files with 11534 additions and 0 deletions

View File

@@ -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!

View File

@@ -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