在Kubernetes上用Redis部署一个无状态的Go应用的详细步骤和完整代码

251 阅读2分钟

在Kubernetes上用Redis部署一个无状态的Go应用

Rajeev SinghDevopsAugust07, 20212mins read

Deploying a stateless Go app with Redis on Kubernetes 在这篇文章中,我们将更进一步,在Kubernetes上部署一个带有Redis的无状态Go web应用。你会了解到多个不同Pod的部署是如何进行的,以及两个Pod如何在集群中相互通信。

建立一个使用Redis的Go应用样本

我们将在Go中创建一个简单的Web应用程序,其中包含一个显示 "每日报价 "的API。

该应用程序从托管在http://quotes.rest/ 的公共 API 中获取当天的报价,然后将结果缓存在 Redis 中,直到一天结束。在随后的API调用中,该应用程序将从Redis缓存中返回结果,而不是从公共API中获取。

打开你的终端,输入以下命令来创建项目并初始化Go模块

$ mkdir go-redis-kubernetes
$ cd go-redis-kubernetes
$ go mod init github.com/callicoder/go-redis-kubernetes 
# Change `callicoder` to your Github username

接下来,用以下代码创建一个名为main.go 的文件。

package main

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/go-redis/redis"
	"github.com/gorilla/mux"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Welcome! Please hit the `/qod` API to get the quote of the day."))
}

func quoteOfTheDayHandler(client *redis.Client) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		currentTime := time.Now()
		date := currentTime.Format("2006-01-02")

		val, err := client.Get(date).Result()
		if err == redis.Nil {
			log.Println("Cache miss for date ", date)
			quoteResp, err := getQuoteFromAPI()
			if err != nil {
				w.Write([]byte("Sorry! We could not get the Quote of the Day. Please try again."))
				return
			}
			quote := quoteResp.Contents.Quotes[0].Quote
			client.Set(date, quote, 24*time.Hour)
			w.Write([]byte(quote))
		} else {
			log.Println("Cache Hit for date ", date)
			w.Write([]byte(val))
		}
	}
}

