Skip to content

Instantly share code, notes, and snippets.

@mtilson
Last active December 11, 2020 08:33
Show Gist options
  • Save mtilson/c5b799a2e54f30137718ad13852ef17e to your computer and use it in GitHub Desktop.
Save mtilson/c5b799a2e54f30137718ad13852ef17e to your computer and use it in GitHub Desktop.
how to create CI/CD pipeline on localhost with 'make' and 'docker' [golang] [makefile] [dockerfile]
# MI (modules image) is for getting dependencies.
# MI will be cached till the dependency changing.
# It is useful trick to speed up the process of building
FROM golang:1.13 as MI
ADD go.mod go.sum /m/
RUN cd /m && go mod download
# BI (build image) is for building App
FROM golang:1.13 as BI
# The `ARG's` are passed with `--build-arg` from `Makefile`
ARG APP
# We only need `pkg` from MI to build App
COPY --from=MI /go/pkg /go/pkg
# Avoid running containers under the `root`
RUN useradd -u 12345 $APP
# Copy our workspace (this git repo dir)
RUN mkdir -p /workspace
ADD . /workspace
WORKDIR /workspace
# Build App for appropriate OS and ARCH
RUN APP=$APP GOARCH=amd64 GOOS=linux make build
# Finale image starts with `scratch`
FROM scratch
# These `ARG's` are passed with `--build-arg` from `Makefile`
ARG APP
ARG PORT
ARG PORT_DIAG
# These are what we only need from BI
COPY --from=BI /etc/passwd /etc/passwd
COPY --from=BI /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=BI /workspace/bin/$APP /service
# `make build` from BI put App with filename `$APP`
# in `/workspace/bin/` - see Makefile's `build` rule.
# We move it to `/` and name it as `service`, to be
# able to use this path in `CMD`
# The same user as was created in BI
USER $APP
# Ports are passed from `Makefile`
EXPOSE $PORT
EXPOSE $PORT_DIAG
CMD ["/service"]
// This is the only package in the only go source file in the project.
// We just start 2 `http listrers` as goroutines, providing
// simple handlers for each one, and block in `main` goroutine waiting
// for either OS interruption signal or any errors from the above listners.
// http.Server.Shutdown() with context.Timeout are used to gracefully
// shutdown servers on unblocking `main` goroutine.
// Full details is available on:
// https://gist.github.com/mtilson/4acb20bcc48faf3cb7665a974187a38d
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/mux"
)
var (
version string = "UNKNOWN" // by default, can be redefined with `-ldflags`
portServ string = "8080" // by default, can be redefined with `-ldflags`
portDiag string = "8081" // by default, can be redefined with `-ldflags`
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request '%v' received", r.URL.RequestURI())
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Hello world.")
})
server := http.Server{
Addr: net.JoinHostPort("", portServ),
Handler: router,
}
routerDiag := mux.NewRouter()
routerDiag.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request '%v' received", r.URL.RequestURI())
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Version: %s", version)
})
serverDiag := http.Server{
Addr: net.JoinHostPort("", portDiag),
Handler: routerDiag,
}
shutdownChan := make(chan error, 2)
go func() {
log.Print("Starting Server...")
err := server.ListenAndServe()
if err != http.ErrServerClosed {
shutdownChan <- err
}
}()
go func() {
log.Print("Starting Diagnostics Server...")
err := serverDiag.ListenAndServe()
if err != http.ErrServerClosed {
shutdownChan <- err
}
}()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
select {
case sig := <-signalChan:
log.Printf("Signal '%v' received from signal channel", sig)
case err := <-shutdownChan:
log.Printf("Error '%v' received from shutdown channel", err)
}
ctxTimeout, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFunc()
err := serverDiag.Shutdown(ctxTimeout)
if err != nil {
log.Print(err)
}
err = server.Shutdown(ctxTimeout)
if err != nil {
log.Print(err)
}
time.Sleep(1 * time.Nanosecond)
}
# App name
APP?=simple-server
# Defauld OS/ARCH for MacOS. When `make build` runs from `Dockerfile`
# these variables (and `APP`) are set up correspondingly, like for example:
# `RUN APP=$APP GOARCH=amd64 GOOS=linux make build`
GOARCH?=amd64
GOOS?=darwin
# Main and diagnostics ports, defined here and passed to all other places
PORT?=8080
PORT_DIAG?=8081
# `PROJECT` is not used here, but can be helpful in case we need full import paths
PROJECT?=github.com/mtilson/go-containers-sample
# `RELEASE` is based on `git tag`
RELEASE?=${shell git rev-parse --git-dir >/dev/null 2>&1 && git describe --tags || echo unset}
# It seems like we don't need `CGO_ENABLED=0` with Go 1.10 and later ?
# The same `build` rule for local and container builds.
.PHONY: build
build:
CGO_ENABLED=0 GOARCH=${GOARCH} GOOS=${GOOS} go build -ldflags "-s -w \
-X main.version=${RELEASE} \
-X main.portServ=${PORT} \
-X main.portDiag=${PORT_DIAG}" \
-o bin/$(APP) ./...
.PHONY: rm-prod-image
rm-prod-image:
@test -z "$$(docker image ls -q $(APP):$(RELEASE))" || docker image rm $(APP):$(RELEASE) > /dev/null
@test -z "$$(docker image ls -q $(APP):$(RELEASE))" || echo "Error: Removing production image failed"
.PHONY: rm-test-image
rm-test-image:
@test -z "$$(docker image ls -q $(APP):$(RELEASE)-test)" || docker image rm $(APP):$(RELEASE)-test > /dev/null
@test -z "$$(docker image ls -q $(APP):$(RELEASE)-test)" || echo "Error: Removing test image failed"
# We use separate file (`testenv.Dockerfile`) for tests
.PHONY: test
test: rm-test-image
docker build --pull -t $(APP):$(RELEASE)-test -f testenv.Dockerfile .
# We use `Dockerfile` (in repo's `root`) for finale image.
# `--build-arg` passes values for `docker builder's ARG's`
.PHONY: image
image: rm-prod-image
docker build --pull -t $(APP):$(RELEASE) \
--build-arg APP=$(APP) \
--build-arg PORT=$(PORT) \
--build-arg PORT_DIAG=$(PORT_DIAG) .
.PHONY: stop
stop:
@test -z "$$(docker container ls -q -f name=${APP})" || { docker container stop ${APP} && docker container rm ${APP} ; }
@test -z "$$(docker container ls -q -f name=${APP})" || echo "Error: Removing prod container failed"
# Run container from the built image.
# `run` depends on `test` - it is full CI/CD pipeline
.PHONY: run
run: test stop image
docker run --name ${APP} \
-e "PORT=${PORT}" \
-e "PORT_DIAG=${PORT_DIAG}" \
-p ${PORT}:${PORT} \
-p ${PORT_DIAG}:${PORT_DIAG} \
-d $(APP):$(RELEASE)
.PHONY: clean
clean: stop rm-prod-image rm-test-image
@docker system prune -f > /dev/null
@rm -fr bin/$(APP)
@test ! -e "bin/$(APP)" || echo "Error: Removing local app binary failed"
.DEFAULT_GOAL := run
FROM golang:1.13
# Install `golangci` linter
ENV GOLANGCI v1.21.0
RUN curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI}
# Copy our workspace (this git repo dir)
RUN mkdir -p /workspace
ADD . /workspace
WORKDIR /workspace
# Run `golangci` linter
RUN golangci-lint --version
RUN golangci-lint run -v --deadline=600s --out-format=tab --disable-all --tests=false \
--enable=unconvert --enable=megacheck --enable=structcheck --enable=gas \
--enable=gocyclo --enable=dupl --enable=misspell --enable=unparam --enable=varcheck \
--enable=deadcode --enable=typecheck --enable=ineffassign --enable=varcheck ./...
# Run tests
RUN go test --race -timeout=60s -v ./...
# Check if the Code is buildable
RUN GOARCH=amd64 GOOS=linux make build
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment