Skip to main content

Deployment

This chapter will introduce code building and service deployment.

Introduction​

When feature development reaches a certain stage, we will build the code and deploy the built artifacts to run on servers. This process is usually called release or deployment.

Building​

First, let's introduce the build process.
Go's build process is relatively simple, thanks to its cross-platform capabilities. After building, the final product exists as a binary file.

Building Binary​

The simplest build command is:

go build

Usually, when compiling a project, we need to specify the output filename:

go build -o ~/path/to/artifacts/[name]

Cross-Platform Compilation​

Similarly, sometimes we need to build artifacts for different platform architectures, which requires cross-compilation.
For cross-compilation, the Go compiler provides a very simple approach - just specify environment variables.

For example, if I want to build Linux artifacts on a MacOS platform:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ~/path/to/artifacts/[name]

Variable Injection During Build​

Sometimes, we want to inject additional information into the artifacts during the build phase, such as common details like version numbers and build timestamps. In these cases, we can use the -ldflags directive to achieve this purpose.
First, let's create a file to print version information:

main.go
package main

import (
"bytes"
"fmt"
"runtime"
"strings"
"text/template"
)

var (
appName = "app"
version = "major.minor.patch"
branch = "git/branch"
revision = "git/revision"
buildDate = "yyyy-mm-dd hh:mm:ss"
goVersion = runtime.Version()
)

const versionInfoTmpl = `
{{.program}}, version: {{.version}}
(branch: {{.branch}}; revision: {{.revision}})
build date: {{.buildDate}}
go version: {{.goVersion}}
`

// PrintVersion prints version information
func PrintVersion() {
m := map[string]string{
"program": appName,
"version": version,
"branch": branch,
"revision": revision,
"buildDate": buildDate,
"goVersion": goVersion,
}

tmpl, err := template.Must(template.New("version"), nil).Parse(versionInfoTmpl)
if err != nil {
panic(err)
}

buf := bytes.Buffer{}
if err := tmpl.Execute(&buf, m); err != nil {
panic(err)
}

fmt.Println(strings.TrimSpace(buf.String()))
}

func main() {
...
PrintVersion()
...
}

Then we specify the compilation command line arguments:

PACKAGE=name
PREFIX=$(shell pwd)
COMMIT_ID=$(shell git rev-parse --short HEAD)
VERSION=$(shell git describe --tags || echo "v0.0.0")
VERSION_IMPORT_PATH=${PACKAGE}
BUILD_TIME=$(shell date '+%Y-%m-%dT%H:%M:%S%Z')
VCS_BRANCH=$(shell git symbolic-ref --short -q HEAD)

BUILD_ARGS = \
-ldflags "-X $(VERSION_IMPORT_PATH).appName=$(PACKAGE) \
-X $(VERSION_IMPORT_PATH).version=$(VERSION) \
-X $(VERSION_IMPORT_PATH).revision=$(COMMIT_ID) \
-X $(VERSION_IMPORT_PATH).branch=$(VCS_BRANCH) \
-X $(VERSION_IMPORT_PATH).buildDate=$(BUILD_TIME)"

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(BUILD_ARGS) -o ~/path/to/artifacts/[name]
tip

The above command will build a binary with injected version information. Note that for this to work properly, there are prerequisites such as having a git environment and your files being under version control.

Deployment​

Since Go builds executables - .exe files on Windows and binary executables on Linux and MacOS environments. Therefore, you can run it by double-clicking on Windows, or using ./name on Linux. In actual service environments, things can be a bit more complex, especially in cloud server environments. Here we'll briefly introduce two popular deployment methods: systemd and Docker containers.

Systemd​

Systemd is Linux's system and service manager that can be controlled using the Systemctl command. Here's a configuration example:

apiserver.service
[Unit]
Description=API Server
After=network.target network-online.target
Requires=network-online.target

[Service]
User=root
Group=root
ExecStart=/usr/bin/go-sail
ExecReload=/usr/bin/go-sail
KillMode=mixed
KillSignal=SIGQUIT
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=4096
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
Restart=always

[Install]
WantedBy=multi-user.target

Place this configuration file in the installation location, such as /usr/lib/systemd/system/. Then reload systemd and start the service:

sudo systemctl daemon-reload  

sudo systemctl enable apiserver

sudo systemctl start apiserver

sudo systemctl status apiserver

Docker Containers​

Docker containers are also a very popular service deployment solution today. Here's an example of building with a Dockerfile:

Dockerfile
FROM golang:1.20 AS builder

ARG COMMIT_ID
ARG VERSION=""
ARG VCS_BRANCH=""
ARG GRPC_STUB_REVISION=""
ARG PROJECT_NAME=vastness
ARG DOCKER_PROJECT_DIR=/build
ARG EXTRA_BUILD_ARGS=""
ARG GOCACHE=""
ARG GOSUMDB=off

WORKDIR $DOCKER_PROJECT_DIR
COPY . $DOCKER_PROJECT_DIR

ENV GOPROXY=$GOPROXY
ENV GOSUMDB=$GOSUMDB

RUN mkdir -p /output \
&& make build -e GOCACHE=$GOCACHE \
-e COMMIT_ID=$COMMIT_ID -e OUTPUT_FILE=/output/go-sail \
-e VERSION=$VERSION -e VCS_BRANCH=$VCS_BRANCH -e EXTRA_BUILD_ARGS=$EXTRA_BUILD_ARGS

FROM keepchen/alpine-with-tzdata

ENV TZ=Asia/Shanghai

COPY --from=builder /output/go-sail /usr/bin/go-sail

CMD go-sail
tip

The above Dockerfile uses multi-stage builds, which can significantly reduce the final image size.

Build the image using the docker build command:

docker build -t go-sail/go-sail:v1 .

Then, use the docker command to start the service.
You can also use docker compose to manage your service. Here's an example docker-compose.yaml file:

docker-compose.yaml
version: "3"

services:
go-sail:
image: "go-sail/go-sail:v1"
container_name: go-sail
environment:
- Key=Value
volumes:
- ./logs:/logs
restart: always
ports:
- "8080:8080"

Next, use the docker compose command to start the service:

docker compose up -d
tip

After building the image, the service will be containerized and can be deployed on Kubernetes.