2025-03-30 | Modern Full-Stack Development with TypeScript (Cont.)

Introduction

This seminar is the final part of our Modern Full-Stack Development with TypeScript series.

Part 8: Deploy Docker using Ansible (25 minutes)

Ansible Overview

Ansible is an open-source automation tool that simplifies application deployment, configuration management, and task automation.

Key benefits:

  • Agentless architecture (only requires SSH)
  • YAML-based playbooks for easy readability
  • Idempotent operations (safe to run multiple times)
  • Extensive module library
  • Infrastructure as Code approach

Why Ansible for Docker Deployment?

  1. Consistency: Ensure identical deployments across environments
  2. Automation: Reduce manual steps and human error
  3. Scalability: Easily deploy to multiple servers
  4. Orchestration: Coordinate complex deployment sequences
  5. Configuration Management: Handle environment-specific settings

Setting Up Ansible

# inventory.yml
all:
  hosts:
    production:
      ansible_host: 192.168.1.100
      ansible_user: deploy
    staging:
      ansible_host: 192.168.1.101
      ansible_user: deploy
  vars:
    ansible_python_interpreter: /usr/bin/python3

Creating an Ansible Playbook for Docker Deployment

# deploy-docker.yml
---
- name: Deploy Application with Docker
  hosts: all
  become: true
  vars:
    app_name: fullstack-app
    docker_compose_dir: /opt/{{ app_name }}
    env: "{{ lookup('env', 'DEPLOY_ENV') | default('staging', true) }}"
  
  tasks:
    - name: Install required packages
      apt:
        name:
          - docker.io
          - docker-compose
          - python3-pip
        state: present
        update_cache: yes
    
    - name: Ensure Docker service is running
      service:
        name: docker
        state: started
        enabled: yes
    
    - name: Create application directory
      file:
        path: "{{ docker_compose_dir }}"
        state: directory
        mode: '0755'
    
    - name: Copy docker-compose file
      template:
        src: templates/docker-compose.{{ env }}.yml.j2
        dest: "{{ docker_compose_dir }}/docker-compose.yml"
    
    - name: Copy environment variables
      template:
        src: templates/.env.{{ env }}.j2
        dest: "{{ docker_compose_dir }}/.env"
        mode: '0600'
    
    - name: Pull latest Docker images
      command:
        cmd: docker-compose pull
        chdir: "{{ docker_compose_dir }}"
    
    - name: Deploy with docker-compose
      command:
        cmd: docker-compose up -d
        chdir: "{{ docker_compose_dir }}"
    
    - name: Prune unused Docker images
      command: docker image prune -af
      register: prune_result
      changed_when: "'Total reclaimed space:' in prune_result.stdout"

Environment-Specific Configuration Templates

# templates/docker-compose.production.yml.j2
version: '3.8'

services:
  api:
    image: {{ docker_registry }}/{{ app_name }}-api:{{ api_version }}
    restart: always
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      - postgres
    networks:
      - app-network
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
  
  client:
    image: {{ docker_registry }}/{{ app_name }}-client:{{ client_version }}
    restart: always
    ports:
      - "80:80"
    depends_on:
      - api
    networks:
      - app-network
  
  postgres:
    image: postgres:14
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_DB: ${DB_NAME}
    networks:
      - app-network
    restart: always

networks:
  app-network:
    driver: bridge

volumes:
  postgres-data:
    driver: local

Ansible Roles for Reusable Components

Organize your Ansible code into roles for better maintainability:

roles/
├── docker/
│   ├── tasks/
│   │   └── main.yml
│   └── handlers/
│       └── main.yml
├── nginx/
│   ├── tasks/
│   │   └── main.yml
│   ├── templates/
│   │   └── nginx.conf.j2
│   └── handlers/
│       └── main.yml
└── app/
    ├── tasks/
    │   └── main.yml
    ├── templates/
    │   ├── docker-compose.yml.j2
    │   └── .env.j2
    └── defaults/
        └── main.yml

Continuous Deployment with Ansible and CI/CD

# .github/workflows/deploy.yml
name: Deploy Application

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      
      - name: Install Ansible
        run: |
          python -m pip install --upgrade pip
          pip install ansible          
      
      - name: Set up SSH key
        uses: webfactory/ssh-agent@v0.7.0
        with:
          ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
      
      - name: Run Ansible playbook
        run: |
          ansible-playbook -i inventory.yml deploy-docker.yml          
        env:
          DEPLOY_ENV: production
          ANSIBLE_HOST_KEY_CHECKING: False

Best Practices for Ansible Deployments

  1. Vault for Secrets: Use Ansible Vault to encrypt sensitive data

    ansible-vault create secrets.yml
    ansible-playbook deploy.yml --ask-vault-pass
    
  2. Dynamic Inventories: Use cloud provider plugins for dynamic server discovery

  3. Idempotency: Ensure playbooks can be run multiple times without side effects

  4. Tags: Use tags to run specific parts of your playbook

    ansible-playbook deploy.yml --tags "update,restart"
    
  5. Handlers: Use handlers for actions that should only run when a change occurs

  6. Testing: Test playbooks with Molecule before production deployment

Monitoring and Maintenance

  • Set up regular health checks using Ansible
  • Create playbooks for common maintenance tasks:
    • Database backups
    • Log rotation
    • Certificate renewal
    • Security updates

Rollback Strategies

# rollback.yml
---
- name: Rollback to previous version
  hosts: all
  become: true
  vars:
    app_name: fullstack-app
    docker_compose_dir: /opt/{{ app_name }}
    previous_version: "{{ lookup('env', 'PREVIOUS_VERSION') }}"
  
  tasks:
    - name: Update docker-compose with previous version
      lineinfile:
        path: "{{ docker_compose_dir }}/docker-compose.yml"
        regexp: "image: .*/{{ app_name }}-api:.*"
        line: "    image: {{ docker_registry }}/{{ app_name }}-api:{{ previous_version }}"
    
    - name: Restart with previous version
      command:
        cmd: docker-compose up -d
        chdir: "{{ docker_compose_dir }}"

Conclusion and Next Steps (15 minutes)

Summary of the Full-Stack TypeScript Journey

  • Angular for dynamic frontend experiences
  • Nest.js for structured backend development
  • PostgreSQL and Prisma for type-safe data management
  • Swagger for API documentation
  • Docker for containerization
  • Ansible for automated deployment
  • Serverless architectures
  • Edge computing
  • WebAssembly
  • Micro-frontends
  • GraphQL adoption
  • AI-assisted development

Continuous Learning Resources

  • Community forums and Discord servers
  • Advanced courses and certifications
  • Open-source contribution opportunities
  • Tech conferences and meetups

Q&A Session

Open floor for questions about any aspect of the full-stack TypeScript ecosystem we’ve covered throughout this seminar series.



Last modified March 27, 2025: Edit members.yaml (21070ed)