Kubernetesサービス作成のチュートリアル

著者から。 GopherAcademyブログの12月5日の連続で、Goコミュニティ最も多様な代表者が、クリスマス前の特別な一連の投稿で経験を共有しています。 今年、 マイクロサービスワークショップの最初の部分に基づいて書かれた記事をIgor Dolzhikovと提供することも決めました。 Habréについては、 すでにこのガイドの一部をすでに検討しました







Goを試したことがある人なら、Goでのサービスの作成が簡単であることをご存知でしょう。 httpサービスを開始できるように、ほんの数行のコードが必要です。 しかし、そのようなアプリケーションを実稼働環境で準備する場合、何を追加する必要がありますか? Kubernetesで実行する準備ができているサービスの例を見てみましょう。







この記事のすべてのステップは1つのタグで見つけることができます。または、commitで記事のコミットの例に従うことができます。







ステップ1.最も単純なサービス



したがって、非常に単純なアプリケーションがあります。







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を使用すると、このサービスの動作を確認できcurl -i http://127.0.0.1:8000/home



curl -i http://127.0.0.1:8000/home



しかし、このアプリケーションを起動すると、端末にはその状態に関する情報がないことがわかります。







ステップ2.ロギングを追加する



まず、サービスで何が起こっているのかを理解し、エラーやその他の重要な状況を記録できるようにするために、ロギングを追加しましょう。 この例では、Go標準ライブラリの最も単純なロガーを使用しますが、実稼働で開始される実際のサービスには、 gloglogrusなどのより複雑な興味深いソリューションが存在する場合があります。







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.ルーターを追加する



このアプリケーションでは、ほとんどの場合、さまざまなURI、HTTPメソッド、またはその他のルールの処理を簡素化するためにルーターを使用します。 Go標準ライブラリにはルーターがないので、標準のnet/http



ライブラリと完全に互換性のあるgorilla / muxを試してみましょう。







サービスに顕著な数のルーティングルールが必要な場合は、ルーティングに関連するすべてを別のパッケージに入れるのが理にかなっています。 ハンドラーパッケージ内のハンドラー関数と同様に、ルーティングルールの初期化と設定を行いましょう(完全な変更については、 こちらをご覧ください )。







構成されたルーターを返すRouter



関数と、 /home



パスのルールを処理するhome



関数を追加します。 私はそのような機能を別々のファイルに分けることを好みます:







ハンドラー/handlers.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 }
      
      





ハンドラー/ 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



ファイルに小さな変更が必要です。







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)) }
      
      





ステップ4.テスト



いくつかのテストを追加しますhttptest



は標準のhttptest



パッケージを使用できます。 Router



機能の場合、次のように記述できます。







ハンドラー/handlers_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) } }
      
      





ここでは、 /home



GET



メソッドを呼び出すとコード200



が返されることを確認します。 そして、 POST



を送信しようとするPOST



予想される応答はすでに405



ます。 最後に、存在しないパスの場合、 404



予想されます。 一般に、このテストはやや冗長になる可能性があります。これは、ルーターがgorilla/mux



内のテストで既にカバーされているため、ここで確認できるケースはさらに少ないからです。







home



関数の場合、コードだけでなく応答本文も確認するのが理にかなっています。







ハンドラー/ 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 test



を実行し、テストが機能することを確認します







 $ 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



でリッスンし、この値を構成する機能が役立つ場合があります。 サービスを記述するための非常に興味深いアプローチである12要素アプリケーションのマニフェストでは、環境に基づいて構成を保存することを推奨しています。 それでは、 環境変数を使用してポートの構成を設定しましょう:







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



数日前、 make



ユーティリティに関する記事がGopherAcademyブログに公開されました。これは、繰り返しのアクションに対処する必要がある場合に非常に役立ちます。 プロジェクトでこれをどのように使用できるかを見てみましょう。 現在、テストの実行とサービスのコンパイルと開始という2つの反復アクションがあります。 これらのアクションをMakefileに追加しますが、単にgo run



代わりに、 go build



を使用してコンパイル済みのバイナリを実行します。このオプションは、将来アプリケーションを本番用に準備する場合に適しています。







メイクファイル
 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 build



を実行make build



と、最初にclean



が呼び出されます。







ステップ7.バージョン管理



サービスに追加する次のプラクティスは、バージョン管理です。 特定のビルドを特定し、本番環境で使用しているコミット、さらにはバイナリが正確にいつビルドされたかを知ることが役立つ場合があります。







この情報を保存するには、新しいパッケージ-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



追加することもできます(テストを修正することを忘れないでください!):







ハンドラー/ 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) }
      
      





コンパイル時に変数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



およびBUILD_TIME



指定されたコマンドによって定義され、 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}
      
      





また、同じことを数回繰り返さないように、 PROJECT



変数をMakefile



の先頭に追加しました。







Makefile









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





このステップで行われたすべての変更は、 ここにありますmake run



を試してmake run



を確認してください。







ステップ8.依存関係を減らす!



コードについて気に入らないことが1つあります。 handler



パッケージはversion



パッケージに依存します。 これを変更するのは簡単です。 home



機能を設定可能にする必要があります。







handlers/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. Helscheki



Kubernetesでサービスを起動する場合、通常、2つのhelchecksを追加する必要があります。liveness およびreadiness probeです。 活性テストの目的は、サービスが開始されたことを明確にすることです。 活性プローブが失敗すると、サービスが再起動されます。 準備テストの目的は、アプリケーションがトラフィックを受信する準備ができていることを理解することです。 準備プローブが失敗すると、コンテナはサービスロードバランサーから削除されます。







活性プローブを決定するには、常に200



返す単純なハンドラーを作成できます。







