DEV Community

Cover image for Stop Doing ClickOps: A Practical Guide to Terraform and GitHub Actions
Balamurugan pandian
Balamurugan pandian

Posted on

Stop Doing ClickOps: A Practical Guide to Terraform and GitHub Actions

We have all been there. It is late on a Friday afternoon, and you are manually clicking through a cloud provider console trying to remember which specific security group you need to attach to a new web server. This manual process is affectionately known in the industry as “ClickOps”, and it is a complete nightmare for scaling a tech team.

When you rely on human memory and manual clicks to build your infrastructure, mistakes are guaranteed. Environments drift out of sync. Deployments become scary events that require downtime and prayer.

Today, we are going to fix that. We will look at the foundation of modern DevOps by automating infrastructure using Terraform and setting up a continuous integration and continuous deployment (CI/CD) pipeline with GitHub Actions.

Step 1: Infrastructure as Code with Terraform

Instead of clicking buttons in a web UI, Infrastructure as Code allows us to write configuration files that describe exactly what our servers and databases should look like. Terraform is the industry standard for this.

Terraform uses a declarative language. You simply tell it what you want the end state to be, and it figures out the API calls required to make it happen.

Let us look at a basic example. Suppose we need to provision a simple Ubuntu server on AWS to host a new application. Here is what that looks like in Terraform.

# main.tf

# 1. Define the cloud provider
provider aws {
region = us-east-1
}

# 2. Look up the latest Ubuntu AMI
data aws_ami ubuntu {
most_recent = true
owners = [099720109477] # Canonical

filter {
name = name
values = [ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*]
}
}

# 3. Define the actual server resource
resource aws_instance web_server {
ami = data.aws_ami.ubuntu.id
instance_type = t3.micro

tags = {
Name = Production-Web-App
Environment = Prod
}
}
Enter fullscreen mode Exit fullscreen mode

This code is highly readable. We define AWS as our provider, search for the latest Ubuntu machine image, and then declare an EC2 instance.

To deploy this, you simply open your terminal and run two commands:

  1. - terraform init to download the AWS plugins.
  2. - terraform apply to review the plan and build the server.

If someone accidentally deletes this server, you do not have to panic. You just run terraform apply again, and your infrastructure is restored exactly as it was defined in the code. Your code repository is now the single source of truth for your infrastructure.

Step 2: Automating Deployments with GitHub Actions

Now that our server exists, how do we get our application code onto it? Manually logging into a server via SSH to pull code and restart services is just as bad as ClickOps. We need a pipeline.

GitHub Actions allows you to run automated workflows every time a specific event happens in your repository (like merging a pull request).
Here is an example of a workflow file that tests a Node.js application and then deploys it. You place this file inside your repository at .github/workflows/deploy.yml.

name: Node.js CI/CD Pipeline

# Trigger this workflow when code is pushed to the main branch
on:
push:
branches: [ “main” ]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
— name: Check out the repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ‘20’

- name: Install dependencies
run: npm ci

- name: Run unit tests
run: npm test

deploy:
# Only run the deploy job if the build-and-test job succeeds
needs: build-and-test
runs-on: ubuntu-latest

steps:
— name: Deploy to Production Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PROD_SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: 
cd /var/www/my-app
git pull origin main
npm install — production
pm2 restart web-app
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Pipeline

This workflow is split into two distinct jobs.

The build-and-test job spins up a fresh, isolated container on GitHub’s servers. It downloads your code, installs the required packages, and runs your test suite. If any test fails, the entire pipeline stops immediately. This prevents broken code from ever reaching your users.

If the tests pass, the deploy job takes over. It uses a popular community action to securely SSH into the AWS server we built earlier with Terraform. It then runs a simple script to pull the latest code, install production dependencies, and restart the application manager.

Notice how we use ${{ secrets.PROD_SERVER_IP }}. You should never hardcode sensitive information like IP addresses or SSH keys into your repository. GitHub provides a secure secrets manager to handle these variables safely.

The DevOps Payoff

Moving away from manual processes takes a little upfront effort. You have to learn the syntax of Terraform and figure out the quirks of YAML files for GitHub Actions.

However, the payoff is massive. When you combine Infrastructure as Code with automated pipelines, your deployments become incredibly boring and highly predictable. You can release new features on a Friday afternoon with confidence, knowing the machines will handle the heavy lifting. In the world of DevOps, boring is exactly what you want.

Top comments (0)