Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

Microservicesにジョインするには知らない技術が多すぎたので一通り触ってみた話

Mercari Advent Calendar 2018 の2日目はCrossUXチームの@mkazutaka(twitterは@makazutaka)がお送りします

昨日のアドベントカレンダーに@stanakaさんが取り上げているようにメルカリではMicroservices化に向けて開発が進んでおります。その流れに乗るように前QまでPHPを使って開発していた自分も今QからMicroservicesで実現されているサービスでの開発を行っております

tech.mercari.com

メルカリではMicroservicesの実現にあたってGoogleCloudPlatform(GCP)、Terraform、Docker、Kubernetes、Halyard、gRPC、Goといったさまざまなサービスからフレームワーク、言語を利用しています

メルカリでのMicroservices上での開発をする以上、これらに対して多少なりとも理解が必要です

本記事ではメルカリでのMicroservices上で使われてるサービスを理解するため自分が行ったTerraformによるGCPプロジェクトの構築からSpinnakerによる自動デプロイまでの方法を紹介します

履歴

12/03 [追記]ServiceAccountをTerraformで作成する修正及びSpinnaker欄を追加しました

Spinnakerについては下記の記事もご参照ください!

tech.mercari.com

tech.mercari.com

本記事では、下記のすべてが実現しているプロジェクトを作ることをゴールとします

  • 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を設定します。 f:id:mkazutaka:20181203083507p:plain:w300

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を押し、下記画像のように入力してください

f:id:mkazutaka:20181203084318p:plain:w300

次に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 TriggersAddし値を入力します
基本的に、このあたりは少しバージョンは古いですが公式ドキュメントをご覧ください。

Triggerは下記のように設定します
f:id:mkazutaka:20181203084457p:plain:w300

次に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が自動で実行されます
f:id:mkazutaka:20181203085426p:plain:w600

確認します

||*'-') <  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同士でやりとりも必要...) それでも新しい技術に触れ動かすことができるというのは楽しいものであり、またこれを実際に深い部分まで理解してメルカリ上で動かしているチームがいるというのも非常に尊敬するところであります

弊社では、新しい技術を学ぶことを楽しめるエンジニアを引き続き募集しています 最近は東京のみならず福岡にも支社できてるので興味ある方よかったら応募してみてください

careers.mercari.com

最後まで読んでいただきありがとうございました

明日 3日目の執筆担当は @shoe116 です。引き続きお楽しみください