Skip to main content

Ansible Application Deployment

Introduction

Application deployment is one of the most critical and frequent tasks in modern software development workflows. Manual deployments are time-consuming, error-prone, and difficult to reproduce consistently. Ansible provides a powerful solution to automate application deployments, ensuring consistency, repeatability, and efficiency across your infrastructure.

In this tutorial, we'll explore how to use Ansible for automating application deployments, from basic concepts to practical real-world examples. By the end, you'll understand how to implement reliable deployment pipelines that scale with your needs.

Why Use Ansible for Application Deployment?

Ansible offers several advantages for application deployment:

  • Agentless architecture: No need to install special software on target servers
  • Simple YAML syntax: Easy to read and write playbooks
  • Idempotent operations: Running the same playbook multiple times produces the same result
  • Extensive module library: Built-in support for most deployment tasks
  • Declarative approach: You specify the desired state rather than step-by-step instructions

Prerequisites

Before we begin, ensure you have:

  • Ansible installed on your control node (version 2.9+)
  • SSH access to your target servers
  • Basic understanding of YAML syntax
  • Familiarity with basic Ansible concepts (playbooks, roles, inventory)

Basic Application Deployment Flow

Let's visualize a typical application deployment workflow with Ansible:

Creating Your First Deployment Playbook

Let's create a basic playbook for deploying a simple web application:

yaml
---
- name: Deploy Web Application
hosts: web_servers
become: true
vars:
app_name: mywebapp
app_version: 1.2.0
app_root: /var/www/{{ app_name }}

tasks:
- name: Ensure application directory exists
file:
path: "{{ app_root }}"
state: directory
owner: www-data
group: www-data
mode: '0755'

- name: Download application archive
get_url:
url: "https://example.com/releases/{{ app_name }}-{{ app_version }}.tar.gz"
dest: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
mode: '0640'

- name: Extract application archive
unarchive:
src: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
dest: "{{ app_root }}"
remote_src: yes
owner: www-data
group: www-data

- name: Configure application
template:
src: config.j2
dest: "{{ app_root }}/config.yml"
owner: www-data
group: www-data
mode: '0640'

- name: Restart web service
service:
name: nginx
state: restarted

This playbook:

  1. Creates the application directory
  2. Downloads the application archive
  3. Extracts it to the correct location
  4. Applies configuration using a template
  5. Restarts the web server to apply changes

Advanced Deployment Strategies

Blue-Green Deployment

Blue-green deployment is a technique that reduces downtime by maintaining two identical production environments, only one of which is live at any time.

yaml
---
- name: Blue-Green Deployment
hosts: web_servers
become: true
vars:
app_name: mywebapp
app_version: 1.2.0
blue_dir: /var/www/blue
green_dir: /var/www/green
live_symlink: /var/www/current

tasks:
- name: Determine current live environment
stat:
path: "{{ live_symlink }}"
register: symlink_stat

- name: Set target directory
set_fact:
inactive_dir: "{{ green_dir if (symlink_stat.stat.exists and symlink_stat.stat.lnk_source == blue_dir) else blue_dir }}"

- name: Ensure target directory exists
file:
path: "{{ inactive_dir }}"
state: directory
owner: www-data
group: www-data
mode: '0755'

- name: Deploy application to target directory
unarchive:
src: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
dest: "{{ inactive_dir }}"
remote_src: yes

- name: Configure application
template:
src: config.j2
dest: "{{ inactive_dir }}/config.yml"

- name: Switch to new deployment
file:
src: "{{ inactive_dir }}"
dest: "{{ live_symlink }}"
state: link
owner: www-data
group: www-data
notify: restart web service

handlers:
- name: restart web service
service:
name: nginx
state: restarted

Rolling Deployment

Rolling deployments update servers one by one to minimize downtime:

yaml
---
- name: Rolling Deployment
hosts: web_servers
serial: 1 # Process one server at a time
become: true
vars:
app_name: mywebapp
app_version: 1.2.0
app_root: /var/www/{{ app_name }}

tasks:
- name: Remove server from load balancer
delegate_to: "{{ item }}"
with_items: "{{ groups['load_balancers'] }}"
shell: "/usr/local/bin/disable_backend.sh {{ inventory_hostname }}"

