Go logo

GoLand

A cross-platform Go IDE with extended support for JavaScript, TypeScript, and databases

Tutorials

Deploying Go Apps with Kubernetes

We live in a world where things change at a rapid pace, and the latest and greatest quickly becomes outdated. The same goes for deploying applications to servers. You used to have to physically travel to a data center to deploy your changes. Later on, we moved to VMs. Then containers came along and changed the game again.

Containers have been widely adopted by most industries, and one of the most popular containerization tools is Docker. However, as complexity grew, people started looking for orchestration tools that were effective at scale, performed load balancing, self-healed, and more. There were many contenders in the competition, like Apache Mesos, HashiCorp Nomad, and Docker Swarm, but Kubernetes has thrived for a long time due to its robust ecosystem, extensive community support, scalability, and ability to manage complex, distributed applications across multiple environments.

Source: drgarcia1986.medium.com

Kubernetes is an open-source container orchestration platform that automates the deployment, scaling, and management of containerized applications. Originally developed by Google, it is now maintained by the CNCF.

Kubernetes is one of the largest open-source projects to date. With over a decade of development, its maturity is undeniable, boasting more than 88,000 contributors. Check out the 10 Years of Kubernetes blog post for more insights.

In this tutorial, we are going to create a Go application and prepare it to run inside a Kubernetes cluster. 

Let’s get started!

Creating a Go application in GoLand

In this tutorial, we’ll start by creating a basic Go application which performs CRUD operations. We’ll then containerize the application and deploy it to the local Kubernetes cluster using Docker Desktop.

You can access the source code used in this tutorial here.

To create your project, launch GoLand and click New Project

Provide necessary information such as the project name, GOROOT, and environment variables.

Even if you don’t have the Go SDK installed on your system, GoLand will assist you in downloading the correct SDK.

Then click Create.

Installing packages

Gorilla Mux

Once the project has been created, install Gorilla. The Gorilla Mux package is among the most widely used routers. It offers functionalities for route matching, serving static files, supporting middleware and websockets, managing CORS requests, and testing handlers.

Installing it from GoLand is simple and straightforward. Just import the package name, and the IDE will prompt you to install it.

Alternatively, you can use the default method by accessing the Terminal and executing the following command:

go get -u github.com/gorilla/mux

GORM

GORM is an Object Relational Mapping (ORM) library for Go. It simplifies database interactions by making it easier for developers to work with database records and perform CRUD (Create, Read, Update, Delete) operations.

*NOTE: We will be using the Postgres driver.

To install, run the following command in the Terminal:

go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

Alternatively, you can also directly mention the package in the go.mod file and GoLand will take care of the installation.

NOTE: When you see // indirect next to a dependency in the require block of your go.mod file, it indicates that your project does not import this package directly in its code, but some other package that your project imports does. 

Building core business functions

Now, we have installed the core packages required to build the application. It’s time to start writing the core business logic. 

Database

Let’s begin with the database.

Create a database.go file under project root. 

Let’s break it down step-by-step. 

In this section we are managing a database client using the GORM library for Postgres.

  1. DBClient: This is an interface with two method signatures:
  • Ready(): This function returns a boolean value based on whether the database is ready or not. 
  • RunMigration(): This function performs database migrations. 
  1. Client: This is a concrete type Client that implements the DBClient interface. It contains a single db *gorm.DB field which points to a gorm.DB instance.
  1. Next, in the Ready method we perform a RAW SQL query to check database readiness. It will return a boolean response (true or false). 
  1. Under RunMigration, we first check whether the database is ready. If successful, we proceed to invoke the AutoMigrate method provided by GORM to apply migrations to the database schema. As noted in the comment, we need to register the model to run the migration. We haven’t created a model yet, but don’t worry – we’ll get to that shortly. 
  1. The NewDBClient function constructs a database connection from environment variables, creating a Client that can be used to interact with the database.

The database section is done. Now let’s create our user model. 

User model

Create a model.go file under the project root. 

Here you can see the User struct with fields ID, Name, Email, and Age, each annotated with JSON tags for serialization and GORM tags for database constraints, including primary key, uniqueness, and non-null constraints.

These tags specify database constraints and behaviors using GORM:

gorm:"primaryKey": The ID field is the primary key.

gorm:"not null": The Name and Email fields cannot be NULL in the database.

gorm:"unique": The Email must be unique across the database table.