func main() {
	// Create Redis Client
	var (
		host     = getEnv("REDIS_HOST", "localhost")
		port     = string(getEnv("REDIS_PORT", "6379"))
		password = getEnv("REDIS_PASSWORD", "")
	)

	client := redis.NewClient(&redis.Options{
		Addr:     host + ":" + port,
		Password: password,
		DB:       0,
	})

	_, err := client.Ping().Result()
	if err != nil {
		log.Fatal(err)
	}

	// Create Server and Route Handlers
	r := mux.NewRouter()

	r.HandleFunc("/", indexHandler)
	r.HandleFunc("/qod", quoteOfTheDayHandler(client))

	srv := &http.Server{
		Handler:      r,
		Addr:         ":8080",
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	// Start Server
	go func() {
		log.Println("Starting Server")
		if err := srv.ListenAndServe(); err != nil {
			log.Fatal(err)
		}
	}()

	// Graceful Shutdown
	waitForShutdown(srv)
}

func waitForShutdown(srv *http.Server) {
	interruptChan := make(chan os.Signal, 1)
	signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

	// Block until we receive our signal.
	<-interruptChan

	// Create a deadline to wait for.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	srv.Shutdown(ctx)

	log.Println("Shutting down")
	os.Exit(0)
}

func getQuoteFromAPI() (*QuoteResponse, error) {
	API_URL := "http://quotes.rest/qod.json"
	resp, err := http.Get(API_URL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	log.Println("Quote API Returned: ", resp.StatusCode, http.StatusText(resp.StatusCode))

	if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
		quoteResp := &QuoteResponse{}
		json.NewDecoder(resp.Body).Decode(quoteResp)
		return quoteResp, nil
	} else {
		return nil, errors.New("Could not get quote from API")
	}

}

func getEnv(key, defaultValue string) string {
	value := os.Getenv(key)
	if value == "" {
		return defaultValue
	}
	return value
}

同时,在一个名为quote.go 的文件中创建以下结构,以解析从http://quotes.rest/ API返回的JSON响应。

package main

type QuoteData struct {
	Id         string   `json:"id"`
	Quote      string   `json:"quote"`
	Length     string   `json:"length"`
	Author     string   `json:"author"`
	Tags       []string `json:"tags"`
	Category   string   `json:"category"`
	Date       string   `json:"date"`
	Permalink  string   `json:"parmalink"`
	Title      string   `json:"title"`
	Background string   `json:"Background"`
}

type QuoteResponse struct {
	Success  APISuccess   `json:"success"`
	Contents QuoteContent `json:"contents"`
}

type QuoteContent struct {
	Quotes    []QuoteData `json:"quotes"`
	Copyright string      `json:"copyright"`
}

type APISuccess struct {
	Total string `json:"total"`
}

现在让我们在本地构建并运行该应用程序。

$ go build
$ ./go-redis-kubernetes
2019/07/28 13:32:05 Starting Server
$ curl localhost:8080
Welcome! Please hit the `/qod` API to get the quote of the day.

$ curl localhost:8080/qod
I’ve missed more than 9000 shots in my career. I’ve lost almost 300 games. 26 times, I’ve been trusted to take the game winning shot and missed. I’ve failed over and over and over again in my life. And that is why I succeed.

容器化的Go应用

现在让我们通过创建一个具有以下配置的Docker文件,将我们的Go应用容器化。

# Dockerfile References: https://docs.docker.com/engine/reference/builder/

# Start from the latest golang base image
FROM golang:latest as builder

# Add Maintainer Info
LABEL maintainer="Rajeev Singh <rajeevhub@gmail.com>"

# Set the Current Working Directory inside the container
WORKDIR /app

# Copy go mod and sum files
COPY go.mod go.sum ./

# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
RUN go mod download

# Copy the source from the current directory to the Working Directory inside the container
COPY . .

# Build the Go app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .


######## Start a new stage from scratch #######
FROM alpine:latest  

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy the Pre-built binary file from the previous stage
COPY --from=builder /app/main .

# Expose port 8080 to the outside world
EXPOSE 8080

# Command to run the executable
CMD ["./main"] 

我已经在docker hub上为我们的应用构建并发布了docker镜像。你可以使用下面的命令来完成。

# Build the image
$ docker build -t go-redis-kubernetes .

# Tag the image
$ docker tag go-redis-kubernetes callicoder/go-redis-app:1.0.0

# Login to docker with your docker Id
$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don\'t have a Docker ID, head over to https://hub.docker.com to create one.
Username (callicoder): callicoder
Password:
Login Succeeded

# Push the image to docker hub
$ docker push callicoder/go-redis-app:1.0.0

为Redis创建Kubernetes部署和服务清单

现在让我们创建配置,在Kubernetes上部署我们的Redis应用。我们需要创建一个用于管理Redis实例的部署,以及一个用于将流量从我们的Go应用代理到Redis Pod的服务。

在项目的根目录下创建一个名为deployments 的文件夹,以存储所有的部署清单。然后,创建一个名为redis-master.yml 的文件,配置如下。

type: post
---
apiVersion: apps/v1  # API version
kind: Deployment
metadata:
  name: redis-master # Unique name for the deployment
  labels:
    app: redis       # Labels to be applied to this deployment
spec:
  selector:
    matchLabels:     # This deployment applies to the Pods matching these labels
      app: redis
      role: master
      tier: backend
  replicas: 1        # Run a single pod in the deployment
  template:          # Template for the pods that will be created by this deployment
    metadata:
      labels:        # Labels to be applied to the Pods in this deployment
        app: redis
        role: master
        tier: backend
    spec:            # Spec for the container which will be run inside the Pod.
      containers:
      - name: master
        image: redis
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 6379
type: post
---        
apiVersion: v1
kind: Service        # Type of Kubernetes resource
metadata:
  name: redis-master # Name of the Kubernetes resource
  labels:            # Labels that will be applied to this resource
    app: redis
    role: master
    tier: backend
spec:
  ports:
  - port: 6379       # Map incoming connections on port 6379 to the target port 6379 of the Pod
    targetPort: 6379
  selector:          # Map any Pod with the specified labels to this service
    app: redis
    role: master
    tier: backend

redis-master 服务只能在容器集群内访问,因为服务的默认类型是 ClusterIP。ClusterIP为服务所指向的Pod集合提供了一个单一的IP地址。这个IP地址只能在集群内访问。

Go应用程序的Kubernetes部署清单

现在让我们为Go应用程序创建一个部署和一个服务。我们将为Go应用运行3个Pod,这些Pod将通过一个服务暴露给外部世界。

type: post
---
apiVersion: apps/v1
kind: Deployment                 # Type of Kubernetes resource
metadata:
  name: go-redis-app             # Unique name of the Kubernetes resource
spec:
  replicas: 3                    # Number of pods to run at any given time
  selector:
    matchLabels:
      app: go-redis-app          # This deployment applies to any Pods matching the specified label
  template:                      # This deployment will create a set of pods using the configurations in this template
    metadata:
      labels:                    # The labels that will be applied to all of the pods in this deployment
        app: go-redis-app 
    spec:
      containers:
      - name: go-redis-app
        image: callicoder/go-redis-app:1.0.0 
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
          - containerPort: 8080  # Should match the port number that the Go application listens on    
        env:                     # Environment variables passed to the container
          - name: REDIS_HOST
            value: redis-master
          - name: REDIS_PORT
            value: "6379"    
type: post
---
apiVersion: v1
kind: Service                    # Type of kubernetes resource
metadata:
  name: go-redis-app-service     # Unique name of the resource
spec:
  type: NodePort                 # Expose the Pods by opening a port on each Node and proxying it to the service.
  ports:                         # Take incoming HTTP requests on port 9090 and forward them to the targetPort of 8080
  - name: http
    port: 9090
    targetPort: 8080
  selector:
    app: go-redis-app            # Map any pod with label `app=go-redis-app` to this service

Golang应用可以使用主机名redis-master ,与Redis进行通信。这将由Kubernetes自动解析为指向服务的IP地址redis-master

在Kubernetes上部署Go应用和Redis

我们将在使用Minikube创建的本地Kubernetes集群上部署Go web应用和Redis。

如果你还没有安装Minikube和Kubectl,请安装它们。请查看Kubernetes官方文档的说明。

使用minikube启动一个Kubernetes集群

$ minikube start

部署Redis

$ kubectl apply -f deployments/redis-master.yml
deployment.apps/redis-master created
service/redis-master created
$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
redis-master-7b44998456-pl8h9   1/1     Running   0          34s

部署Go应用程序

$ kubectl apply -f deployments/go-redis-app.yml
deployment.apps/go-redis-app created
service/go-redis-app-service created
$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
go-redis-app-57b7d4d4cd-fkddw   1/1     Running   0          27s
go-redis-app-57b7d4d4cd-l9wg9   1/1     Running   0          27s
go-redis-app-57b7d4d4cd-m9t8b   1/1     Running   0          27s
redis-master-7b44998456-pl8h9   1/1     Running   0          82s

访问应用程序

Go应用程序通过服务以NodePort的形式公开。你可以使用minikube获得服务的URL,就像这样。

$ minikube service go-redis-app-service --url
http://192.168.99.100:30435

你可以使用上述端点来访问该应用程序。

$ curl http://192.168.99.100:30435
Welcome! Please hit the `/qod` API to get the quote of the day.

$  curl http://192.168.99.100:30435/qod
I’ve missed more than 9000 shots in my career. I’ve lost almost 300 games. 26 times, I’ve been trusted to take the game winning shot and missed. I’ve failed over and over and over again in my life. And that is why I succeed.

结论

在这篇文章中,你学到了如何在使用Minikube创建的本地Kubernetes集群上部署一个带有Redis的无状态Go Web应用。

我希望这篇实践文章对你有用。谢谢你的阅读。