TutorialsDec 10, 202512 min read

CI/CD for Go Microservices on Scaleway Kubernetes with CircleCI

Olususi Oluyemi

Fullstack Developer and Tech Author

Development teams depend on microservices to build, deploy, and scale features independently. Microservices have become the backbone of modern, scalable applications. Scaleway’s managed Kubernetes service (Kubernetes Kapsule) offers a powerful, cost-effective platform for running containerized workloads in the cloud. It’s a great fit for startups and solo engineers who want to focus on shipping features, not managing infrastructure.

In this tutorial, you’ll learn how to build a minimal Go microservice, containerize it with Docker, and deploy it to Scaleway Kubernetes Kapsule using Helm and CircleCI. By the end, you’ll have a fully automated, GitOps-friendly workflow for continuous delivery on a robust Kubernetes platform.

Prerequisites

Here’s what you will need to follow along with this tutorial:

Build the Go microservice

Start by building a simple Go microservice. This service will provide a feedback API, allowing users to submit their names and messages, which will be stored in memory for simplicity. This approach keeps the example lightweight and easy to follow, while still demonstrating the core concepts of microservice development and deployment.

Project setup

Create a new directory and initialize a Go module:

mkdir feedback-service
cd feedback-service
go mod init github.com/yemiwebby/feedback-service

This creates a new folder called feedback-service, opens it, and initializes a Go module.

Note: The project created in this article uses the module path github.com/yemiwebby/feedback-service, which matches the author’s GitHub repo. You can use your own GitHub username, just make sure to update the go.mod file and all internal import paths.

Define the feedback data structure

Create a folder named models and add a file named feedback.go. Paste this content into the file:

package models

type Feedback struct {
	Name    string `json:"name"`
	Message string `json:"message"`
}

This struct represents a single feedback entry, with fields for the user’s name and message. The struct tags ensure correct JSON encoding and decoding.

Implement the feedback API handler

The HTTP handler will process feedback submissions and retrievals. Start by creating a folder named handlers and add a file named feedback.go. Paste this code into the file:

package handlers

import (
	"encoding/json"
	"net/http"
	"sync"

	"github.com/yemiwebby/feedback-service/models"
)

var (
	feedbacks []models.Feedback
	mu        sync.Mutex
)

func FeedbackHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodPost:
		var fb models.Feedback
		if err := json.NewDecoder(r.Body).Decode(&fb); err != nil {
			http.Error(w, "Invalid input", http.StatusBadRequest)
			return
		}

		if fb.Name == "" || fb.Message == "" {
			http.Error(w, "Name and message required", http.StatusBadRequest)
			return
		}

		mu.Lock()
		feedbacks = append(feedbacks, fb)
		mu.Unlock()

		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(fb)

	case http.MethodGet:
		mu.Lock()
		defer mu.Unlock()

		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(feedbacks)

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

This handler supports both POST and GET requests on the /feedback endpoint. When a POST request is received, the handler expects a JSON payload containing a user’s name and message. It validates the input, and if both fields are present, it stores the feedback entry in memory. For GET requests, the handler responds with a JSON array containing all previously submitted feedback entries. To ensure that multiple requests do not cause data races or inconsistent states, a mutex (mu) is used to synchronize access to the shared feedback slice, making the handler safe for concurrent use.

Build the main application

