From 1ea6754c7f4dac3fd472cc8ecbe99309ac2723d6 Mon Sep 17 00:00:00 2001 From: sid palas Date: Sun, 5 Feb 2023 10:16:47 -0500 Subject: [PATCH] Add healthcheck endpoints and scripts --- .../api-golang/healthcheck/healthcheck.go | 36 ++++++++++++ 05-example-web-application/api-golang/main.go | 8 ++- .../api-node/healthcheck/healthcheck.js | 21 +++++++ .../api-node/src/index.js | 6 +- .../client-react/nginx.conf | 5 ++ .../api-golang/Dockerfile.7 | 58 +++++++++++++++++++ .../api-golang/Makefile | 4 +- .../api-node/Dockerfile.8 | 39 +++++++++++++ .../api-node/Makefile | 4 +- 08-running-containers/docker-compose.yml | 2 +- .../docker-compose-dev.yml | 12 ++-- 11-deploying-containers/Makefile | 12 ++-- .../docker-compose-prod.yml | 27 ++++++++- 11-deploying-containers/docker-swarm.yml | 27 ++++++++- 14 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 05-example-web-application/api-golang/healthcheck/healthcheck.go create mode 100644 05-example-web-application/api-node/healthcheck/healthcheck.js create mode 100644 06-building-container-images/api-golang/Dockerfile.7 create mode 100644 06-building-container-images/api-node/Dockerfile.8 diff --git a/05-example-web-application/api-golang/healthcheck/healthcheck.go b/05-example-web-application/api-golang/healthcheck/healthcheck.go new file mode 100644 index 0000000..2d4ecb0 --- /dev/null +++ b/05-example-web-application/api-golang/healthcheck/healthcheck.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "time" +) + +func main() { + + port, exists := os.LookupEnv("PORT") + if !exists { + port = "8080" + } + + client := http.Client{ + Timeout: 2 * time.Second, + } + + resp, err := client.Get("http://localhost:" + port + "/ping") + if err != nil { + log.Fatal(err) + } + + // Print the HTTP Status Code and Status Name + fmt.Println("HTTP Response Status:", resp.StatusCode, http.StatusText(resp.StatusCode)) + + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + fmt.Println("HTTP Status is in the 2xx range") + } else { + fmt.Println("Argh! Broken") + os.Exit(1) + } +} diff --git a/05-example-web-application/api-golang/main.go b/05-example-web-application/api-golang/main.go index e6a5a4b..b5972d0 100644 --- a/05-example-web-application/api-golang/main.go +++ b/05-example-web-application/api-golang/main.go @@ -42,5 +42,11 @@ func main() { "now": tm, }) }) - r.Run() // listen and serve on 0.0.0.0:8080 + + r.GET("/ping", func(c *gin.Context) { + tm = database.GetTime(c) + c.JSON(200, "pong") + }) + + r.Run() // listen and serve on 0.0.0.0:8080 (or "PORT" env var) } diff --git a/05-example-web-application/api-node/healthcheck/healthcheck.js b/05-example-web-application/api-node/healthcheck/healthcheck.js new file mode 100644 index 0000000..1036768 --- /dev/null +++ b/05-example-web-application/api-node/healthcheck/healthcheck.js @@ -0,0 +1,21 @@ +var http = require('http'); + +var options = { + timeout: 2000, + host: 'localhost', + port: process.env.PORT || 3000, + path: '/ping', +}; + +var request = http.request(options, (res) => { + console.info('STATUS: ' + res.statusCode); + process.exitCode = res.statusCode === 200 ? 0 : 1; + process.exit(); +}); + +request.on('error', function (err) { + console.error('ERROR', err); + process.exit(1); +}); + +request.end(); diff --git a/05-example-web-application/api-node/src/index.js b/05-example-web-application/api-node/src/index.js index 740d52a..57e83ac 100644 --- a/05-example-web-application/api-node/src/index.js +++ b/05-example-web-application/api-node/src/index.js @@ -4,7 +4,7 @@ const express = require('express'); const morgan = require('morgan'); const app = express(); -const port = 3000; +const port = process.env.PORT || 3000; // setup the logger app.use(morgan('tiny')); @@ -16,6 +16,10 @@ app.get('/', async (req, res) => { res.send(response); }); +app.get('/ping', async (_, res) => { + res.send('pong'); +}); + const server = app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); diff --git a/05-example-web-application/client-react/nginx.conf b/05-example-web-application/client-react/nginx.conf index b1073ea..b705cd8 100644 --- a/05-example-web-application/client-react/nginx.conf +++ b/05-example-web-application/client-react/nginx.conf @@ -1,5 +1,10 @@ server { listen 80; + location /ping { + access_log off; + add_header 'Content-Type' 'text/plain'; + return 200 "pong"; + } location /api/golang/ { resolver 127.0.0.1; proxy_set_header X-Forwarded-Host $host; diff --git a/06-building-container-images/api-golang/Dockerfile.7 b/06-building-container-images/api-golang/Dockerfile.7 new file mode 100644 index 0000000..4da3482 --- /dev/null +++ b/06-building-container-images/api-golang/Dockerfile.7 @@ -0,0 +1,58 @@ +# Pin specific version for stability +# Use separate stage for building image +# Use debian for easier build utilities +FROM golang:1.19-bullseye AS build + +# Add non root user +RUN useradd -u 1001 nonroot + +WORKDIR /app + +# Copy only files required to install dependencies (better layer caching) +COPY go.mod go.sum ./ + +# Use cache mount to speed up install of existing dependencies +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + go mod download + +COPY . . + +# Compile healthcheck +RUN go build \ + -ldflags="-linkmode external -extldflags -static" \ + -tags netgo \ + -o healthcheck \ + ./healthcheck/healthcheck.go + +# Compile application during build rather than at runtime +# Add flags to statically link binary +RUN go build \ + -ldflags="-linkmode external -extldflags -static" \ + -tags netgo \ + -o api-golang + +# Use separate stage for deployable image +FROM scratch + +# Set gin mode +ENV GIN_MODE=release + +WORKDIR / + +# Copy the passwd file +COPY --from=build /etc/passwd /etc/passwd + +# Copy the healthcheck binary from the build stage +COPY --from=build /app/healthcheck/healthcheck healthcheck + +# Copy the app binary from the build stage +COPY --from=build /app/api-golang api-golang + +# Use nonroot user +USER nonroot + +# Indicate expected port +EXPOSE 8080 + +CMD ["/api-golang"] \ No newline at end of file diff --git a/06-building-container-images/api-golang/Makefile b/06-building-container-images/api-golang/Makefile index 6f3380c..745c118 100644 --- a/06-building-container-images/api-golang/Makefile +++ b/06-building-container-images/api-golang/Makefile @@ -11,7 +11,7 @@ build-N: .PHONY: build-all build-all: - for number in 0 1 2 3 4 5 6 ; do \ + for number in 0 1 2 3 4 5 6 7; do \ N=$$number $(MAKE) build-N; \ done @@ -21,6 +21,6 @@ push-N: .PHONY: push-all push-all: - for number in 0 1 2 3 4 5 6 ; do \ + for number in 0 1 2 3 4 5 6 7; do \ N=$$number $(MAKE) push-N; \ done diff --git a/06-building-container-images/api-node/Dockerfile.8 b/06-building-container-images/api-node/Dockerfile.8 new file mode 100644 index 0000000..fb7158a --- /dev/null +++ b/06-building-container-images/api-node/Dockerfile.8 @@ -0,0 +1,39 @@ +# Pin specific version for stability +# Use alpine for reduced image size +FROM node:19.4-alpine + +# Set NODE_ENV +ENV NODE_ENV production + +# Specify working directory other than / +WORKDIR /usr/src/app + +# Copy only files required to install +# dependencies (better layer caching) +COPY package*.json ./ + +# Install only production dependencies +# Use cache mount to speed up install of existing dependencies +RUN --mount=type=cache,target=/usr/src/app/.npm \ + npm set cache /usr/src/app/.npm && \ + npm ci --only=production + +# Use non-root user +# Use --chown on COPY commands to set file permissions +USER node + +# Copy the healthcheck script +COPY --chown=node:node ./healthcheck/ . + +# Copy remaining source code AFTER installing dependencies. +# Again, copy only the necessary files +COPY --chown=node:node ./src/ . + +# Indicate expected port +EXPOSE 3000 + +CMD [ "node", "index.js" ] + +# TODO: Use multi-stage with distroless image or chainguard image? +# https://github.com/GoogleContainerTools/distroless/blob/main/examples/nodejs/Dockerfile +# https://edu.chainguard.dev/chainguard/chainguard-images/reference/node/overview/ \ No newline at end of file diff --git a/06-building-container-images/api-node/Makefile b/06-building-container-images/api-node/Makefile index 75d654f..f77254a 100644 --- a/06-building-container-images/api-node/Makefile +++ b/06-building-container-images/api-node/Makefile @@ -11,7 +11,7 @@ build-N: .PHONY: build-all build-all: - for number in 0 1 2 3 4 5 6 7 ; do \ + for number in 0 1 2 3 4 5 6 7 8; do \ N=$$number $(MAKE) build-N; \ done @@ -21,6 +21,6 @@ push-N: .PHONY: push-all push-all: - for number in 0 1 2 3 4 5 6 7; do \ + for number in 0 1 2 3 4 5 6 7 8; do \ N=$$number $(MAKE) push-N; \ done \ No newline at end of file diff --git a/08-running-containers/docker-compose.yml b/08-running-containers/docker-compose.yml index 0cca43d..161aa70 100644 --- a/08-running-containers/docker-compose.yml +++ b/08-running-containers/docker-compose.yml @@ -10,7 +10,7 @@ services: api-node: build: context: ../05-example-web-application/api-node/ - dockerfile: ../../06-building-container-images/api-node/Dockerfile.7 + dockerfile: ../../06-building-container-images/api-node/Dockerfile.8 init: true depends_on: - db diff --git a/10-development-workflow/docker-compose-dev.yml b/10-development-workflow/docker-compose-dev.yml index db412d4..6fc6112 100644 --- a/10-development-workflow/docker-compose-dev.yml +++ b/10-development-workflow/docker-compose-dev.yml @@ -7,8 +7,10 @@ services: - 5173:5173 volumes: - type: bind - source: ../05-example-web-application/client-react/src - target: /usr/src/app/src + source: ../05-example-web-application/client-react/ + target: /usr/src/app/ + - type: volume + target: /usr/src/app/node_modules - type: bind source: ../08-running-containers/client-react/vite.config.js target: /usr/src/app/vite.config.js @@ -20,8 +22,10 @@ services: target: dev volumes: - type: bind - source: ../05-example-web-application/api-node/src/ - target: /usr/src/app/src/ + source: ../05-example-web-application/api-node/ + target: /usr/src/app/ + - type: volume + target: /usr/src/app/node_modules init: true depends_on: - db diff --git a/11-deploying-containers/Makefile b/11-deploying-containers/Makefile index 6bb3713..f0d59d9 100644 --- a/11-deploying-containers/Makefile +++ b/11-deploying-containers/Makefile @@ -9,19 +9,21 @@ compose-up-d: ### +CIVO_SSH:="ssh://ubuntu@212.2.244.220" + .PHONY: swarm-init swarm-init: - docker swarm init + DOCKER_HOST=${CIVO_SSH} docker swarm init .PHONY: swarm-deploy-stack swarm-deploy-stack: - docker stack deploy -c stack.yaml example-app + DOCKER_HOST=${CIVO_SSH} docker stack deploy -c docker-swarm.yml example-app .PHONY: swarm-remove-stack swarm-remove-stack: - docker stack rm example-app + DOCKER_HOST=${CIVO_SSH} docker stack rm example-app .PHONY: create-secrets create-secrets: - echo -n "foobarbaz" | docker secret create postgres-passwd - - echo -n "postgres://postgres:foobarbaz@db:5432/postgres" | docker secret create database-url - \ No newline at end of file + echo -n "foobarbaz" | DOCKER_HOST=${CIVO_SSH} docker secret create postgres-passwd - + echo -n "postgres://postgres:foobarbaz@db:5432/postgres" | DOCKER_HOST=${CIVO_SSH} docker secret create database-url - \ No newline at end of file diff --git a/11-deploying-containers/docker-compose-prod.yml b/11-deploying-containers/docker-compose-prod.yml index 08f099b..521b0cf 100644 --- a/11-deploying-containers/docker-compose-prod.yml +++ b/11-deploying-containers/docker-compose-prod.yml @@ -7,8 +7,14 @@ services: ports: - 80:80 restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/ping"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s api-node: - image: sidpalas/devops-directive-docker-course-api-node:7 + image: sidpalas/devops-directive-docker-course-api-node:8 networks: - frontend - backend @@ -18,8 +24,14 @@ services: environment: - DATABASE_URL=postgres://postgres:foobarbaz@db:5432/postgres restart: unless-stopped + healthcheck: + test: ["CMD", "node", "src/healthcheck.js"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s api-golang: - image: sidpalas/devops-directive-docker-course-api-golang:6 + image: sidpalas/devops-directive-docker-course-api-golang:7 networks: - frontend - backend @@ -29,6 +41,12 @@ services: environment: - DATABASE_URL=postgres://postgres:foobarbaz@db:5432/postgres restart: unless-stopped + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s db: image: postgres:15.1-alpine networks: @@ -37,6 +55,11 @@ services: - pgdata:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=foobarbaz + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 volumes: pgdata: diff --git a/11-deploying-containers/docker-swarm.yml b/11-deploying-containers/docker-swarm.yml index 07e6736..ce25ed5 100644 --- a/11-deploying-containers/docker-swarm.yml +++ b/11-deploying-containers/docker-swarm.yml @@ -7,8 +7,14 @@ services: - frontend ports: - 80:80 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/ping"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s api-node: - image: sidpalas/devops-directive-docker-course-api-node:7 + image: sidpalas/devops-directive-docker-course-api-node:8 environment: - DATABASE_URL_FILE=/run/secrets/database-url secrets: @@ -18,8 +24,14 @@ services: - backend ports: - 3000:3000 + healthcheck: + test: ["CMD", "node", "/usr/src/app/healthcheck.js"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s api-golang: - image: sidpalas/devops-directive-docker-course-api-golang:6 + image: sidpalas/devops-directive-docker-course-api-golang:7 networks: - frontend - backend @@ -29,6 +41,12 @@ services: - database-url ports: - 8080:8080 + healthcheck: + test: ["CMD", "/healthcheck"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s db: image: postgres:15.1-alpine networks: @@ -41,6 +59,11 @@ services: - POSTGRES_PASSWORD_FILE=/run/secrets/postgres-passwd secrets: - postgres-passwd + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 volumes: pgdata: