⚙️Building a Linux Web Server with Terraform & Ansible – Part 8: GitHub Actions

By now, we have a working deploy.yml Ansible playbook that deploys our Flask app to our server. But running this manually every time we push a change isn’t very DevOps, is it?

In this part of the series, we’ll automate deployments using GitHub Actions so that every push to the main branch deploys our latest version automatically.


📁 Step 1: Create the GitHub Actions Workflow

First, create the folder to hold our CI/CD config:

mkdir .github/workflows/

Then create the workflow file at .github/workflows/deploy.yml.


📝 GitHub Actions Configuration

name: Deploy Flask App

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: 🛎️ Checkout repository
        uses: actions/checkout@v3
        
      - name: 📂 Set up SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

          # Preload host fingerprint to avoid prompt
          ssh-keyscan -f inventory >> ~/.ssh/known_hosts

      - name: 🚀 Run Ansible Playbook
        uses: dawidd6/action-ansible-playbook@v2
        with:
          playbook: deploy.yml
          inventory: inventory
          private_key: ${{ secrets.SSH_PRIVATE_KEY }}

📚 What’s Happening Here?

  • We check out the repository using GitHub’s official checkout action.
  • We load the SSH private key from GitHub Secrets and add it to ~/.ssh/id_rsa.
  • We dynamically load the host fingerprint from our inventory file using ssh-keyscan.
  • Finally, we run the Ansible playbook using a prebuilt action.

🤔 Why Not Just Use known_hosts in the Action?

You might wonder: “Can’t we just use the known_hosts: parameter instead of doing that SSH setup manually?”

Yes, we could use a simpler setup like this:

name: Deploy Flask App

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: 🛎️ Checkout repository
        uses: actions/checkout@v3

      - name: 🚀 Run Ansible Playbook
        uses: dawidd6/action-ansible-playbook@v2
        with:
          playbook: deploy.yml
          inventory: inventory
          private_key: ${{ secrets.SSH_PRIVATE_KEY }}
          known_hosts: |
            your.droplet.ip

This works fine—but you need to manually specify the IP address of your server.

Unfortunately, GitHub Actions doesn’t support things like $(cat inventory) inside the with: block, and you can’t inject it from a shell command. So while it’s shorter, it’s also less flexible—especially if you rebuild your droplet or rotate IPs often.

Personally, I prefer the first approach since it adapts automatically to the inventory file.


🔐 Creating a GitHub Actions SSH Key

We’ll need a dedicated SSH key that GitHub Actions can use to connect to your server. To generate one:

ssh-keygen -t ed25519 -f ci_key -N "" -C "github-actions-deploy-key"

Explanation:

  • -N "" → no passphrase
  • -C → gives the key a clear label
  • ci_key is the private key you’ll upload to GitHub
  • ci_key.pub is what you add to your server

🔐 Optionally: Remove the Passphrase From an Existing Key

If you’d rather not create a new SSH key just for GitHub Actions, you can reuse an existing key—you’ll just need to make sure it doesn’t have a passphrase, since GitHub Actions can’t handle interactive prompts.

To remove the passphrase:

ssh-keygen -p -f ~/.ssh/id_rsa
# Press Enter when asked for a new passphrase (i.e. leave it empty)

🟡 Caution: This will affect all tools or scripts that rely on that key, so make sure it’s safe to do.

Why a separate key is better:

  • Easier to revoke just the CI access without affecting your personal SSH config
  • Helps keep responsibilities and audit trails separate
  • Avoids accidental lockouts or security risks

🔐 Add the Key to Your Server

You’ll need to add the new public key to your droplet’s authorized keys.

If you’re using the same setup from earlier parts of the series, you can do this by updating create-sudo-user.yml (or creating a new playbook):

  - name: add SSH key for github actions
    authorized_key:
      user: admin
      state: present
      key: "{{ lookup('file', '~/.ssh/ci_key.pub') }}"
      path: "/home/admin/.ssh/authorized_keys"

🔒 Add the Private Key to GitHub Secrets

Go to:

GitHub > Repo > Settings > Secrets and variables > Actions

Create a new secret named:

SSH_PRIVATE_KEY

Paste in the full contents of your ci_key file (not the .pub one).


🚀 Triggering a Deploy

To test the pipeline, make a small change in your app. For example:

@app.route('/')
def hello():
    return "Deployed from GitHub Actions!"

Commit and push the change to the main branch:

git commit -am "Add github actions"
git push

Then open the Actions tab in GitHub to watch the workflow run.

Once complete, you can test your server:

curl http://<your-droplet-ip>

✅ You should see:

Deployed from GitHub Actions!

✅ Summary

In this article, we:

  • Created a GitHub Actions workflow to automate Ansible deployments
  • Configured SSH access securely using GitHub Secrets
  • Set up a dedicated SSH key for CI use
  • Automatically deployed our Flask app by pushing to main

With this in place, we now have a fully automated deployment pipeline—just push your code and let GitHub handle the rest 💥


🔜 Next Up…

In the next part of the series, we’ll install MySQL and refactor our app to use a real database.

See you there! 🐬

Leave a Reply

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