歡迎光臨
每天分享高質量文章

從零開始寫一個執行在Kubernetes上的服務程式

這是一篇對於Go語言和Kubernetes新手來說再適合不過的文章了。文中詳細介紹了從程式碼編寫到用容器的方式在Kubernetes叢集中釋出,一步一步,一行一行,有例子有說明,解釋透徹,貫穿始末,值得每一個容器愛好者和Go語言程式員去閱讀和學習。

也許你已經嘗試過了Go語言,也許你已經知道了可以很容易的用Go語言去寫一個服務程式。沒錯!我們僅僅需要幾行程式碼[1]就可以用Go語言寫出一個http的服務程式。但是如果我們想把它放到生產環境裡,我們還需要準備些什麼呢?讓我用一個準備放在Kubernetes上的服務程式來舉例說明一下。

你可以從這裡[2]找到這篇章中使用的,跟隨我們一步一步[3]地進行。

第1步 最簡單的http服務程式

下麵就是這個程式:

main.go

package main
import (
   "fmt"
   "net/http"
)

func main()
{
   http.HandleFunc("/home", func(w http.ResponseWriter, _ *http.Request) {
       fmt.Fprint(w, "Hello! Your request was processed.")
   },
   )
   http.ListenAndServe(":8000", nil)
}

如果是第一次執行,僅僅執行go run main.go就可以了。如果你想知道它是怎麼工作的,你可以用下麵這個命令:curl -i http://127.0.0.1:8000/home。但是當我們執行這個應用的時候,我們找不到任何關於狀態的資訊。

第2步 增加日誌

首先,增加日誌功能可以幫助我們瞭解程式現在處於一個什麼樣的狀態,並記錄錯誤(譯者註:如果有錯誤的話)等其他一些重要資訊。在這個例子裡我們使用Go語言標準庫裡最簡單的日誌模組,但是如果是跑在Kubernetes上的服務程式,你可能還需要一些額外的庫,比如glog[4]或者logrus[5]。

比如,如果我們想記錄3種情況:當程式啟動的時候,當程式啟動完成,可以對外提供服務的時候,當http.listenAndServe 傳回出錯的時候。所以我們程式如下:

main.go

func main() {
   log.Print("Starting the service...")
   http.HandleFunc("/home", func(w http.ResponseWriter, _ *http.Request) {
       fmt.Fprint(w, "Hello! Your request was processed.")
   },
   )
   log.Print("The service is ready to listen and serve.")
   log.Fatal(http.ListenAndServe(":8000", nil))
}

第3步 增加一個路由

現在,如果我們寫一個真正實用的程式,我們也許需要增加一個路由,根據規則去響應不同的URL和處理HTTP的方法。在Go語言的標準庫中沒有路由,所以我們需要取用gorilla/mux[6],它們相容Go語言的標準庫net/http。

如果你的服務程式需要處理大量的不同路由規則,你可以把所有相關的路由放在各自的函式中,甚至是package裡。現在我們就在例子中,把路由的初始化和規則放到handlers package裡(點這裡[7]有所有的更改)。

現在我們增加一個Router函式,它傳回一個配置好的路由和能夠處理/home 的home函式。就我個人習慣,我把它們分成兩個檔案:

handler/handers.go

package handlers
import (
   "github.com/gorilla/mux"
)

// Router register necessary routes and returns an instance of a router.
func Router() *mux.Router
{
   r := mux.NewRouter()
   r.HandleFunc("/home", home).Methods("GET")
   return r
}

handlers/home.go

package handlers
import (
   "fmt"
   "net/http"
)

// home is a simple HTTP handler function which writes a response.
func home(w http.ResponseWriter, _ *http.Request)
{
   fmt.Fprint(w, "Hello! Your request was processed.")
}

然後我們稍微修改一下main.go


package main
import (
   "log"
   "net/http"
   "github.com/rumyantseva/advent-2017/handlers"
)

// How to try it: go run main.go
func main()
{
   log.Print("Starting the service...")
   router := handlers.Router()
   log.Print("The service is ready to listen and serve.")
   log.Fatal(http.ListenAndServe(":8000", router))
}

第四步 測試

現在是時候增加一些測試了。我選擇httptest ,對於Router函式,我們需要增加如下修改:

handlers/handles_test.go