- name: Deploy application
include_tasks: deploy_tasks.yml

- name: Verify application health
uri:
url: "http://{{ inventory_hostname }}/health"
return_content: yes
register: health_check
failed_when: "'OK' not in health_check.content"
retries: 5
delay: 10

- name: Add server back to load balancer
delegate_to: "{{ item }}"
with_items: "{{ groups['load_balancers'] }}"
shell: "/usr/local/bin/enable_backend.sh {{ inventory_hostname }}"

Using Ansible Roles for Deployment

For more complex applications, it's best practice to organize your deployment code into roles. Here's a simple structure:

roles/
├── app_deploy/
│ ├── defaults/
│ │ └── main.yml
│ ├── tasks/
│ │ ├── main.yml
│ │ ├── install.yml
│ │ ├── configure.yml
│ │ └── service.yml
│ ├── templates/
│ │ └── config.j2
│ └── handlers/
│ └── main.yml

You can then create a playbook that uses this role:

yaml
---
- name: Deploy Application
hosts: app_servers
become: true
roles:
- role: app_deploy
vars:
app_version: 1.3.0
app_environment: production
app_debug_mode: false

Handling Deployment Failures

Ansible provides mechanisms to handle deployment failures gracefully:

yaml
---
- name: Deploy with Rollback
hosts: web_servers
become: true
vars:
app_name: mywebapp
app_version: 1.2.0
app_root: /var/www/{{ app_name }}
backup_dir: /var/www/backups

tasks:
- name: Create backup of current deployment
archive:
path: "{{ app_root }}/"
dest: "{{ backup_dir }}/{{ app_name }}-backup-{{ ansible_date_time.iso8601 }}.tar.gz"
register: backup_result

- name: Deploy new version
block:
- name: Ensure app directory exists
file:
path: "{{ app_root }}"
state: directory

- name: Extract application
unarchive:
src: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
dest: "{{ app_root }}"
remote_src: yes

- name: Verify deployment
uri:
url: "http://localhost/health"
return_content: yes
register: health_check
failed_when: "'OK' not in health_check.content"

rescue:
- name: Deployment failed, restoring backup
unarchive:
src: "{{ backup_result.dest }}"
dest: "{{ app_root }}"
remote_src: yes

- name: Restart web service after rollback
service:
name: nginx
state: restarted

- name: Alert on deployment failure
debug:
msg: "Deployment failed, system rolled back to previous version."

- fail:
msg: "Deployment failed and was rolled back. Check logs for details."

Real-World Example: Deploying a Node.js Application

Let's put everything together with a real-world example of deploying a Node.js application:

yaml
---
- name: Deploy Node.js Application
hosts: app_servers
become: true
vars:
app_name: nodejsapp
app_version: 2.1.0
app_root: /opt/applications/{{ app_name }}
app_user: nodejs
app_port: 3000
node_version: 16.x

tasks:
- name: Ensure Node.js is installed
include_role:
name: geerlingguy.nodejs
vars:
nodejs_version: "{{ node_version }}"

- name: Ensure app user exists
user:
name: "{{ app_user }}"
state: present
system: yes

- name: Ensure application directory exists
file:
path: "{{ app_root }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'

- name: Clone application repository
git:
repo: "https://github.com/example/{{ app_name }}.git"
version: "v{{ app_version }}"
dest: "{{ app_root }}"
become_user: "{{ app_user }}"
register: git_clone

- name: Install application dependencies
npm:
path: "{{ app_root }}"
state: present
production: yes
become_user: "{{ app_user }}"
when: git_clone.changed

- name: Configure application environment
template:
src: env.j2
dest: "{{ app_root }}/.env"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0640'
notify: restart application

- name: Setup systemd service
template:
src: nodejs-app.service.j2
dest: "/etc/systemd/system/{{ app_name }}.service"
owner: root
group: root
mode: '0644'
notify: restart application

- name: Ensure application is started and enabled
systemd:
name: "{{ app_name }}"
state: started
enabled: yes
daemon_reload: yes

handlers:
- name: restart application
systemd:
name: "{{ app_name }}"
state: restarted

Let's also create the necessary template files:

env.j2:

NODE_ENV=production
PORT={{ app_port }}
DB_HOST={{ db_host }}
DB_USER={{ db_user }}
DB_PASS={{ db_password }}
LOG_LEVEL=info

nodejs-app.service.j2:

[Unit]
Description={{ app_name }} service
After=network.target

[Service]
Environment=NODE_ENV=production
Type=simple
User={{ app_user }}
WorkingDirectory={{ app_root }}
ExecStart=/usr/bin/node {{ app_root }}/app.js
Restart=on-failure

[Install]
WantedBy=multi-user.target

Automating Database Migrations

For applications that require database migrations during deployment:

yaml
---
- name: Run Database Migrations
hosts: app_servers
become: true
become_user: "{{ app_user }}"
vars:
app_root: /opt/applications/myapp
migration_command: "npm run migrate"

tasks:
- name: Take database backup
shell: "/usr/local/bin/backup_db.sh"
delegate_to: "{{ groups['database'][0] }}"
run_once: true

- name: Run database migrations
shell: "cd {{ app_root }} && {{ migration_command }}"
register: migration_result
failed_when: migration_result.rc != 0
run_once: true # Only run from one server

- name: Show migration results
debug:
var: migration_result.stdout_lines
when: migration_result.stdout_lines is defined

Deployment Notification and Logging

It's important to notify your team about deployments and keep records:

yaml
---
- name: Deployment Notifications
hosts: localhost
connection: local
vars:
app_name: mywebapp
app_version: 1.2.0
env: production
team_slack_webhook: "https://hooks.slack.com/services/TXXXXX/BXXXXX/XXXXXXX"

tasks:
- name: Record deployment in log
local_action:
module: lineinfile
path: "/var/log/deployments.log"
line: "{{ ansible_date_time.iso8601 }} - Deployed {{ app_name }}-{{ app_version }} to {{ env }}"
create: yes

- name: Send Slack notification
uri:
url: "{{ team_slack_webhook }}"
method: POST
body_format: json
body:
text: ":rocket: Deployment completed: {{ app_name }}-{{ app_version }} to {{ env }} environment"
register: slack_notification
failed_when: slack_notification.status != 200

Security Considerations in Deployment

When deploying applications, always consider security best practices:

yaml
---
- name: Secure Application Deployment
hosts: web_servers
become: true
vars:
app_root: /var/www/myapp

tasks:
- name: Set secure file permissions
file:
path: "{{ app_root }}"
state: directory
owner: www-data
group: www-data
mode: '0750'
recurse: yes

- name: Protect sensitive configuration files
file:
path: "{{ item }}"
mode: '0640'
owner: www-data
group: www-data
with_items:
- "{{ app_root }}/config/database.yml"
- "{{ app_root }}/.env"
- "{{ app_root }}/config/secrets.yml"

- name: Remove development files and directories
file:
path: "{{ app_root }}/{{ item }}"
state: absent
with_items:
- "test"
- "spec"
- ".git"
- "README.md"
- "CONTRIBUTING.md"

Summary

In this tutorial, we've covered:

  • The basics of application deployment with Ansible
  • How to create deployment playbooks for various scenarios
  • Advanced deployment strategies like blue-green and rolling deployments
  • Using Ansible roles to organize deployment code
  • Handling deployment failures and rollbacks
  • Real-world examples for deploying applications
  • Security considerations for application deployments

By automating your application deployments with Ansible, you can achieve faster, more reliable, and consistent deployments across your infrastructure, allowing your team to focus on building features rather than wrestling with deployment issues.

Additional Resources

To deepen your knowledge of Ansible application deployment:

  • Official Ansible documentation on deployments
  • The Ansible for DevOps book by Jeff Geerling
  • Ansible Galaxy for community roles related to application deployment
  • The "Infrastructure as Code" pattern in the DevOps handbook

Exercises

  1. Create a simple Ansible playbook to deploy a static website to a web server.
  2. Modify the Node.js deployment example to include health checks and automatic rollback.
  3. Implement a blue-green deployment strategy for an application of your choice.
  4. Create an Ansible role for deploying a Python/Django application.
  5. Set up a complete CI/CD pipeline that uses Ansible for deployment.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)