Now you can bring everything together by creating the main entry point for your Go microservice. In the root of your project directory, create a file named main.go. This file will set up the HTTP server, define a simple health check endpoint, and route requests to your feedback handler. Enter this content into the file:

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/yemiwebby/feedback-service/handlers"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Server Ok")
	})

	mux.HandleFunc("/feedback", handlers.FeedbackHandler)

	log.Println("Server listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

This code initializes a new HTTP server that listens on port 8080.

It registers two routes:

  • /healthz performs a basic health check.
  • /feedback handles feedback submissions and retrievals using the handler you implemented earlier. The server logs a message when it starts and will report any fatal errors.

Testing the Go application locally

Before deploying your microservice, it’s important to verify that it works as expected on your local machine. Start by running the application:

go run main.go

This command launches the server on port 8080. You can now interact with the feedback API using curl or any HTTP client.

To submit feedback, send a POST request with a JSON payload:

curl -X POST http://localhost:8080/feedback \
 -H "Content-Type: application/json" \
 -d '{"name": "Yemi", "message": "Great job on the CircleCI tutorial!"}'

The server should respond with the feedback entry you just submitted:

{ "name": "Yemi", "message": "Great job on the CircleCI tutorial!" }

To retrieve all feedback entries, send a GET request:

curl http://localhost:8080/feedback

You should get a JSON array containing the feedback you submitted. For example:

[{ "name": "Yemi", "message": "Great job on the CircleCI tutorial!" }]

This confirms that your Go microservice is running correctly and handling requests as intended.

Add unit tests

Next, ensure your feedback handler works correctly and handles different scenarios. You’ll do that by writing unit tests using Go’s built-in testing tools. This helps catch bugs early and gives you confidence that your API behaves as expected.

Create a file named feedback_test.go in the handlers directory. Add this code:

package handlers

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/yemiwebby/feedback-service/models"
)

func TestFeedbackHandler(t *testing.T) {
	// Reset shared state
	feedbacks = []models.Feedback{}

	t.Run("GET empty feedback", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/feedback", nil)
		res := httptest.NewRecorder()

		FeedbackHandler(res, req)

		if res.Code != http.StatusOK {
			t.Fatalf("Expected 200, got %d", res.Code)
		}

		var got []models.Feedback
		if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
			t.Fatalf("Failed to decode response: %v", err)
		}

		if len(got) != 0 {
			t.Errorf("Expected 0 feedbacks, got %d", len(got))
		}
	})

	t.Run("POST valid feedback", func(t *testing.T) {
		payload := models.Feedback{Name: "User", Message: "Clean API!"}
		body, _ := json.Marshal(payload)

		req := httptest.NewRequest(http.MethodPost, "/feedback", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		res := httptest.NewRecorder()

		FeedbackHandler(res, req)

		if res.Code != http.StatusCreated {
			t.Fatalf("Expect 201 created, got %d", res.Code)
		}

		var got models.Feedback
		if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
			t.Fatalf("Failed to decode response: %v", err)
		}

		if got.Name != payload.Name || got.Message != payload.Message {
			t.Errorf("Mismatch in response: got %+v, expected %+v", got, payload)
		}
	})

	t.Run("POST invalid feedback (missing fields)", func(t *testing.T) {
		payload := models.Feedback{Name: "", Message: ""}
		body, _ := json.Marshal(payload)

		req := httptest.NewRequest(http.MethodPost, "/feedback", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		res := httptest.NewRecorder()

		FeedbackHandler(res, req)

		if res.Code != http.StatusBadRequest {
			t.Fatalf("Expected 400 Bad Request, got %d", res.Code)
		}
	})

	t.Run("GET after feedback added", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/feedback", nil)
		res := httptest.NewRecorder()

		FeedbackHandler(res, req)

		var got []models.Feedback
		if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
			t.Fatalf("Failed to decode response: %v", err)
		}

		if len(got) != 1 {
			t.Errorf("Expected 1 feedback, got %d", len(got))
		}
	})
}

This test suite covers the main scenarios for your feedback API:

  • Retrieving an empty list.
  • Submitting valid feedback.
  • Handling invalid input.
  • Confirming that feedback is stored and retrievable.

Each test uses Go’s httptest package to simulate HTTP requests and responses, making it easy to test your handler logic in isolation.

Run the tests

From your terminal run:

go test ./handlers -v

Your output should show that all tests passed. This confirms that your handler works as intended:

=== RUN   TestFeedbackHandler
=== RUN   TestFeedbackHandler/GET_empty_feedback
=== RUN   TestFeedbackHandler/POST_valid_feedback
=== RUN   TestFeedbackHandler/POST_invalid_feedback_(missing_fields)
=== RUN   TestFeedbackHandler/GET_after_feedback_added
--- PASS: TestFeedbackHandler (0.00s)
    --- PASS: TestFeedbackHandler/GET_empty_feedback (0.00s)
    --- PASS: TestFeedbackHandler/POST_valid_feedback (0.00s)
    --- PASS: TestFeedbackHandler/POST_invalid_feedback_(missing_fields) (0.00s)
    --- PASS: TestFeedbackHandler/GET_after_feedback_added (0.00s)