package handlers
import (
   "net/http"
   "net/http/httptest"
   "testing"
)
func TestRouter(t *testing.T) {
   r := Router()
   ts := httptest.NewServer(r)
   defer ts.Close()
   res, err := http.Get(ts.URL + "/home")
   if err != nil {
       t.Fatal(err)
   }
   if res.StatusCode != http.StatusOK {
       t.Errorf("Status code for /home is wrong. Have: %d, want: %d.", res.StatusCode, http.StatusOK)
   }
   res, err = http.Post(ts.URL+"/home", "text/plain", nil)
   if err != nil {
       t.Fatal(err)
   }
   if res.StatusCode != http.StatusMethodNotAllowed {
       t.Errorf("Status code for /home is wrong. Have: %d, want: %d.", res.StatusCode, http.StatusMethodNotAllowed)
   }
   res, err = http.Get(ts.URL + "/not-exists")
   if err != nil {
       t.Fatal(err)
   }
   if res.StatusCode != http.StatusNotFound {
       t.Errorf("Status code for /home is wrong. Have: %d, want: %d.", res.StatusCode, http.StatusNotFound)
   }
}

在這裡我們會監測如果GET方法傳回200。另一方面,如果我們發出POST,我們期待傳回405。最後,增加一個如果訪問錯誤的404。實際上,這個例子有有一點“冗餘”了,因為路由作為 gorilla/mux的一部分已經處理好了,所以其實你不需要處理這麼多情況。

對於home合理的檢查一下響應碼和傳回值:

handlers/home_test.go

package handlers
import (
   "io/ioutil"
   "net/http"
   "net/http/httptest"
   "testing"
)
func TestHome(t *testing.T) {
   w := httptest.NewRecorder()
   home(w, nil)
   resp := w.Result()
   if have, want := resp.StatusCode, http.StatusOK; have != want {
       t.Errorf("Status code is wrong. Have: %d, want: %d.", have, want)
   }
   greeting, err := ioutil.ReadAll(resp.Body)
   resp.Body.Close()
   if err != nil {
       t.Fatal(err)
   }
   if have, want := string(greeting), "Hello! Your request was processed."; have != want {
       t.Errorf("The greeting is wrong. Have: %s, want: %s.", have, want)
   }
}

現在我們執行go tests來檢查程式碼的正確性:

$ go test -v ./...
?       github.com/rumyantseva/advent-2017      [no test files]
=== RUN   TestRouter
--- PASS: TestRouter (0.00s)
=== RUN   TestHome
--- PASS: TestHome (0.00s)
PASS
ok      github.com/rumyantseva/advent-2017/handlers     0.018s

第5步 配置

下一個問題就是如何去配置我們的服務程式。因為現在它只能監聽8000埠,如果能配置這個埠,我們的服務程式會更有價值。Twelve-Factor App manifesto,為服務程式提供了一個很好的方法,讓我們用環境變數去儲存配置資訊。所以我們做瞭如下修改:

main.go

package main
import (
   "log"
   "net/http"
   "os"
   "github.com/rumyantseva/advent-2017/handlers"
)

// How to try it: PORT=8000 go run main.go
func main()
{
   log.Print("Starting the service...")
   port := os.Getenv("PORT")
   if port == "" {
       log.Fatal("Port is not set.")
   }
   r := handlers.Router()
   log.Print("The service is ready to listen and serve.")
   log.Fatal(http.ListenAndServe(":"+port, r))
}

在這個例子裡,如果沒有設定埠,應用程式會退出並傳回一個錯誤。因為如果配置錯誤了,就沒有必要再繼續執行了。

第6步 Makefile

幾天以前有一篇文章[8]介紹make工具,如果你有一些重覆性比較強的工作,那麼使用它就大有幫助。現在我們來看一看我們的應用程式如何使用它。當前,我們有兩個操作,測試和編譯並執行。我們對Makefile檔案進行瞭如下修改[9]。但是我們用go build代替了go run,並且執行那個編譯出來的二進製程式,因為這樣修改更適合為我們的生產環境做準備:

Makefile

APP?=advent
PORT?=8000
clean:
   rm -f ${APP}
build: clean
   go build -o ${APP}
run: build
   PORT=${PORT} ./${APP}
test:
   go test -v -race ./...

這個例子裡,為了省去重覆性操作,我們把程式命名為變數app的值。

這裡,為了執行應用程式,我們需要刪除掉舊的程式(如果它存在的話),編譯程式碼並用環境變數代表的引數執行新編譯出的程式,做這些操作,我們僅僅需要執行make run。

