Last active
December 11, 2020 08:33
-
-
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]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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