Now we need to pass the User model to the AutoMigrate function which we discussed earlier. 

Server

We have implemented the database and the user model, so now it’s time to construct the mux server. 

Create the server.go and routes.go files under the project root.

We’ll just leave this routes.go file empty for now, and we’ll cover what to do with it in the next section when we start defining HTTP handlers.

Let’s break down the ‘server.go’ file step-by-step.

The Server interface declares two methods: 

Start() error: Starts the server and returns any errors that pop up. 

routes(): Defines the server routes.

The MuxServer struct implements the Server interface. 

It contains: 

gorilla *mux.Router: An instance of Gorilla Mux Router. 

Client: An embedded field pointing to a database client. 

NewServer is a constructor function that creates and initializes a MuxServer instance. 

  •  It accepts a Client which refers to a database client. 
  •  A new MuxServer is created with: 

– A new router from mux.NewRouter()

– The provided db client. 

– The server.routes()method is called to set up the routes. 

The Start method takes care of starting up the HTTP server and listening on port 8080. 

We haven’t defined any HTTP handlers yet, which is why the routes function is currently empty. 

Let’s take care of that now.

HTTP handlers

Create a new file called controller.go under the project root. 

Once you’ve created the file, go ahead and open model.go and add the following struct:

The UserParam struct serves as a data transfer object (DTO) specifically for input handling, often seen in web APIs or web forms. 

Separation of Concerns:

The User struct represents the data structure of a user entity in the system, which corresponds directly to the database schema. The UserParam struct is used for handling input validation and data transfer, particularly from HTTP requests.

Security:

You’ll have better control over your data by separating fields into two categories: (1) information received from requests (like user input), and (2) information stored in the database. This gives you control over what data is exposed, enhances security by filtering out sensitive info, and ensures you only transfer necessary data between layers. 

Let’s go ahead and start implementing the HTTP handlers. 

Head back into the controller.go file.

Let’s break it down step-by-step. We are going to implement the basic CRUD (Create, Read, Update, and Delete) operations on the User model. 

Add User

To create a new user and add it to the database.

List Users

To list all users from the database.

Update User

To update an existing user’s details.

Delete User

To delete an existing user.

Now, it’s time to update the routes. 

In this function, we’ll set up various kinds of routes (GET, POST, PUT and DELETE) to handle requests.

Running the application

We’re almost done! It’s time to define the entry point of the application where we can initialize the database, run migrations, and start the server.

Create a new file called main.go under the project root.

As you can see from the code below, we are initializing the database client, running database migration, and starting up the server. 

Now, it’s time to start the server. Before that, make sure you are running a local instance of Postgres. I will use Docker to spin up a postgres container.

Run the following command in the Terminal:

docker run --name goland-k8s-demo -p 5432:5432 -e POSTGRES_PASSWORD=********** -d postgres

Once the container is up and running, go ahead and modify the Run Configuration

Add these variables to the Environment field, as shown in the image below:

  • DB_HOST
  • DB_USERNAME
  • DB_PASSWORD
  • DB_NAME
  • DB_PORT

Once done, apply the changes.

Click the play icon to start the application. 

Once the application is running, navigate to http-client | apis.http.

You can play around with the REST APIs directly from the IDE itself. 

Diving into K8s

Now that we have developed the entire application,  it’s time to deploy the application inside the Kubernetes cluster. 

The process starts with creating the Dockerfile.

Dockerfile

A Dockerfile is a text document that contains a set of instructions for building a Docker image. It defines how the image should be constructed, including the base image to use, the files to include, and any commands to run during the build process.

Create a new file under project root and name it “Dockerfile”.

Simply follow the steps I’ve outlined to build the Docker image. I’ll walk you through it step by step.

FROM golang:1.23-alpine AS builder

Starts with golang:1.23-alpine as the base image and labels the stage as builder.

WORKDIR /app

Set the working directory to /app.

COPY . .

Copies the entire current directory (.) into the /app directory.

RUN CGO_ENABLED=0 GOOS=linux go build -o go_k8s

Runs the Go build command to compile the application. 

  • CGO_ENABLED=0 disables CGO (CGO enables the creation of Go packages that call C code).
  •  GOOS=linux sets the target OS to Linux. 
  • The output binary is named go_k8s.

FROM gcr.io/distroless/base