第7步 版本控制

下一步,我們將為我們的程式加入版本控制。因為有的時候,它對我們知道正在生產環境中執行和編譯的程式碼非常有幫助。(譯者註:就是說,我們在生產環境中執行的程式碼,有的時候我們自己都不知道對這個程式碼進行和什麼樣的提交和修改,有了版本控制,就可以顯示出這個版本的變化和歷史記錄)。

為了儲存這些資訊,我們增加一個新的package -version:

version/version.go

package version
var (
   // BuildTime is a time label of the moment when the binary was built
   BuildTime = "unset"
   // Commit is a last commit hash at the moment when the binary was built
   Commit = "unset"
   // Release is a semantic version of current build
   Release = "unset"
)

我們可以在程式啟動時,用日誌記錄這些版本資訊:

main.go

...
func main() {
   log.Printf(
       "Starting the service...\ncommit: %s, build time: %s, release: %s",
       version.Commit, version.BuildTime, version.Release,
   )
...
}

現在我們給home和test也增加上版本控制資訊:

handlers/home.go

package handlers
import (
   "encoding/json"
   "log"
   "net/http"
   "github.com/rumyantseva/advent-2017/version"
)

// home is a simple HTTP handler function which writes a response.
func home(w http.ResponseWriter, _ *http.Request)
{
   info := struct {
       BuildTime string `json:"buildTime"`
       Commit    string `json:"commit"`
       Release   string `json:"release"`
   }{
       version.BuildTime, version.Commit, version.Release,
   }
   body, err := json.Marshal(info)
   if err != nil {
       log.Printf("Could not encode info data: %v", err)
       http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
       return
   }
   w.Header().Set("Content-Type", "application/json")
   w.Write(body)
}

我們用Go linker在編譯中去設定BuildTime、Commit和Release變數。

為Makefile增加一些變數:

Makefile

RELEASE?=0.0.1
COMMIT?=$(shell git rev-parse --short HEAD)
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')