PASS
ok  	github.com/yemiwebby/feedback-service/handlers	(cached)

Package and containerize the Go microservice using Docker

To deploy your Go microservice anywhere, package it as a Docker container. Start by creating a new file named Dockerfile in your project root. Add this content:

FROM golang:1.24-alpine

WORKDIR /app

COPY go.mod ./
RUN go mod download

COPY . .

RUN go build -o server .

EXPOSE 8080

CMD ["./server"]

This Dockerfile uses a lightweight Go base image, sets up the working directory, installs dependencies, copies your source code, builds the Go binary, exposes port 8080, and sets the default command to run your server.

Build the Docker image

If you want your image to run on both AMD64 and ARM64 architectures (to support both Intel and Apple Silicon machines for example), use Docker Buildx. Build a multi-architecture image and push it to Docker Hub:

docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t yourdockerhubusername/feedback-service:latest --push .

Note: Make sure to replace yourdockerhubusername with your actual Docker Hub username.

If you only need to build for your current architecture, you can use the standard build command:

docker build -t yourdockerhubusername/feedback-service:latest --push .

Note: Make sure to replace yourdockerhubusername with your actual Docker Hub username.

These commands will build your Docker image and push it to your Docker Hub repository. After pushing, your image will be available at https://hub.docker.com/r/yourdockerhubusername/feedback-service.

Run the container locally

To verify that your Docker image works as expected, run the following command:

docker run -p 8080:8080 yourdockerhubusername/feedback-service:latest

Note: Make sure to replace yourdockerhubusername with your actual Docker Hub username.

Then, in a separate terminal, test the health endpoint:

curl http://localhost:8080/healthz

The output should be:

Server Ok

This confirms that your containerized Go microservice is running and accessible on your local machine.

Set up an API key on Scaleway

To interact with Scaleway services from your CLI or CI/CD pipeline, you’ll need to generate an API key. Log into the Scaleway console and create a new project, if you don’t have one yet.

Scaleway project creation

Next, go to the Scaleway API keys page to create a new API key. In the console:

  • Go to the IAM and API Keys section.
  • Choose an expiration date for your key if desired.
  • Make sure to select your preferred project.

Scaleway API key creation

Once the API key is generated, copy and store both your secret key and access key securely. You will need these credentials to authenticate with Scaleway services, and note that the secret key will only be shown once.

Creating a Scaleway Kapsule Kubernetes cluster

To deploy your Go microservice on Scaleway, you need to create a Kubernetes cluster using Scaleway’s managed Kubernetes Kapsule service. This process involves configuring the Scaleway CLI with your API keys, creating the cluster, and setting up access credentials.

Configure Scaleway CLI

Start by initializing the Scaleway CLI. This will prompt you for your Scaleway access key, secret key, default organization ID and default project. Make sure you have your API keys ready.

scw init

After completing the prompts, your CLI will be configured and ready to interact with Scaleway resources.

Create the Kapsule cluster

Now, create a new Kubernetes cluster. To create a cluster named feedback-cluster in the fr-par region with Kubernetes version 1.31.7, run:

scw k8s cluster create name=feedback-cluster region=fr-par version=1.31.7

You should get output similar to:

ID                <YOUR-CLUSTER-ID>
Type              kapsule
Name              feedback-cluster
Status            creating
Version           1.31.7
Region            fr-par
...

Once the cluster is created, make a note the cluster ID from the output. You’ll need it for the next steps of the tutorial.

Add a node pool

Add a node pool to your cluster. Replace <cluster-id> with your actual cluster ID:

scw k8s pool create cluster-id=<cluster-id> name=default-pool node-type=DEV1-M region=fr-par size=1 autoscaling=false

This command provisions a single node (you can scale this later as needed).

You should get output similar to:

ID                            <YOUR-POOL-ID>
ClusterID                     <YOUR-CLUSTER-ID>
CreatedAt                     now
UpdatedAt                     now
Name                          default-pool
Status                        scaling
Version                       1.31.7
NodeType                      dev1_m
...