Uses a minimal distroless base image for the final container, focusing on security by excluding unnecessary components. To learn more about distroless images, check this out. 

WORKDIR /app

Sets the working directory to /app in the final stage.

COPY --from=builder /app/go_k8s .

Copies the go_k8s binary from the /app directory of the builder stage into the /app directory of the final image.

CMD ["./go_k8s"]

Sets the command to run when the container starts, which is the go_k8s binary.

The final image is kept as small and secure as possible, containing only the Go application binary without any unnecessary build tools or dependencies.

Go ahead and build the Docker image. 

Click Run ‘Dockerfile’.

  • Note: Before running, make sure the Docker daemon is running in the background. For this tutorial we are going to be using Docker Desktop.

Once the image is successfully built, push the image to the Docker registry.

Right-click the image tag and select Edit Configuration.

Provide the image tag and apply the changes.

Note:

  • Before pushing, make sure to change the image tag based on the Docker repository which you have created in DockerHub.
  • The image tag should follow the format <hub-user>/<repo-name>[:<tag>]. Follow the steps to create repositories.
  • In this example, the tag mukulmantosh/go_k8s:1.0 is for demonstration only and may change based on your account type. Here, mukulmantosh represents the user, while go_k8s is the repository name and 1.0 is the specified tag.

Make sure to re-run the build process. 

You can see that the image tag has been applied. 

It’s time to push the image. 

Right-click on the image tag, then select Push Image.

Click Add and provide your Docker registry information.

Once successfully authenticated, click OK to push the image.

Once the image is successfully pushed, you can observe the changes in DockerHub. 

Well, the image is built and pushed. Now it’s time to work on the second part – writing the Kubernetes YAML files. 

Writing K8s manifests

This part of the tutorial covers how to deploy applications to local Kubernetes clusters.

In this tutorial, we have utilized Docker Desktop, though you can also opt for Minikube or Kind.

If you’ve chosen Docker Desktop as your preferred platform for running Kubernetes, be sure to enable Kubernetes in the settings by clicking the Enable Kubernetes checkbox.

Once Kubernetes is up and running, it’s time to create a namespace.

What is a namespace?

In Kubernetes, a namespace is a logical partitioning of the cluster that allows you to divide resources and organize them into groups. Namespaces enable multiple teams or projects to share the same cluster while maintaining isolation and avoiding naming conflicts.

Source: belowthemalt.com

Begin by creating a directory called k8s in the root of your project.

Next, create a new file and name it ns.yaml.

NOTE: A Kubernetes manifest is typically written in YAML or JSON format and outlines various parameters for the resource, including its type, metadata, and specifications.

This YAML file would create a namespace named go-k8s-demo in your Kubernetes cluster.

Let’s break it down.

apiVersion: v1: This specifies the API version of the Kubernetes resource. In this case, v1 indicates that the resource is using version 1 of the Kubernetes API.

kind: Namespace: This indicates the type of Kubernetes resource being defined. It can be Deployment, Service, etc.

metadata: This section holds metadata about the Kubernetes resource. Metadata usually includes details like the name, labels, and annotations.

If you type the following command in the Terminal, it will show you lists of the API resources available in the Kubernetes cluster. 

kubectl api-resources

Okay – you’ve created the YAML file. Now it’s time to execute it. 

There are two ways you can create a namespace:

If you prefer using the Terminal, you can run this command:

kubectl create ns go-k8s-demo

Or, you can apply a file by running this command:

cd k8s
kubectl apply -f ns.yaml

Both methods will create the same namespace.

Creating a namespace with GoLand

You also have the option of doing this in GoLand. Yes, you read that right, you can play with your Kubernetes clusters directly from the GoLand IDE. 

As a side note, if you’re using GoLand 2024.2 or later, the Kubernetes plugin is already bundled with the IDE, so you don’t need to install it separately.

Open the Service tool window  by going to View | Tool Windows | Services.

Right-click on Kubernetes | Add Clusters | From Default Directory.

Select docker-desktop and click Add Clusters.

You will see docker-desktop as your newly added cluster. Click the play icon to connect to it.

Return to the YAML file and hover over the top right corner of the screen and click Apply to Cluster to set your cluster to docker-desktop.

Once done, apply the changes.

The namespace is successfully created. 

We will now switch to the newly created namespace to easily view the applications running within it.

You might be asking, “This works with a local cluster, but what about connecting to an external one?” Good news! You can do that as well.