這裡面的COMMIT和RELEASE可以在命令列中提供,也可以用semantic version設定RELEASE`。

現在我們為了那些變數重寫build那段:

Makefile

build: clean
   go build \
       -ldflags "-s -w -X ${PROJECT}/version.Release=${RELEASE} \
       -X ${PROJECT}/version.Commit=${COMMIT} -X ${PROJECT}/version.BuildTime=${BUILD_TIME}"
\
       -o ${APP}

我也在Makefile檔案的開始部分定義了PROJECT變數去避免做一些重覆性的事。

Makefile

PROJECT?=github.com/rumyantseva/advent-2017

所有的變化都可以在這裡[10]找到,現在可以用make run去執行它了。

第8步 減少一些依賴

這裡有一些程式碼裡我不喜歡的地方:handlepakcage依賴於versionpackage。這個很容易修改:我們需要讓home 處理變得可以配置。

handler/home.go

// home returns a simple HTTP handler function which writes a response.
func home(buildTime, commit, release string) http.HandlerFunc {
   return func(w http.ResponseWriter, _ *http.Request) {
       ...
   }
}

別忘了同時去修改測試和必須的環境變數。

第9步 健康檢查

在某些情況下,我們需要經常對執行在Kubernetes裡的服務程式進行健康檢查:liveness and readiness probes[11]。這麼做的目的是為了知道容器裡的應用程式是否還在執行。如果liveness探測失敗,這個服務程式將會被重啟,如果readness探測失敗,說明服務還沒有準備好。

為了支援readness探測,我們需要實現一個簡單的處理函式,去傳回 200:

handlers/healthz.go

// healthz is a liveness probe.
func healthz(w http.ResponseWriter, _ *http.Request) {
   w.WriteHeader(http.StatusOK)
}

readness探測方法一般和上面類似,但是我們需要經常去增加一些等待的事件(比如我們的應用已經連上了資料庫)等:

handlers/readyz.go

// readyz is a readiness probe.
func readyz(isReady *atomic.Value) http.HandlerFunc {
   return func(w http.ResponseWriter, _ *http.Request) {
       if isReady == nil || !isReady.Load().(bool) {
           http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
           return
       }
       w.WriteHeader(http.StatusOK)
   }
}

在上面的例子裡,如果變數isReady被設定為true就傳回200。

現在我們看看怎麼使用:

handles.go

func Router(buildTime, commit, release string) *mux.Router {
   isReady := &atomic.Value;{}
   isReady.Store(false)
   go func() {
       log.Printf("Readyz probe is negative by default...")
       time.Sleep(10 * time.Second)
       isReady.Store(true)
       log.Printf("Readyz probe is positive.")
   }()
   r := mux.NewRouter()
   r.HandleFunc("/home", home(buildTime, commit, release)).Methods("GET")
   r.HandleFunc("/healthz", healthz)
   r.HandleFunc("/readyz", readyz(isReady))
   return r
}

在這裡,我們想在10秒後把服務程式標記成可用,當然在真正的環境裡,不可能會等待10秒,我這麼做僅僅是為了報出警報去模擬程式要等待一個時間完成之後才能可用。

所有的修改都可以從這個GitHub[12]找到。

第10步 程式優雅的關閉

當服務需要被關閉的停止的時候,最好不要立刻就斷開所有的連結和終止當前的操作,而是盡可能的去完成它們。Go語言自從1.8版本開始http.Server支援程式以優雅的方式退出。下麵我們看看如何使用這種方式:

main.go

func main() {
   ...
   r := handlers.Router(version.BuildTime, version.Commit, version.Release)
   interrupt := make(chan os.Signal, 1)
   signal.Notify(interrupt, os.Interrupt, os.Kill, syscall.SIGTERM)
   srv := &http.Server;{
       Addr:    ":" + port,
       Handler: r,
   }
   go func() {
       log.Fatal(srv.ListenAndServe())
   }()
   log.Print("The service is ready to listen and serve.")
   killSignal :=    switch killSignal {
   case os.Kill:
       log.Print("Got SIGKILL...")
   case os.Interrupt:
       log.Print("Got SIGINT...")
   case syscall.SIGTERM:
       log.Print("Got SIGTERM...")
   }
   log.Print("The service is shutting down...")
   srv.Shutdown(context.Background())
   log.Print("Done")
}

這裡,我們會捕獲系統訊號,如果發現有SIGKILL,SIGINT或者SIGTERM,我們將優雅的關閉程式。

第11步 Dockerfile

我們的應用程式馬上就以執行在Kubernetes裡了,現在我們把它容器化。

下麵是一個最簡單的Dockerfile:

Dockerfile

FROM scratch
ENV PORT 8000
EXPOSE $PORT
COPY advent /
CMD ["/advent"]

我們建立了一個最簡單的容器,複製程式並且執行它(當然不會忘記設定PORT這個環境變數)。

我們再對Makefile進行一下修改,讓他能夠產生容器映象,並且執行一個容器。在這裡為了交叉編譯,定義環境變數GOOS 和GOARCH在build段。

Makefile

...
GOOS?=linux
GOARCH?=amd64
...
build: clean
   CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build \
       -ldflags "-s -w -X ${PROJECT}/version.Release=${RELEASE} \
       -X ${PROJECT}/version.Commit=${COMMIT} -X ${PROJECT}/version.BuildTime=${BUILD_TIME}"
\
       -o ${APP}
container: build
   docker build -t $(APP):$(RELEASE) .
run: container
   docker stop $(APP):$(RELEASE) || true && docker rm $(APP):$(RELEASE) || true
   docker run --name ${APP} -p ${PORT}:${PORT} --rm \
       -e "PORT=${PORT}" \
       $(APP):$(RELEASE)
...

我們還增加了container段去產生一個容器的映象,並且在run段運去以容器的方式執行我們的程式。所有的變化可以從這裡[13]找到。

現在我們終於可以用make run去檢驗一下整個過程了。

第12步 釋出

在我們的專案裡,我們還依賴一個外部的包(github.com/gorilla/mux)。而且,我們需要為生產環境裡的readness安裝依賴管理。所以我們用了dep之後我們唯一要做的就是執行dep init:

$ dep init
 Using ^1.6.0 as constraint for direct dep github.com/gorilla/mux
 Locking in v1.6.0 (7f08801) for direct dep github.com/gorilla/mux
 Locking in v1.1 (1ea2538) for transitive dep github.com/gorilla/context

這個工具會建立兩個檔案Gopkg.toml和Gopkg.lock,還有一個目錄vendor,個人認為,我會把vendor放到git上去,特別是對與那些比較重要的專案來說。

第13步 Kubernetes

這也是最後一步了。執行一個應用程式到Kubernetes上。最簡單的方法就是在本地去安裝和配置一個minikube(這是一個單點的kubernetes測試環境)。

Kubernetes從容器倉庫拉去映象。在我們的例子裡,我們會用公共容器倉庫——Docker Hub。在這一步裡,我們增加一些變數和執行一些命令。

Makefile:

CONTAINER_IMAGE?=docker.io/webdeva/${APP}
...
container: build
   docker build -t $(CONTAINER_IMAGE):$(RELEASE) .
...
push: container
   docker push $(CONTAINER_IMAGE):$(RELEASE)

這個CONTAINER_IMAGE變數用來定義一個映象的名字,我們用這個映象存放我們的服務程式。如你所見,在這個例子裡包含了我的使用者名稱(webdeva)。如果你在hub.docker.com上沒有賬戶,那你就先得建立一個,然後用docker login命令登陸,這個時候你就可以推送你的映象了。

現在我們試一下make push:

$ make push
...
docker build -t docker.io/webdeva/advent:0.0.1 .
Sending build context to Docker daemon   5.25MB
...
Successfully built d3cc8f4121fe
Successfully tagged webdeva/advent:0.0.1
docker push docker.io/webdeva/advent:0.0.1
The push refers to a repository [docker.io/webdeva/advent]
ee1f0f98199f: Pushed
0.0.1: digest: sha256:fb3a25b19946787e291f32f45931ffd95a933100c7e55ab975e523a02810b04c size: 528

現在你看它可以工作了,從這裡[14]可以找到這個映象。

現在我們來定義一些Kubernetes裡需要的配置檔案。通常情況下,對於一個簡單的服務程式,我們需要定一個deployment,一個service和一個ingress。預設情況下所有的配置都是靜態的,即配置檔案裡不能使用變數。希望以後可以使用helm來建立一份靈活的配置。

在這個例子裡,我們不會使用helm,雖然這個工具可以定義一些變數ServiceName和Release,它給我們的部署帶來了很多靈活性。以後,我們會使用sed命令去替換一些事先定好的值,以達到“變數”目的。

現在我們看一下deployment的配置:

deployment.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
 name: {{ .ServiceName }}
 labels:
   app: {{ .ServiceName }}
spec:
 replicas: 3
 strategy:
   type: RollingUpdate
   rollingUpdate:
     maxUnavailable: 50%
     maxSurge: 1
 template:
   metadata:
     labels:
       app: {{ .ServiceName }}
   spec:
     containers:
     - name: {{ .ServiceName }}
       image: docker.io/webdeva/{{ .ServiceName }}:{{ .Release }}
       imagePullPolicy: Always
       ports:
       - containerPort: 8000
       livenessProbe:
         httpGet:
           path: /healthz
           port: 8000
       readinessProbe:
         httpGet:
           path: /readyz
           port: 8000
       resources:
         limits:
           cpu: 10m
           memory: 30Mi
         requests:
           cpu: 10m
           memory: 30Mi
     terminationGracePeriodSeconds: 30

我們需要用另外一篇文章來討論Kubernetes的配置,但是現在你看見了,我們這裡所有定義的資訊裡包括了容器的名稱, liveness和readness探針。

一個典型的service看起來更簡單:

service.yaml

apiVersion: v1
kind: Service
metadata:
 name: {{ .ServiceName }}
 labels:
   app: {{ .ServiceName }}
spec:
 ports:
 - port: 80
   targetPort: 8000
   protocol: TCP
   name: http
 selector:
   app: {{ .ServiceName }}

最後是ingress,這裡我們定義了一個規則來能從Kubernetes外面訪問到裡面。假設,你想要訪問的域名是advent.test(這當然是一個假的域名)。

ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
 annotations:
   kubernetes.io/ingress.class: nginx
   ingress.kubernetes.io/rewrite-target: /
 labels:
   app: {{ .ServiceName }}
 name: {{ .ServiceName }}
spec:
 backend:
   serviceName: {{ .ServiceName }}
   servicePort: 80
 rules:
 - host: advent.test
   http:
     paths:
     - path: /
       backend:
         serviceName: {{ .ServiceName }}
         servicePort: 80

現在為了檢查它是否能夠工作,我們需要安裝一個minikube,它的官方檔案在[這裡[15]。我們還需要kubectl這個工具去把我們的配置檔案應用到上面,並且去檢查服務是否正常啟動。

執行minikube,需要開啟ingress並且準備好kubectl,我們要用它執行一些命令


minikube start
minikube addons enable ingress
kubectl config use-context minikube

我們在Makefile裡加一個minikube段,讓它去安裝我們的服務:

Makefile

minikube: push
   for t in $(shell find ./kubernetes/advent -type f -name "*.yaml"); do \
       cat $$t | \
           gsed -E "s/\{\{(\s*)\.Release(\s*)\}\}/$(RELEASE)/g" | \
           gsed -E "s/\{\{(\s*)\.ServiceName(\s*)\}\}/$(APP)/g"; \
       echo ---; \
   done > tmp.yaml
   kubectl apply -f tmp.yaml

這個命令會把所有的yaml檔案的配置資訊都合併成一個臨時檔案,然後替換變數Release和ServiceName(這裡要註意一下,我使用的gsed而不是sed)並且執行kubectl apply進行安裝的Kubernetes。

現在我們來看一下我的工作成果:

$ kubectl get deployment
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
advent    3         3         3            3           1d
$ kubectl get service
NAME         CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
advent       10.109.133.147          80/TCP    1d
$ kubectl get ingress
NAME      HOSTS         ADDRESS        PORTS     AGE
advent    advent.test   192.168.64.2   80        1d

現在我們可以傳送一個http的請求到我們的服務上,但是首先還是要把域名adventtest增加到/etc/host檔案裡:

echo "$(minikube ip) advent.test" | sudo tee -a /etc/hosts

現在,我們終於可以使用我們的服務了:

curl -i http://advent.test/home
HTTP/1.1 200 OK
Server: nginx/1.13.6
Date: Sun, 10 Dec 2017 20:40:37 GMT
Content-Type: application/json
Content-Length: 72
Connection: keep-alive
Vary: Accept-Encoding
{"buildTime":"2017-12-10_11:29:59","commit":"020a181","release":"0.0.5"}%

看,它工作了!

從這裡[16]你可找到所有的步驟,這裡[17]是提交的歷史,這裡[18]是最後的結果。如果你還有任何的疑問,請建立一個issue或者透過twitter:@webdeva或者是留一條comment。

建立一個在生產環境中靈活的服務程式對你來說也許很有意思。在這個例子裡可以去看一下takama/k8sapp[19],一個用Go語言寫的能夠執行在Kubernetes上面的應用程式模版。

相關連結:

  1. https://github.com/rumyantseva/advent-2017/commit/76864ab0587dd9

  2. https://github.com/rumyantseva/advent-2017/tree/all-steps

  3. https://github.com/rumyantseva/advent-2017/commits/master

  4. https://github.com/golang/glog

  5. https://github.com/sirupsen/logrus

  6. https://github.com/gorilla/mux

  7. https://github.com/rumyantseva/advent-2017/commit/1a61e7952e227e33eaab81404d7bff9278244080

  8. https://blog.gopheracademy.com/advent-2017/make

  9. https://github.com/rumyantseva/advent-2017/commit/90966780ba6656f8dc0aebd166938c9adcbe0514

  10. https://github.com/rumyantseva/advent-2017/commit/eaa4ff224b32fb343f5eac2a1204cc3806a22efd

  11. https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/

  12. https://github.com/rumyantseva/advent-2017/commit/e73b996f8522b736c150e53db059cf041c7c3e64

  13. https://github.com/rumyantseva/advent-2017/commit/909fef6d585c85c5e16b5b0e4fdbdf080893b679

  14. https://hub.docker.com/r/webdeva/advent/tags/

  15. https://github.com/kubernetes/minikube#installation

  16. https://github.com/rumyantseva/advent-2017

  17. https://github.com/rumyantseva/advent-2017/commits/master

  18. https://github.com/rumyantseva/advent-2017/tree/all-steps

  19. https://github.com/takama/k8sapp

原文連結:https://blog.gopheracademy.com/advent-2017/kubernetes-ready-service

基於Kubernetes的容器雲平臺實踐培訓

本次培訓包含:Kubernetes核心概念;Kubernetes叢集的安裝配置、運維管理、架構規劃;Kubernetes元件、監控、網路;針對於Kubernetes API介面的二次開發;DevOps基本理念;Docker的企業級應用與運維等,點選識別下方二維碼加微信好友瞭解具體培訓內容

點選閱讀原文連結即可報名。
贊(0)

分享創造快樂