ハンドラー/ healthz.go
 // healthz is a liveness probe. func healthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }
      
      





準備サンプルの場合、多くの場合、同様のソリューションで十分ですが、トラフィックの処理を開始するために、何らかのイベント(たとえば、データベースの準備ができている)を待つ必要がある場合があります。







ハンドラー/ 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



変数isReady



true



設定されている場合にのみ200



を返しtrue









これがどのように使用できるかを見てみましょう:







handlers.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で見つけることができます。







ご注意 アプリケーションが大量のトラフィックを受信すると、不安定な応答を開始します。 たとえば、タイムアウトのために活性プローブが失敗し、コンテナがリロードされます。 このため、一部のエンジニアは、活性サンプルをまったく使用しないことを好みます。 個人的には、より多くのリクエストがサービスに来ていることに気付いたら、リソースをスケーリングする方が良いと思います。 たとえば、 HPAを使用して炉の自動スケーリングを試すことができます。







ステップ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, 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 := <-interrupt switch killSignal { 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") }
      
      





この例では、システム信号SIGINT



およびSIGTERM



をインターセプトし、これらのいずれかがキャッチされた場合、サービスを正しく停止します。







ご注意 このコードを書いたとき、ここでもSIGKILL



傍受しようとしました。
私はこのアプローチを異なるライブラリーで何度か見ましたが、確実に機能することを確信しました。 しかし、SandorSzücsが指摘したように、 SIGKILL



傍受することSIGKILL



できません。
SIGKILL



場合SIGKILL



アプリケーションはすぐに停止します。







ステップ11. Dockerfile



アプリケーションは、Kubernetesで実行する準備がほぼ整いました。今度はコンテナ化するときです。







必要な最も単純なDockerfile



、次のようになります。







Dockerfile









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





可能な限り最小のコンテナを作成し、そこにバイナリをコピーして実行します(さらに、 PORT



変数を転送することも忘れませんでした)。







ここで、 Makefile



少し変更して、イメージアセンブリとコンテナー起動を追加します。 ここでは、 build



ターゲットの一部としてクロスコンパイルに使用する2つの新しい変数GOOS



GOARCH



が便利になります。







メイクファイル
 ... 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



ターゲットを調整して、バイナリを起動する代わりにコンテナが起動するようにしました。 すべての変更はここから入手できます







これで、 make run



make run



してプロセス全体をテストできます。







ステップ12.依存関係管理



プロジェクトには外部依存関係が1つありますgithub.com/gorilla/mux



。 したがって、実際に本番用の準備が整ったアプリケーションの場合、 依存関係管理を追加する必要があります。 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.lock



Gopkg.lock



、および使用されるすべての依存関係を含むvendor



ディレクトリが作成されました。 個人的には、特に重要なプロジェクトの場合、 vendor



にgitをプッシュすることを好みます。







ステップ13. Kubernetes



最後に、 最終ステップ :Kubernetesでアプリケーションを起動します。 Kubernetesを試す最も簡単な方法は、ローカル環境にminikubeをインストールして構成することです。







Kubernetesは、レジストリ(Dockerレジストリ)から画像をダウンロードします。 私たちの場合、パブリックレジストリで十分です-Docker HubMakefile



別の変数と別のコマンドが必要です。







メイクファイル
 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 docker login



を使用して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
      
      





うまくいく! これで、作成されたイメージがレジストリに見つかります







Kubernetesに必要な構成(マニフェスト)を定義します。 これらはJSONまたはYAML形式の静的ファイルであるため、「変数」の代わりにsed



ユーティリティを使用する必要があります。 この例では、 deploymentservice、およびingressの 3つのタイプのリソースを見ていきます。







ご注意 helmプロジェクトは、一般にKubernetesの構成リリースを管理する問題を解決し、特に柔軟な構成の作成を検討します。 したがって、 sed



だけsed



は十分でない場合sed



、Helmを知ることは理にかなっています。







展開の構成を検討します。







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の構成の問題については、別の記事で対処するのが最適ですが、ご覧のとおり、特にコンテナーのレジストリとイメージがここで定義され、活性と準備サンプルのルールも定義されています。







サービスの一般的な構成はよりシンプルに見えます。







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 }}
      
      





そして最後に、イングレス。 ここでは、たとえば、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



をインストールします 。 さらに、構成を適用してサービスを検証するには、 kubectlユーティリティが必要になります。







minikube



を起動し、イングレスを有効にしてkubectl



を準備するには、次のコマンドkubectl



必要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



実際の値に置き換え(通常のsed



ではなくgsed



を使用)、 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 <none> 80/TCP 1d $ kubectl get ingress NAME HOSTS ADDRESS PORTS AGE advent advent.test 192.168.64.2 80 1d
      
      





ここで、指定されたドメインを介してサービスにリクエストを送信してみましょう。 まず、ローカルの/etc/hosts



advent.test



ドメインを追加する必要があり/etc/hosts



(Windowsの場合- %SystemRoot%\System32\drivers\etc\hosts



):







 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"}%
      
      





やれやれ!










マニュアルのすべてのステップはここにあります。2つのオプションがあります: commit-by-commit1つのディレクトリ内のすべてのステップです 。 質問がある場合は、問題作成したり、Twitterで@webdevaをノックしたり、ここにコメントを残したりできます。







生産準備が整った真のより柔軟なサービスがどのように見えるかについて興味がある場合は、 kubernetesの要件を満たすGoアプリケーションテンプレートであるtakama / k8sappプロジェクトをご覧ください。







PSレビューとコメントを寄せてくれたナタリー・ピストノビッチポール ・ブルソーサンドール シュッツマキシム・フィラトフ 、その他のコミュニティの仲間に感謝します。








All Articles