You can also modify the paths for the kubectl and helm executables. Additionally, you have the option to customize Kubernetes configuration files at either the global or project level.

Database and K8s

The namespace has been created. Now let’s start working on the database.

PersistentVolume

We are going to create a persistent volume. A PersistentVolume (PV) in Kubernetes provides storage for your application’s pods. Think of it as a storage space that exists independently of any specific application.

Unlike regular storage that disappears when an application stops, a PersistentVolume retains the data, making it suitable for applications that need to save files or databases.

Create a new folder called db in the project root, and then add a new file named pv.yaml inside it.

This YAML configuration defines a PersistentVolume named postgres-pv with 1 GB of storage. It is associated with the postgres application and can be accessed as read-write by one node at a time. The volume is stored locally on the host at the path /data/db.

PersistentVolumeClaim

Create a new file called pvc.yaml under db.

A PersistentVolumeClaim (PVC) in Kubernetes is a request for storage by a user or application. It allows you to specify how much storage you need and what characteristics it should have, such as access modes (like read/write).

In this YAML configuration we are creating a PVC in the go-k8s-demo namespace requesting 1 GiB of storage with a ReadWriteOnce access mode using the manual storage class.

ConfigMap

Create a new file cm.yaml under db.

A ConfigMap in Kubernetes is a resource used to store configuration data in a key-value format. It allows you to separate configuration from application code, making it easier to manage and modify settings without needing to rebuild your application.

Deployment

A Deployment in Kubernetes is a resource used to manage and orchestrate the deployment of applications. It allows you to define how many instances of your application (called Pods) you want to run, and it ensures that they are running as expected.

Create a new file deploy.yaml under db.

This YAML file defines a deployment of a single PostgreSQL container running version 17.0, which exposes port 5432 and runs only one instance. It loads environment variables from a ConfigMap and uses a PersistentVolume to store data. 

Service

A Service in Kubernetes is an abstraction that defines a logical set of pods and a way to access them. It provides a stable endpoint for your applications, making it easier to communicate with groups of pods.

Source: kubernetes.io

Create a new file svc.yaml under db.

In this YAML file we have defined a Kubernetes Service named postgres-service. The Service exposes port 5432 and routes traffic to the pods labeled with app: postgres-db, so it will allow other applications within the cluster to connect to the database.

Launching DB

We now have all of the configuration files needed to start the database. Let’s execute them.

There are two methods to do this.

First, open the Terminal, navigate to the db directory, and run the following command:

cd db
kubectl apply -f .

To see the current status of your pods, you can run the following command:

kubectl get pods -n go-k8s-demo

The second option is quite easy with GoLand. You don’t need to remember the commands – just the follow along with the video below:

Application and K8s

Now that the database is up and running, it’s time to prepare our backend application.

Begin by creating an app folder inside the k8s directory.

ConfigMap

Create a new file called cm.yaml under app.

Enter the required database credentials.

NOTE:

  • Grab the credentials from db/cm.yaml that you defined earlier when creating the database pod.
  • postgres-service under DB_HOST refers to the db/svc.yaml service we created earlier.

Deployment

Now let’s move on to the deployment. 

Create a new file called deploy.yaml under app

In this YAML file we define a Kubernetes deployment that runs a single replica of a pod, which contains a single container using the mukulmantosh/go_k8s:1.0 image. The container exposes port 8080 and gets its environment variables from a ConfigMap named app-cm.

Service

Now let’s wrap up the last file. 

Create a file called svc.yaml under app.

To summarize, we set up a service named app-service that allows external traffic to reach your application running in the cluster through port 30004. Requests received here are forwarded to port 8080 on the application pods.

Testing

Now let’s deploy our application and start testing it out. 

The process is going to be exactly the same as what we did for the database.

Navigate to the app directory and run the following command:

cd app
kubectl apply -f .

Alternatively, you can do this in GoLand, which is quite easy and straightforward. 

You can also check the status of your application by running the following command:

kubectl get pods -n go-k8s-demo

Let’s test out the application by sending an HTTP request.

The application works!

This was just a brief demonstration of how to use Kubernetes with Go, but there are many more possibilities to explore.

References

If you already have a strong grasp of Kubernetes and want to learn how to deploy in a live cluster, take a look at my tutorial on deploying Go apps in Google Kubernetes Engine.

image description