Retrieve the Kubeconfig file

To interact with your cluster using kubectl, download the kubeconfig file. From the root of your project directory, run:

scw k8s kubeconfig get cluster-id=<your-cluster-id> > kubeconfig.yaml

This file contains the credentials and configuration needed to access your Kubernetes cluster.

Note: Be sure to add kubeconfig.yaml to your .gitignore to avoid committing sensitive information.

Set up your environment

Export the path to your kubeconfig file so kubectl uses it by default. Run:

export KUBECONFIG=$(pwd)/kubeconfig.yaml

Verify cluster access

Check that your node is ready:

kubectl get nodes

Your output should be similar to:

NAME                                             STATUS   ROLES    AGE     VERSION
scw-feedback-cluster-default-pool-a9eb2d730006   Ready    <none>   2m      v1.31.7

Secure your Kubeconfig

Restrict permissions on your kubeconfig file:

chmod 600 kubeconfig.yaml

Your Scaleway Kubernetes cluster is now ready for application deployment.

Create Helm chart

To deploy your Go microservice to Kubernetes in a reusable and configurable way, you’ll use Helm; the package manager for Kubernetes.

Scaffold the Helm chart

First, create the directory structure and files for your Helm chart:

mkdir -p helm/feedback-service/templates
touch helm/feedback-service/Chart.yaml
touch helm/feedback-service/values.yaml
touch helm/feedback-service/templates/deployment.yaml
touch helm/feedback-service/templates/service.yaml

Open helm/feedback-service/Chart.yaml and add this content:

apiVersion: v2
name: feedback-service
description: A simple feedback microservice written in Go
type: application
version: 0.1.0
appVersion: "1.0.0"

This file contains metadata about your Helm chart.

In helm/feedback-service/values.yaml, define the default values for your chart:

replicaCount: 1

image:
  repository: <your-dockerhub-username>/feedback-service
  pullPolicy: IfNotPresent
  tag: "latest"

service:
  type: ClusterIP
  port: 8080

resources: {}

nodeSelector: {}

tolerations: []

affinity: {}

Note: Make sure to replace yourdockerhubusername with your actual Docker Hub username.

This file defines the default values for your chart’s templates, such as image repository, tag, and service configuration:

Open helm/feedback-service/templates/deployment.yaml and add:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: feedback-service
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: feedback-service
  template:
    metadata:
      labels:
        app: feedback-service
    spec:
      containers:
        - name: feedback-service
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080

This template describes how to deploy your application as a Kubernetes Deployment. It uses values from values.yaml for flexibility:

Open helm/feedback-service/templates/service.yaml and enter:

apiVersion: v1
kind: Service
metadata:
  name: feedback-service
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: 8080
  selector:
    app: feedback-service

This template defines a Kubernetes Service to expose your deployment inside the cluster.

Notes:

  • Make sure to use spaces (not tabs) in all YAML files.
  • All values must be properly indented for Helm and Kubernetes to parse them correctly. </i>

With these files in place, you have a basic but production-ready Helm chart for your Go microservice.

Deploy the Kubernetes cluster

To deploy the feedback service to your Scaleway Kubernetes cluster, you’ll use Helm. Make sure Helm is installed on your local machine. If you haven’t installed it yet, follow the Helm installation guide.

Install your Helm chart with:

helm install feedback helm/feedback-service

Your output should be similar to:

NAME: feedback
LAST DEPLOYED: Thu Jul 3 06:42:07 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

Check that your pod is running:

kubectl get pods

Example output:

NAME                               READY   STATUS    RESTARTS   AGE
feedback-service-785d98668-dwbk9   1/1     Running   0          4h47m

Check the service

List your services to confirm the feedback service is running:

kubectl get svc

Your output:

NAME               TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
feedback-service   ClusterIP   10.32.2.129   <none>        8080/TCP   3m31s
kubernetes         ClusterIP   10.32.0.1     <none>        443/TCP    43m

Expose the service externally

To expose your service externally, you can use a LoadBalancer service type or an Ingress controller. For this tutorial, port-forward the service for local testing.

kubectl port-forward svc/feedback-service 8080:8080

Now, in another terminal, test the health endpoint:

