🚀Building a Linux Web Server with Terraform & Ansible – Part 7: Ansible Deployment

Now that everything is up and running, it’s time to add a simple deployment strategy using Ansible. This will allow us to push new versions of our app with a single command—and later we’ll automate it further using CI/CD.

Let’s walk through how to:

  • Restart Gunicorn automatically after a deploy
  • Structure an Ansible-based deployment
  • Deploy a Flask app from a GitHub repo
  • Set up versioned releases with symlinks
  • Restart Gunicorn automatically after a deploy

📄 Step 1: Create the Deployment Playbook

Inside your main ansible/ directory (next to main.yml), create a file named deploy.yml.

- name: Deploy Flask App
  hosts: digitalocean
  become: true

  vars_files:
    - vars.yml

  remote_user: "{{ username }}"

  tasks:

    - name: Generate timestamp
      command: date +"%Y-%m-%d-%H-%M-%S"
      register: timestamp
      changed_when: false

    - name: Set deployment paths
      set_fact:
        deploy_dir: "{{ home_dir }}/{{ timestamp.stdout }}"
        current_dir: "{{ home_dir }}/current"

    - name: Clone the latest repository
      git:
        repo: "https://github.com/{{ git_repo }}.git"
        dest: "{{ deploy_dir }}"
        version: main

    - name: Set up Python virtual environment
      command: python3 -m venv venv
      args:
        chdir: "{{ deploy_dir }}/app"

    - name: Install dependencies
      pip:
        requirements: "{{ deploy_dir }}/app/requirements.txt"
        virtualenv: "{{ deploy_dir }}/app/venv"

    - name: Update symlink for current deployment
      file:
        src: "{{ deploy_dir }}/app"
        dest: "{{ current_dir }}"
        state: link
        force: yes

    - name: Restart Gunicorn
      systemd:
        name: "{{ service_name }}"
        state: restarted
        daemon_reload: true

🔍 Breaking It Down

🔧 vars_files

  vars_files:
    - vars.yml

We’ve extracted shared variables (like username, home_dir, and service_name) to a vars.yml file. This keeps things DRY and makes variables reusable across main.yml and deploy.yml.

Make sure to update main.yml to include the same vars_files: block if you haven’t already.


🕒 Generate Timestamp & Deployment Paths

  tasks:

    - name: Generate timestamp
      command: date +"%Y-%m-%d-%H-%M-%S"
      register: timestamp
      changed_when: false

    - name: Set deployment paths
      set_fact:
        deploy_dir: "{{ home_dir }}/{{ timestamp.stdout }}"
        current_dir: "{{ home_dir }}/current"
  • We create a timestamp to give each deploy its own unique folder.
  • This enables versioned deployments, and switching versions is as simple as updating a symlink.

📦 Clone Repo & Install Dependencies

    - name: Clone the latest repository
      git:
        repo: "https://github.com/{{ git_repo }}.git"
        dest: "{{ deploy_dir }}"
        version: main

    - name: Set up Python virtual environment
      command: python3 -m venv venv
      args:
        chdir: "{{ deploy_dir }}/app"

    - name: Install dependencies
      pip:
        requirements: "{{ deploy_dir }}/app/requirements.txt"
        virtualenv: "{{ deploy_dir }}/app/venv"
  • We clone your app from GitHub into the deploy_dir.
  • A virtual environment is created inside the cloned app folder.
  • Dependencies from requirements.txt are installed using Ansible’s pip module.

🔗 Update Symlink & Restart Gunicorn

    - name: Update symlink for current deployment
      file:
        src: "{{ deploy_dir }}/app"
        dest: "{{ current_dir }}"
        state: link
        force: yes

    - name: Restart Gunicorn
      systemd:
        name: "{{ service_name }}"
        state: restarted
        daemon_reload: true
  • We update the current symlink to point to the new release folder.
  • Then we restart Gunicorn to pick up the changes.

This approach is simple, repeatable, and allows rollback by just repointing the current symlink.


📁 Step 2: Create the Flask App Repository

In the parent directory:

cd ..
mkdir app
cd app

requirements.txt

flask
gunicorn

wsgi.py

from src.app import app

if __name__ == "__main__":
    app.run()

src/app.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Deployed with Ansible!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

🔁 Step 3: Initialize the Git Repo

Login to GitHub and create a new private or public repository for your app.

Then, on the command line:

git init
git remote add origin ssh://<your repo>.git

Add a .gitignore file:

*.tfstate
*.tfstate.*
.terraform

🚨 Also make sure not to commit any .pem, .key, .env, or other sensitive files.

Now push the app:

git add .
git commit -m "Initial commit"
git push --set-upstream origin $(git_current_branch) # or "gpsup" if you're using zsh

🚀 Step 4: Run the Deploy Script

With your GitHub repo set up and pushed, you can now deploy:

play deploy.yml

Once the playbook finishes, test the app:

curl http://<droplet ip>

✅ You should see:

Deployed with Ansible!

✅ Summary

In this part, we:

  • Created a separate Ansible deployment playbook
  • Implemented timestamped deployments using symlinks
  • Cloned our app from GitHub and installed dependencies
  • Restarted Gunicorn to serve the new release

This gives us a solid, repeatable deployment process—no manual steps required!


🔜 Next Up…

In the next part of the series, we’ll hook this up to GitHub Actions so that every push to your repo automatically triggers a deployment—zero clicks required.

See you there 👋

Leave a Reply

Your email address will not be published. Required fields are marked *