Mercari Advent Calendar 2018 の2日目はCrossUXチームの@mkazutaka(twitterは@makazutaka)がお送りします
昨日のアドベントカレンダーに@stanakaさんが取り上げているようにメルカリではMicroservices化に向けて開発が進んでおります。その流れに乗るように前QまでPHPを使って開発していた自分も今QからMicroservicesで実現されているサービスでの開発を行っております
メルカリではMicroservicesの実現にあたってGoogleCloudPlatform(GCP)、Terraform、Docker、Kubernetes、Halyard、gRPC、Goといったさまざまなサービスからフレームワーク、言語を利用しています
メルカリでのMicroservices上での開発をする以上、これらに対して多少なりとも理解が必要です
本記事ではメルカリでのMicroservices上で使われてるサービスを理解するため自分が行ったTerraformによるGCPプロジェクトの構築からSpinnakerによる自動デプロイまでの方法を紹介します
履歴
12/03 [追記]ServiceAccountをTerraformで作成する修正及びSpinnaker欄を追加しました
Spinnakerについては下記の記事もご参照ください!
本記事では、下記のすべてが実現しているプロジェクトを作ることをゴールとします
- Terraformを用いてGCP上にKubernetes clusterが作成されている
- Terraformを用いてGCP上に必要なService Accountが作成されている
- Kubernetes Cluster上でSpinnakerが動作している
- Kubernetes Cluster上でgRPCプロジェクトが1つ動いている
- Kubernetes Cluster上でgRPCプロジェクトにリクエストを投げることができる、またレスポンスを受け取ることができる
- Gitのmasterブランチのレポジトリ内容を更新するとSpinnaker上で自動デプロイ・ビルドが起こる
また本記事で取り扱わないことは下記とします。
- 各アプリケーションの説明
- 各アプリケーションのインストール方法
- 各アプリケーションのコードの詳細な解説
本記事で使う書くソフトウェアのバージョンは下記のようになっております
||*'-') < gcloud --version Google Cloud SDK 226.0.0 cloud-build-local kubectl 2018.09.17 ||*'-') < hal --version 1.12.0-20181024113436 ||*'-') < terraform --version Terraform v0.11.10
始める前に以下の準備が必要です
- GCP上でProjectの作成
はじめにgcloudのconfigurationの設定を行います
GCPのProject名は、m-advent-201802
としています。
||*'-') < gcloud init ... ||*'-') < gcloud gcloud config set compute/zone asia-northeast1-a ||*'-') < gcloud config set compute/region asia-northeast1 ||*'-') < gcloud config list ... project = m-advent-201802
次にTerraform用にServiceAccountを作成します
#!/bin/bash SA_NAME=terraform-service-account gcloud iam service-accounts create ${SA_NAME} --display-name ${SA_NAME} # Grant this service account access to m-advent-201802 SA_EMAIL=$(gcloud iam service-accounts list --filter="displayName:$SA_NAME" --format='value(email)') PROJECT=m-advent-201802 gcloud projects add-iam-policy-binding ${PROJECT} --role roles/owner --member serviceAccount:${SA_EMAIL} # Create the key of service account gcloud iam service-accounts keys create .terraform-service-account.json --iam-account ${SA_EMAIL}
上記が終わったのでTerraformでKubernetesClusterを作成します
基本的に公式のGetting Startedを参考にしていますが、
追加でLocal Valueも使用しています
// edit values.tf locals { project = "m-advent-201802" region = "asia-east1" zone = "asia-northeast1-a" kubernetes = { name = "terraform-cluster" } network = { name = "terraform-network" } credentials = "${file(".terraform-service-account.json")}" //上記で作成したKeyFileのPath }
// edit kubernetes.tf provider "google" { project = "${local.project}" region = "${local.region}" zone = "${local.zone}" credentials = "${local.credentials}" } resource "google_container_cluster" "cluster" { name = "${local.kubernetes["name"]}" initial_node_count = 3 network = "${google_compute_network.vpc_network.self_link}" enable_legacy_abac = true } resource "google_compute_network" "vpc_network" { name = "${local.network["name"]}" auto_create_subnetworks = "true" }
applyします
||*'-') < terraform init ||*'-') < terraform plan # check ||*'-') < terraform apply
しばらく待てば、KubernetesClusterが作成できていると思います。
次に、今後使うHalyard用のServiceAccountもTerraformで作成します
// edit halyard-service-account.tf resource "google_service_account" "halyard-account" { account_id = "halyard-account" display_name = "halyard-account" } resource "google_project_iam_binding" "project-roles-storage-admin" { project = "${local.project}" role = "roles/storage.admin" members = [ "serviceAccount:${google_service_account.halyard-account.email}" ] } resource "google_project_iam_binding" "project-roles-browser" { project = "${local.project}" role = "roles/browser" members = [ "serviceAccount:${google_service_account.halyard-account.email}" ] }
再度applyします
||*'-') < terraform plan # check ||*'-') < terraform apply
それでは、KubernetesCluster上にHalyardを使いSpinnakerをDeployします
少々長いので、適宜リンク先のドキュメントをご参照ください
なおリンク先のドキュメントにはServiceAccount作成のコマンドが書いていますが既にTerraformで作成済みのため必要ありません
#!/bin/bash # 1.Get Credential KUBERNETES_CLUSTER=terraform-cluster gcloud container clusters get-credentials ${KUBERNETES_CLUSTER} # 2. Halyard config settings PROJECT=$(gcloud info --format='value(config.project)') hal config --set-current-deployment ${PROJECT} # 3. Distribute installation # ref: https://www.spinnaker.io/setup/install/environment/#distributed-installation ACCOUNT=my-k8s-v2-account hal config provider kubernetes enable hal config provider kubernetes account add ${ACCOUNT} --provider-version v2 --context $(kubectl config current-context) hal config features edit --artifacts true hal config deploy edit --type distributed --account-name $ACCOUNT # 4. Create Service key of Halyard Account that is already created by terraform # ref: https://www.spinnaker.io/setup/install/storage/gcs/ SERVICE_ACCOUNT_NAME=halyard-account SA_EMAIL=$(gcloud iam service-accounts list --filter="displayName:$SERVICE_ACCOUNT_NAME" --format='value(email)') SERVICE_ACCOUNT_DEST=.halyard-service-account.json gcloud iam service-accounts keys create ${SERVICE_ACCOUNT_DEST} --iam-account ${SA_EMAIL} # 5. Choose Storage Service # ref: https://www.spinnaker.io/setup/install/storage/gcs/ BUCKET_LOCATION=us hal config storage gcs edit --project ${PROJECT} --bucket-location ${BUCKET_LOCATION} --json-path ${SERVICE_ACCOUNT_DEST} hal config storage edit --type gcs # 6. Container Registry # ref. https://www.spinnaker.io/setup/install/providers/docker-registry/#google-container-registry PASSWORD_FILE=${SERVICE_ACCOUNT_DEST} hal config provider docker-registry enable hal config provider docker-registry account add my-docker-registry --address ${ADDRESS} --username _json_key --password-file ${PASSWORD_FILE} # 7. Access Private Docker Registry kubectl create secret docker-registry ${SECRETNAME} --docker-server=https://gcr.io --docker-username=_json_key --docker-email=user@example.com --docker-password="$(cat ${SERVICE_ACCOUNT_DEST})" # 8. Deploy hal version list VERSION=1.10.5 hal config version edit --version ${VERSION} hal deploy apply # 9. Check kubectl get pods --namespace spinnaker
これで、TerraformのDeployは完了です。次にgRPCを使ったアプリケーションを作成します
ちなみにHalyardを使わなくてもGoogle Cloud Platform Marketplaceから1クリックでSpinnaker環境を構築できたりもします。
アプリケーションのためにディレクトリを作成します
ここでは、ディレクトリ名をm-advent-201802-src
としています。
同様にGithubのRepositoryも作成しておいてください。
Go
とリクエストすればBold!
とレスポンスが返ってくるアプリケーションを作成します。
最終的に下記のようなアプリケーションを作成します
m-advent-201802-src ├── Dockerfile ├── cloudbuild.yaml ├── cmd │ ├── client │ │ └── main.go │ └── server │ └── main.go ├── proto │ └── value.proto └── value ├── server.go └── value.pb.go
はじめにProtoファイルを作成します
// edit proto/value.proto syntax = "proto3"; package value; service valueService { rpc Say (SayRequest) returns (SayResponse); } message SayRequest { string message = 1; } message SayResponse { string message = 1; }
Protoファイルをコンパイルします。
||*'-') < mkdir value ||*'-') < protoc --proto_path=proto --go_out=plugins=grpc:value value.proto
アプリケーションのメインロジックを書きます
// edit value/server.go package value import ( "context" ) type Server interface { Say(ctx context.Context, in *SayRequest) (*SayResponse, error) } func New() (Server, error) { return &server{}, nil } type server struct{} func (s *server) Say(ctx context.Context, in *SayRequest) (*SayResponse, error) { if in.Message == "Go" { return &SayResponse{ Message: "Bold!", }, nil } return &SayResponse{ Message: "Mercari", }, nil }
アプリケーションの実行用のコマンドを書きます
// Server // edit cmd/server/main.go package main import ( "google.golang.org/grpc" "log" "net" "github.com/mkazutaka/m-advent-201802-src/value" ) const ( port = ":50001" ) func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() server, nil := value.New() value.RegisterValueServiceServer(s, server) // Register reflection service on gRPC server. if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
// Client // edit cmd/client/main.go package main import ( "golang.org/x/net/context" "google.golang.org/grpc" "log" "time" pb "github.com/mkazutaka/m-advent-201802-src/value" ) const ( address = "localhost:50001" ) func main() { // Set up a connection to the server. conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewValueServiceClient(conn) // Contact the server and print out its response. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() req := &pb.SayRequest{Message: "Go"} log.Printf("Say: %s", req.Message) r, err := c.Say(ctx, req) if err != nil { log.Fatalf("could not: %v", err) } log.Printf("Ans: %s", r.Message) }
確認します
||*'-') < go run server/main.go ||*'-') < go run client/main.go Say: Go Ans: Bold!
次にGithubにPushするたびにDocker Imageを作成するためのCloudBuilderの設定をします
Cloud Builderの設定
ドキュメントと同じなので下記URLをご参照ください。
Continuous Delivery with Containers on GCP – Spinnaker
自分は下記の画像のようにしています。_GITHUB_USER
と_GITHUB_REPO
のenviromentを設定します。
Cloud Builder用のファイルを作成します
// edit cloudbuild.yaml steps: - name: 'gcr.io/cloud-builders/go' args: ['get', 'google.golang.org/grpc'] env: ['PROJECT_ROOT=github.com/$_GITHUB_USER/$_GITHUB_REPO'] - name: 'gcr.io/cloud-builders/go' args: ['get', 'github.com/golang/protobuf/proto'] env: ['PROJECT_ROOT=github.com/$_GITHUB_USER/$_GITHUB_REPO'] - name: 'gcr.io/cloud-builders/go' args: ['install', 'github.com/$_GITHUB_USER/$_GITHUB_REPO/cmd/server'] env: ['PROJECT_ROOT=github.com/$_GITHUB_USER/$_GITHUB_REPO'] - name: "gcr.io/cloud-builders/docker" args: ["build", "-t", "gcr.io/$PROJECT_ID/$_GITHUB_REPO", "-f", "Dockerfile", "."] - name: "gcr.io/cloud-builders/docker" args: ["tag", "gcr.io/$PROJECT_ID/$_GITHUB_REPO", "gcr.io/$PROJECT_ID/$_GITHUB_REPO:$BRANCH_NAME"] images: - "gcr.io/$PROJECT_ID/$_GITHUB_REPO"
Dockerfileを作成します
// edit Dockerfile FROM alpine COPY gopath/bin/server /go/bin/server EXPOSE 50001 ENTRYPOINT /go/bin/server
Cloud Build等のチェックをします
# check local ||*'-') < cloud-build-local --config=cloudbuild.yaml --dryrun=false --substitutions=_GITHUB_REPO=m-advent-201802-src,_GITHUB_USER=mkazutaka,BRANCH_NAME=master,COMMIT_SHA=11111 . # check remote ||*'-') < gcloud builds submit --config=cloudbuild.yaml --substitutions=_GITHUB_REPO=m-advent-201802-src,_GITHUB_USER=mkazutaka,BRANCH_NAME=master,COMMIT_SHA=11111 . # if you check trigger ||*'-') < git push
上記が動いたら、CloudBuild側の設定はおしまいです!あとSpinnaker側です。あとちょっとです!
Terraform上でApplicationを作成します
はじめにTerraformに接続します
||*'-') < hal deploy connect
localhost:9000
にアクセスして、右上のActionsボタンからCreate Applicationを押し、下記画像のように入力してください
次にLoadBalancerの設定をします
INFRASTRUCRE
からLOAD BALANCER
を選択し、Create Load Balancer
ボタンを押します
その後,Manifestを入力します。Manifestは下記のようなしました
kind: Service apiVersion: v1 metadata: name: advent-201802-service labels: app: advent-201802 spec: type: LoadBalancer selector: app: advent-201802 ports: - protocol: TCP port: 50001
Pipelineを作成します
PIPELINES
を選択し、Create
を押します
適当にPipeline Name
を入力します。
その後、Automated Triggers
をAdd
し値を入力します
基本的に、このあたりは少しバージョンは古いですが公式ドキュメントをご覧ください。
Triggerは下記のように設定します
次にDeploy用のManifestを書きます
apiVersion: apps/v1 kind: Deployment metadata: name: advent-201802-deployment labels: app: advent-201802 spec: replicas: 3 selector: matchLabels: app: advent-201802 template: metadata: labels: app: advent-201802 spec: containers: - name: advent-201802 image: gcr.io/m-advent-201802/m-advent-201802-src ports: - containerPort: 50001
上記が終わればStart Manual Execution
から実行してみてください
うまく行けば、podsが表示されます
||*'-') < kubectl get pods NAME READY STATUS RESTARTS AGE advent-201802-deployment-6f7b9594f8-kmdfp 1/1 Running 0 58s ... ||*'-') < kubectl port-forward advent-201802-deployment-6f7b9594f8-kmdfp 50001:50001 Forwarding from 127.0.0.1:50001 -> 50001 ... # 別Terminalで ||*'-') < go run cmd/client/main.go Say: Go Ans: Bold!
AutoTriggerの確認
最後にAutoTriggerが動作しているか確認します
適当にcmd/client/main.goを編集します
// edit cmd/client/main.go --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -28,7 +28,7 @@ func main() { - req := &pb.SayRequest{Message: "Go"} + req := &pb.SayRequest{Message: "Be"} // edit cmd/server/main.go --- a/value/server.go +++ b/value/server.go + if in.Message == "Be" { + return &SayResponse{ + Message: "Professional!", + }, nil + }
変更後、git push
するとTriggerが自動で実行されます
確認します
||*'-') < git commit -am 'modified Request Message' && git push ||*'-') < kubectl get pods ... ||*'-') < kubectl port-forward <Pod Name> 50001:50001 # 別ターミナルで ||*'-') < go run cmd/client/main.go Say: Be Ans: Professional!
できました!おめでとうございます!
今日の記事は、アプリケーションそれぞれの表面的な部分しかなぞっていません。もちろん本番環境で使うなどできないようなものです(実際は監視ツール群などが必要でさらにMicroservices同士でやりとりも必要…)
それでも新しい技術に触れ動かすことができるというのは楽しいものであり、またこれを実際に深い部分まで理解してメルカリ上で動かしているチームがいるというのも非常に尊敬するところであります
弊社では、新しい技術を学ぶことを楽しめるエンジニアを引き続き募集しています
最近は東京のみならず福岡にも支社できてるので興味ある方よかったら応募してみてください
最後まで読んでいただきありがとうございました
明日 3日目の執筆担当は @shoe116 です。引き続きお楽しみください