curl http://localhost:8080/healthz

Your output:

Server Ok

Your Go microservice is now running on Kubernetes and accessible locally.

Configure CircleCI

To automate testing, building, and deployment, you’ll use CircleCI. This configuration will run your Go unit tests, build and push your Docker image to Docker Hub, and deploy your Helm chart to Scaleway Kubernetes.

Create a .circleci/config.yml file in your project root. Open it and add:

version: 2.1

executors:
  go-docker:
    docker:
      - image: cimg/go:1.24.4

jobs:
  test:
    executor: go-docker
    steps:
      - checkout
      - run: go test ./...

  build-and-push:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Build Docker image
          command: |
            docker build --platform linux/amd64 -t $DOCKERHUB_USERNAME/feedback-service:latest .
      - run:
          name: Push to Docker Hub
          command: |
            echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
            docker push $DOCKERHUB_USERNAME/feedback-service:latest

  deploy:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Install Helm & Kubectl
          command: |
            curl -LO https://get.helm.sh/helm-v3.14.4-linux-amd64.tar.gz
            tar -zxvf helm-v3.14.4-linux-amd64.tar.gz
            sudo mv linux-amd64/helm /usr/local/bin/helm

            curl -LO "https://dl.k8s.io/release/v1.31.1/bin/linux/amd64/kubectl"
            chmod +x kubectl
            sudo mv kubectl /usr/local/bin/

      - run:
          name: Set up kubeconfig
          command: |
            echo $KUBECONFIG_CONTENT | base64 -d > kubeconfig.yaml
            export KUBECONFIG=$(pwd)/kubeconfig.yaml
            kubectl get nodes

      - run:
          name: Deploy with Helm
          command: |
            export KUBECONFIG=$(pwd)/kubeconfig.yaml
            helm upgrade --install feedback ./helm/feedback-service

workflows:
  ci-cd:
    jobs:
      - test
      - build-and-push:
          requires:
            - test
      - deploy:
          requires:
            - build-and-push

This CircleCI configuration defines three jobs. The test job checks out your code and runs all Go tests to ensure your application works as expected. The build-and-push job builds a Docker image for your service and pushes it to Docker Hub using your credentials. The deploy job:

  • Installs Helm and Kubectl.
  • Sets up your Kubernetes configuration.
  • Deploys (or upgrades) your application on the Scaleway Kubernetes cluster using your Helm chart.

The workflow ensures that tests must pass before building and pushing the Docker image, and that deployment happens only after a successful build and push.

Save your changes, then push your project to GitHub.

Set up the project in CircleCI

Log into CircleCI and create a new project. Select the feedback-service repository (or whichever repo you’re using for this tutorial).

CircleCI project setup

Choose the branch that contains the .circleci/config.yml file and click Set Up Project. This will trigger the first build.

The pipeline will likely fail at first. That’s to be expected, because you still need to add the required environment variables.

CircleCI pipeline failure

Create environment variables in CircleCI

In CircleCI, go to your project settings and add these environment variables:

  • DOCKERHUB_USERNAME: Your Docker Hub username
  • DOCKERHUB_PASSWORD: Your Docker Hub password or access token with the read and write permission
  • KUBECONFIG_CONTENT: The base64-encoded content of your kubeconfig.yaml file. You can generate this by running base64 -w 0 kubeconfig.yaml in your terminal.

Now, re-run the pipeline, and it should pass successfully.

CircleCI pipeline success

Verify deployment

After the pipeline completes successfully, your Go microservice should be deployed to your Scaleway Kubernetes cluster. You can verify again by running:

kubectl get pods

Your feedback-service pod should be running. If it’s not, you can check the logs for more information:

kubectl logs <pod-name>

Conclusion

By following this tutorial, you have built a production-ready Go microservice, packaged it with Docker, and deployed it to a real Kubernetes cluster using Scaleway Kapsule. You also set up a fully automated CI/CD pipeline with CircleCI and Helm, ensuring that your application is tested, built, and deployed automatically with every change. This workflow not only streamlines your development process, it gives you confidence in the reliability and scalability of your deployments.

If you’d like to explore the complete source code or use it as a reference for your own projects, you can find everything on GitHub.


Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.