GitHub Actions CI/CD pipeline to deploy Go application
Introduction
Automating the deployment and building processes of your application can help in delivering your products quickly, reducing costs, and making them easier to produce. This article focuses on creating a CI/CD pipeline with GitHub Actions to deploy a Go application on a Linux Ubuntu virtual machine.
This Go application demonstrates the Music Album Service API. The main purpose of using GitHub Actions CI/CD is that we want to build an automate pipeline system for three actions: first, build the Go application, second, test the application, and third, deploy to an Ubuntu virtual machine that Nginx web server is already installed.
package main
import (
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Singer string `json:"singer"`
Price float64 `json:"price"`
}
var albums = []album{
{ID: "1", Title: "The record", Singer: "boygenius", Price: 20.00},
{ID: "2", Title: "Local Natives - Time Will Wait For No One", Singer: "Indie Rock", Price: 23.99},
{ID: "3", Title: "Clutch - Sunrise On Slaughter Beach", Singer: "Sarah Vaughan", Price: 9.09},
{ID: "4", Title: "Tears For Fears - The Tipping Point", Singer: "Sarah Vaughan", Price: 14.17},
}
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file", err.Error())
}
validApiKey := os.Getenv("APIKey")
router := gin.Default()
router.Use(ApiKeyMiddleware(validApiKey))
router.GET("/albums", getAlbums)
router.GET("/albums/:id", getAlbumByID)
router.Run("localhost:5000")
}
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
func ApiKeyMiddleware(apiKey string) gin.HandlerFunc {
return func(c *gin.Context) {
clientApiKey := c.GetHeader("API-Key")
if clientApiKey != apiKey {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"Error": "Invalid API key"})
return
}
c.Next()
}
}
In the project code we have created .github/workflows/ directory to store workflow file. and in .github/workflows/ directory we added a YAML file called CICD_GolangApp.yml
1- Workflow trigger
name: Golang-App-Workflow
'on':
push:
branches:
- main
In this case, the workflow should be triggered on every push event that occurs to the main branch whenever new changes are pushed to the project repository.
2- Workflow runners and jobs
jobs:
build-GoAppliation:
runs-on: ubuntu-latest
After added the event in the previous step we created a job called build-GoAppliation this job will be run on a virtual machine that hosted by GitHub it has the latest Ubuntu version.
Runners in GitHub Actions are virtual machines that execute or run some jobs.
3- Workflow step (actions/checkout@version)
steps:
- name: Checkout-Repository
uses: actions/checkout@v4
In this step we use actions/checkout@v4 action as a step to fetch the contents of the project repository into the runner environment.
Steps are tasks that will be executed in sequence in the runner environment.
Action is a command or a custom application that is performed on the runner server.
4- Workflow step (actions/setup-go@version)
- name: Setup-Golang
uses: actions/setup-go@v4
with:
go-version: '1.21'
check-latest: true
- run: go version
In this step we should use actions/setup-go@v4 Action to setup the latest Golang version on the runner environment to run and build the go application.
5- Workflow step (Install Dependencies)
- name: Install-Golang-Dependencies
run: go mod download
We created a nother step to runs go mod download command which fetches the dependencies already defined in the Go module file go.mod
6- Workflow step (Build)
- name: Build-Golang-App
run: GOOS=linux go build -o build/MusicAlbumAPI -v
This step is used to build the Go application later that can be run on Linux operating system and places the executable file in the build/ directory.
7- Workflow step (Debug build)
- name: Display-Build-Golang-App
run: ls -R build/
This step is used to run and execute ls and -R commands to list directory contents. It displays the contents of the build/ directory for debugging purposes.
8- Workflow step (Create .env file)
- name: Create-Env-File
run: 'echo "APIKey=${{ secrets.APIKey }}" > build/.env'
Our Go application requires an API KEY to provide its service to the clients we use this step to create an environment file in build/.env directory with a single environment variable APIKey and its value.
9- Workflow step (Display .env file)
- name: Display-Env-File
run: cat build/.env
After creating the .env file in the previous step we added another step to display the .env file for debugging purposes.
10- Workflow step run Unit Test (main_Test.go)
- name: Run-Unit-Test-Golang-App
run: go test
This step is used to run the unit test file in the go-appliation.
11- Workflow step (appleboy/scp-action@master)
- name: Copy-Build-Golang-App
uses: appleboy/scp-action@master
with:
host: '${{ secrets.VM_HOST }}'
username: '${{ secrets.VM_USERNAME }}'
key: '${{ secrets.VM_SSH_KEY }}'
port: '${{ secrets.VM_SSH_PORT }}'
source: build/
target: /var/www/albume-api
This action is used to copy files or directories from the GitHub Actions runner environment to the target directory on the remote server (Ubuntu VM) /var/www/albume-api it use the Secure Copy Protocol (SCP).
12– Workflow step (appleboy/ssh-action@master)
- name: Deploy-Build-Golang-App-To-Ubuntu-VM
uses: appleboy/ssh-action@master
with:
host: '${{ secrets.VM_HOST }}'
username: '${{ secrets.VM_USERNAME }}'
key: '${{ secrets.VM_SSH_KEY }}'
port: '${{ secrets.VM_SSH_PORT }}'
script: |
cd /var/www/albume-api
ls -l
sudo systemctl reload nginx
sudo systemctl status nginx
Finally, we use this action to execute commands on the remote server (Ubuntu VM) to list the files in the /var/www/albume-api directory, reload the Nginx configuration, and check the status of the Nginx server.
Secrets Our workflow uses GitHub secrets to store sensitive information that is required to run actions that use SSH to connect with the remote server (Ubuntu VM).
-
host: (secrets.VM_HOST) This is the hostname or IP address of the remote server.
-
username: (secrets.VM_USERNAME) This is the username of the remote server.
-
key: (secrets.VM_SSH_KEY) This is the private SSH key used to connect to the remote server.
-
port: (secrets.VM_SSH_PORT) : This is the SSH port number on the remote server.
Here is the final result of the GitHub Action Workflow to build and deploy golang application.
The source code of the project can be found on this GitHub repository.
name: Golang-App-Workflow
'on':
push:
branches:
- main
jobs:
build-GoAppliation:
runs-on: ubuntu-latest
steps:
- name: Checkout-Repository
uses: actions/checkout@v4
- name: Setup-Golang
uses: actions/setup-go@v4
with:
go-version: '1.21'
check-latest: true
- run: go version
- name: Install-Golang-Dependencies
run: go mod download
- name: Build-Golang-App
run: GOOS=linux go build -o build/MusicAlbumAPI -v
- name: Display-Build-Golang-App
run: ls -R build/
- name: Create-Env-File
run: 'echo "APIKey=${{ secrets.APIKey }}" > build/.env'
- name: Display-Env-File
run: cat build/.env
- name: Run-Unit-Test-Golang-App
run: go test
- name: Copy-Build-Golang-App
uses: appleboy/scp-action@master
with:
host: '${{ secrets.VM_HOST }}'
username: '${{ secrets.VM_USERNAME }}'
key: '${{ secrets.VM_SSH_KEY }}'
port: '${{ secrets.VM_SSH_PORT }}'
source: build/
target: /var/www/GoApplications
- name: Deploy-Build-Golang-App-To-Ubuntu-VM
uses: appleboy/ssh-action@master
with:
host: '${{ secrets.VM_HOST }}'
username: '${{ secrets.VM_USERNAME }}'
key: '${{ secrets.VM_SSH_KEY }}'
port: '${{ secrets.VM_SSH_PORT }}'
script: |
cd /var/www/GoApplications/build
ls -l
sudo systemctl reload nginx
sudo systemctl status nginx