著者から。 GopherAcademyブログの12月5日の連続で、Goコミュニティの最も多様な代表者が、クリスマス前の特別な一連の投稿で経験を共有しています。 今年、 マイクロサービスワークショップの最初の部分に基づいて書かれた記事をIgor Dolzhikovと提供することも決めました。 Habréについては、 すでにこのガイドの一部をすでに検討しました 。
Goを試したことがある人なら、Goでのサービスの作成が簡単であることをご存知でしょう。 httpサービスを開始できるように、ほんの数行のコードが必要です。 しかし、そのようなアプリケーションを実稼働環境で準備する場合、何を追加する必要がありますか? Kubernetesで実行する準備ができているサービスの例を見てみましょう。
この記事のすべてのステップは1つのタグで見つけることができます。または、commitで記事のコミットの例に従うことができます。
ステップ1.最も単純なサービス
したがって、非常に単純なアプリケーションがあります。
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標準ライブラリの最も単純なロガーを使用しますが、実稼働で開始される実際のサービスには、 glogやlogrusなどのより複雑な興味深いソリューションが存在する場合があります。
3つの状況に興味があるかもしれません:サービスが開始したとき、サービスが要求を処理する準備ができたとき、そしてhttp.ListenAndServe
がエラーを返すとき。 結果は次のようになります 。
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
関数を追加します。 私はそのような機能を別々のファイルに分けることを好みます:
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 }
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)) }
ステップ4.テスト
いくつかのテストを追加します 。 httptest
は標準のhttptest
パッケージを使用できます。 Router
機能の場合、次のように記述できます。
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
関数の場合、コードだけでなく応答本文も確認するのが理にかなっています。
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要素アプリケーションのマニフェストでは、環境に基づいて構成を保存することを推奨しています。 それでは、 環境変数を使用してポートの構成を設定しましょう:
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を追加します。
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" )
アプリケーションの起動時にこれらの変数を記録できます。
... func main() { log.Printf( "Starting the service...\ncommit: %s, build time: %s, release: %s", version.Commit, version.BuildTime, version.Release, ) ... }
また、それらをhome
追加することもできます(テストを修正することを忘れないでください!):
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 is a liveness probe. func healthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }
準備サンプルの場合、多くの場合、同様のソリューションで十分ですが、トラフィックの処理を開始するために、何らかのイベント(たとえば、データベースの準備ができている)を待つ必要がある場合があります。
// 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
。
これがどのように使用できるかを見てみましょう:
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
「正常なシャットダウン」をサポートしています。 これをどのように使用できるかを検討してください。
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 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 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
ユーティリティを使用する必要があります。 この例では、 deployment 、 service、およびingressの 3つのタイプのリソースを見ていきます。
ご注意 helmプロジェクトは、一般にKubernetesの構成リリースを管理する問題を解決し、特に柔軟な構成の作成を検討します。 したがって、 sed
だけsed
は十分でない場合sed
、Helmを知ることは理にかなっています。
展開の構成を検討します。
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の構成の問題については、別の記事で対処するのが最適ですが、ご覧のとおり、特にコンテナーのレジストリとイメージがここで定義され、活性と準備サンプルのルールも定義されています。
サービスの一般的な構成はよりシンプルに見えます。
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
にアクセスするときにサービスにリクエストを送信するとします(実際には存在しません)。
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-commitと1つのディレクトリ内のすべてのステップです 。 質問がある場合は、問題を作成したり、Twitterで@webdevaをノックしたり、ここにコメントを残したりできます。
生産準備が整った真のより柔軟なサービスがどのように見えるかについて興味がある場合は、 kubernetesの要件を満たすGoアプリケーションテンプレートであるtakama / k8sappプロジェクトをご覧ください。
PSレビューとコメントを寄せてくれたナタリー・ピストノビッチ 、 ポール ・ブルソー 、 サンドール ・ シュッツ 、 マキシム・フィラトフ 、その他のコミュニティの仲間に感謝します。