Rebin

Rebin

Software Developer focusing on Microsoft development technologies

09 Dec 2023

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.

“Actions secrets”

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            

Categories