If you are not familiar with Makefile, I recommend reading Ruanyifeng’s article “Make Command Tutorial[1]“. This article will guide you to write a more elegant Makefile through a refactoring example, so let’s get started!
Assuming there is a project named foo, developed in Golang and deployed on Docker, its Makefile is as follows:
APP = $(shell basename ${CURDIR})
TAG = $(shell git log --pretty=format:"%cd.%h" --date=short -1)
.PHONY: build
build:
go build -ldflags "-X 'main.version=${TAG}'" -o ./tmp/${APP} .
.PHONY: docker-config
docker-config: env
TAG=${TAG} docker-compose config
.PHONY: docker-build
docker-build: env
TAG=${TAG} docker-compose build
.PHONY: docker-push
docker-push: env
TAG=${TAG} docker-compose push
.PHONY: docker-up
docker-up: env
TAG=${TAG} docker-compose up
.PHONY: docker-down
docker-down:
TAG=${TAG} docker-compose down
It looks quite clean, the only thing to note is that when operating docker-compose, an environment variable named TAG is passed, indicating the current tag of the project. Let’s take a look at the corresponding docker-compose.yml file:
version: "3.0"
services:
server:
image: docker.domain.com/foo:${TAG}
build:
context: .
dockerfile: build/docker/Dockerfile
ports:
- "9090:9090"
- "6060:6060"
At this point, there is an area for improvement: the ports information is duplicated. Let’s look at the corresponding config.toml file:
[rpc]
port = 9090
[debug]
port = 6060
The rpc port 9090 and debug port 6060 were initially configured in the config.toml file, but were repeated again in the docker-compose.yml file. If a change is needed, it would require modifying multiple places.
At this point, a common solution that comes to mind is to pass the port information through environment variables, just like the TAG variable. To obtain the port information, I even wrote a sub-command config:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func NewConfigCmd() *cobra.Command {
configCmd := &cobra.Command{
Use: "config ",
Run: config,
}
return configCmd
}
func config(cmd *cobra.Command, args []string) {
if len(args) != 1 {
_ = cmd.Usage()
os.Exit(1)
}
key := args[0]
value := viper.Get(key)
fmt.Println(value)
}
Having determined the solution, let’s look at the corresponding docker-compose.yml file:
version: "3.0"
services:
server:
image: docker.domain.com/${APP}:${TAG}
build:
context: .
dockerfile: build/docker/Dockerfile
ports:
- "${RPC_PORT}:${RPC_PORT}"
- "${DEBUG_PORT}:${DEBUG_PORT}"
At this point, there is no hardcoded configuration information anymore. Now, let’s take another look at the corresponding Makefile:
APP = $(shell basename ${CURDIR})
TAG = $(shell git log --pretty=format:"%cd.%h" --date=short -1)
RPC_PORT = $(shell ./tmp/${APP} config rpc.port)
DEBUG_PORT = $(shell ./tmp/${APP} config debug.port)
.PHONY: build
build:
go build -ldflags "-X 'main.version=${TAG}'" -o ./tmp/${APP} .
.PHONY: docker-config
docker-config: env
APP=${APP} TAG=${TAG} RPC_PORT={RPC_PORT} DEBUG_PORT={DEBUG_PORT} docker-compose config
.PHONY: docker-build
docker-build: env
APP=${APP} TAG=${TAG} RPC_PORT={RPC_PORT} DEBUG_PORT={DEBUG_PORT} docker-compose build
.PHONY: docker-push
docker-push: env
APP=${APP} TAG=${TAG} RPC_PORT={RPC_PORT} DEBUG_PORT={DEBUG_PORT} docker-compose push
.PHONY: docker-up
docker-up: env
APP=${APP} TAG=${TAG} RPC_PORT={RPC_PORT} DEBUG_PORT={DEBUG_PORT} docker-compose up
.PHONY: docker-down
docker-down:
APP=${APP} TAG=${TAG} RPC_PORT={RPC_PORT} DEBUG_PORT={DEBUG_PORT} docker-compose down
I must say, the long environment variables are quite ugly. Fortunately, docker-compose supports .env files[2], so we can write the environment variables into a .env file and let the docker-compose command read data from it:
APP = $(shell basename ${CURDIR})
TAG = $(shell git log --pretty=format:"%cd.%h" --date=short -1)
RPC_PORT = $(shell ./tmp/${APP} config rpc.port)
DEBUG_PORT = $(shell ./tmp/${APP} config debug.port)
.PHONY: env
env:
echo "APP=${APP}" > .env; \
echo "TAG=${TAG}" >> .env; \
echo "RPC_PORT=${RPC_PORT}" >> .env; \
echo "DEBUG_PORT=${DEBUG_PORT}" >> .env
.PHONY: build
build:
go build -ldflags "-X 'main.version=${TAG}'" -o ./tmp/${APP} .
.PHONY: docker-config
docker-config: env
docker-compose config
.PHONY: docker-build
docker-build: env
docker-compose build
.PHONY: docker-push
docker-push: env
docker-compose push
.PHONY: docker-up
docker-up: env
docker-compose up
.PHONY: docker-down
docker-down:
docker-compose down
In the Makefile, we defined an env operation and made it a prerequisite for all docker-compose operations, finally eliminating the need to write long environment variables. However, remember to add .env to .gitignore!
References
Make Command Tutorial: https://www.ruanyifeng.com/blog/2015/02/make.html
[2]docker-compose supports .env files: https://docs.docker.com/compose/environment-variables/
Recommended Reading
-
Go Memory Leak Debugging Practice