gRPC/Go で簡単なサーバーとクライアントを実装する

はじめに

本記事ではgRPCを利用したサーバーとクライアントを実装します。マイクロサービス開発などでよく使われるgRPCですが、ちゃんと調べたことがなかったので簡単なサーバとクライアントを実装してまとめました。
今回の例として使用したリポジトリGitHubにあります。

github.com

gRPCとは

gRPCはGoogleによって開発されたリモートプロシージャコールシステム(RPC System)です。 gRPCを使用するとクライアントがサーバーのメソッドを直接呼び出すことができます。マイクロサービスでは多くのサービスが存在し、それぞれのサービスを利用する必要があります。そこでgRPCを使用すると他のマイクロサービスのメソッドを容易に呼び出すことが可能になるという利点があります。

f:id:y-zumi:20190907004223j:plain

RPC(遠隔手続き呼び出し)とは

RPCとはクライアントがサーバーのルーチンをネットワークを介して呼び出すことです。RPCではクライアントとサーバーのインターフェースを統一して実装します。クライアントとサーバー間で手続きのインターフェースを提供する方法としてインターフェース記述言語(IDL)が使われます。

Protocol Buffersとは

Protocol Buffersはインターフェース記述言語(IDL)の一つです。gRPCはProtocol Buffersをデフォルトで使用しています。Protocol Buffersは記述がシンプルで、パースも高速です。Protocol BuffersはRPCのプロトコルを記述するだけでなく、データ構造の定義としても利用することができます。プロトコルやデータ構造は.protoというファイルで定義し、protocプログラムによりコンパイルされることで、各クライアントに適合されたコード(xxx.pb.go, xxx.pg.swiftなど)を生成します。
一つのプロトコルファイルから、各クライアントに対応したファイルを生成できる容易さにより、クライアントのプラットフォームが異なる場合でもRPCを実現可能となっています。公式ドキュメントには、Protocol BuffersとXMLの性能差を中心にProtocol Buffersの特徴が記載されています。

gRPCを使用してサーバーとクライアントを実装する

今回は例として 「UserをIDで検索するサーバーを実装し、クライアントからgRPC通信でUserを取得する」 までを実装します。

手順は以下のようになります。

  • Protocol Buffersによるインタフェースとメッセージの定義
  • gRPC Server の実装
  • gRPC Client の実装

準備

protoファイルからgo用のスキーマを生成するためにProtocol Buffersのコンパイラであるprotocをインストールする必要があります。

protocはhomebrewを使用して以下のコマンドでインストールできます。

> brew install protobuf
> protoc --version
libprotoc 3.9.1

Protocol Buffersによるインタフェースとメッセージの定義

まずは、Protocol Buffersを使用して以下のインタフェースとメッセージ構造を定義します。

  • サーバーが提供する機能のインタフェースを定義する
    • FindByID
  • リクエストとレスポンスのメッセージの構造を定義する
    • FindByIDRequest
    • FindByIDResponse
    • User

サーバーが提供する機能のインタフェースを定義する

サーバーが実際に提供するgRPCエンドポイントのインタフェースは以下のようになります。FindByIDがRPCとして定義されており、後述するFindByIDRequestFindByIDResponseを引数と戻り値に定義しています。FindByIDは、サーバー側で実態が実装され、クライアント側からコールしてレスポンスを取得する時のインターフェースとなります。

service Users {
    rpc FindByID (FindByIDRequest) returns (FindByIDResponse) {}
}

リクエストとレスポンスのメッセージの構造を定義する

リクエストとレスポンスは以下のようになります。FindByIDRequestUserのIDのみを定義しています。FindByIDResponseUserを所持しています。

message FindByIDRequest {
    string id = 1;
}

message FindByIDResponse {
    User user = 1;
}

message User {
    string id = 1;
    string name = 2;
}

protoファイルをコンパイルする

.protoファイルを定義したのでprotocコンパイルします。これにより、クライアントとサーバーで実際に使用されるデータアクセス用のクラスが生成されます。

コンパイルには以下のコマンドを使用します。コンパイル後にはuser.pb.goファイルが生成されているはずです。このコマンドはy-zumi/grpc-goで使用した時のものであるため、各自の環境ではでパス等を変更してください。

> protoc -I proto/ proto/user.proto --go_out=plugins=grpc:$GOPATH/src/github.com/y-zumi/grpc-go/proto/
> ls proto/
user.pb.go user.proto

Serverの実装

次に先ほど生成したuser.pb.goをもとにサーバーを実装します。
gRPC ServerにはUserService構造体が定義されており、UserServiceはポインタレシーバであるFindByIDを持っています。 FindByIDは引数にFindByIDRequestをとり、戻り値としてFindByIDResponseを返しています。

// UserService presents pb.UsersService
type UserService struct{}

// FindByID find user by user id
func (s *UserService) FindByID(ctx context.Context, req *pb.FindByIDRequest) (*pb.FindByIDResponse, error) {
    return &pb.FindByIDResponse{
        User: &pb.User{
            Id:   req.Id,
            Name: "Sample",
        },
    }, nil
}

サーバーのmain関数ではgRPC Serverの起動を行います。

func main() {
    // Start listening port
    lis, err := net.Listen("tcp", ":5001")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // Register UsersServer to gRPC Server
    s := grpc.NewServer()
    pb.RegisterUsersServer(s, &UserService{})

    // Add grpc.reflection.v1alpha.ServerReflection
    reflection.Register(s)

    // Start server
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

以上でサーバー側の実装は完了しました。次はクライアントの実装します。

Clientの実装

クライアントの実装は以下のようになります。
gRPCのコネクションを確立して、コネクションをもとにgRPC Clientを作成します。gRPC Clientでサーバー側にコールしてUserを取得しています。

func main() {
    // Set up a gRPC client
    conn, err := grpc.Dial("localhost:5001", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewUsersClient(conn)

    // Request to gRPC server
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.FindByID(ctx, &pb.FindByIDRequest{
        Id: "1",
    })
    if err != nil {
        log.Fatalf("could not find user: %v", err)
    }
    log.Printf("User: %v", r.User)
}

動作確認

サーバーとクライアントの実装が完了したので動作確認をします。
ターミナルを2つ起動して、片方でサーバーを起動し、もう一方でクライアントを起動します。クライアントを起動するとサーバーにコールしてUser情報がターミナル上に表示されます。

# terminal 1
> go run server/main.go

# terminal 2
> go run client/main.go
2019/09/06 22:15:16 User: id:"1" name:"Sample"

まとめ

普段の開発はHTTPSで実装する場合が多く、RPCで実装することがあまりないので、今回はgRPCを利用して簡単なサーバーとクライアントを構築しました。
本記事ではgRPCの基本的な使い方を紹介しましたが、今後の記事ではもう少し複雑な実装に踏み込んだものを書いていきます!

参考文献

grpc.io

developers.google.com

ja.wikipedia.org

Kubernetes Meetup #22 に参加してきました

はじめに

Kubernetes Meetup #22 にブログ枠として参加させていただきました!

今回は会場がほとんど満席でしたが、YouTube Liveで同時配信されているため、後でも確認できるところが良いところですね。
セッションは、 「もともとAWSを使っていてKubernetesに移行する」 という話が多く、GKEで1から構築したことしかない自分にとっては、移行時の苦労話などは新鮮でした。

このイベントレポートでは個人的に気になった部分のみをまとめます。

k8sjp.connpass.com

Kubernetes Meetup Tokyo とは

f:id:y-zumi:20190829015341p:plain

Kubernetes Meetup Tokyoは、Kubernetesユーザが集まり、KubernetesKubernetesを使ったソフトウェアについて情報交換、交流をするための勉強会となっています。勉強会の開催を通じて、Kubernetesのユーザが一堂に集まり、Kubernetesにまつわる様々な分野の知識や情報を交換し、新たな友人やコミュニティとのつながり、仕事やビジネスチャンスを増やせる場所とすることを目標としています。

AWS EKSを用いた開発環境の構築 和田 拓朗

開発検証用のマシンが一台しかない状態から、開発者ごとにServiceを作成して、ブロッキングすることなく開発を進められるようにしているのは素晴らしいと感じました。

Kubernetesを使うに至るまでの過程

  • Squadという組織体制で開発をしている
    • 複数のチームで同じAPIサーバーを並行して開発している
    • 開発サーバーが1つだけでデプロイ作業がブロッキングしていた

Kubernetesの導入で達成したこと

  • 開発者がそれぞれの開発環境で動作確認を簡単にできるようにした
    • 開発ブランチがPushされるとEKSにServiceが作成され、動作確認がしやすくなった
  • 開発環境のURLは一意にして、クライアントからEndpointを切り替えてアクセスできるようにした
    • https://feature-a.smartnews.xxx のようなドメインで受けて、Nginxで各Serviceに振り分けている

<スライド公開待ち>

はてなでのKubernetes利用の取組みとこれから 今井 隼人

導入から撤退までの話はとても貴重でした。知見として 「構築できるからと言って運用できるわけではないこと」「ドキュメントの充実・学習環境の整備」 などが挙げられていました。
これから既存のシステムをKubernetesに移行する方には、とても有益な情報だと感じました!

developer.hatenastaff.com

Kubernetesの導入

Kubernetes運用の課題

  • 学習コストの高さ、定期的なアップデートの対応、運用リソース不足が課題

徹底とこれから

  • 本番環境はロードバランサからKubernetesのNodePortを外すだけで撤退完了
  • 撤退したが導入時点の課題は解決する必要がある
    • VM→ECS→EKSというように移行していく

PayPay での Kubernetes 活用事例 小澤 真佐也

リソースがない中で、キャンペーンを柔軟な対応で捌いていました。短い期間でのリリースや、新施策の対応とKubernetes環境の整備を両立しているのはさすがだなと感じました。

100億円キャンペーンによる高負荷をKubernetesで捌く

  • アクセスの急増によりシステムが不安定な状況に
    • Deploymentを複製し、Read系とWrite系でEndpointを呼び分ける
    • Read系:Slave DBから読み取り
    • Write系:Master DBに書き込み
  • パフォーマンスチューニングの調査にコストがかかる
    • New Relic でメトリクスを可視化しボトルネックの発見を効率化した
  • 認証サービスに負荷が集中した
    • 認証サービスの認証機能とユーザー情報管理機能等を切り分けた
    • Deploymentを複製し、認証のアクセスを新しいServiceに向けた

<スライド公開待ち>

LT大会

LTはTipsとして有用なものが多くあったので紹介します!

Kubernetes実践ガイド」を書きました - hhiroshell

サイン付きのKubernetes実践ガイドを抽選していました!
自分も当選しましたが電子書籍と紙書籍の両方で持っていたのでパスしました。

Kubernetes上で単発のジョブを実行するkube-jobというツールを作った - h3_poteto

KubernetesのCronJobのデメリットを自作ツールでカバーしたことの紹介でした。

認証の仕組みとclient-go credential plugin - int128

認証の動作について詳しく説明されていてとても勉強になりました。

Amazon EKSでFluentBitを使ってKinesis Firehoseへログを転送する手法 - isao

軽量なFluent Bitを使ったログ転送の紹介でした。
Fluentd利用前提の方に対してのTipsとして良いと思いました。

k8sによるスケーラブルジョブ基盤構築 - kekekenta

スケールの問題に対してKubernetesをうまく使って解決できていたのは素晴らしいと感じました。

まとめ

他社でのKubernetesの運用の知見を聞くことができたのは良かったです。また、EKSが東京リージョンに対応していないときの話だったので、どのような技術選定をしてKubernetesの利用に至ったのかも知ることができました。

今回のセッション中でも知らないツールやライブラリがありましたが、Kubernetesのエコシステムはとても多く、名前は知っているけれど触ったことがないものがあるので、今後の記事でまとめられたらと思います!

Kotlin Fest 2019 に参加してきました

はじめに

Kotlin Fest 2019 に行ってきました。 自分が聞いたセッションはサーバーサイドやフロントエンドのKotlinネタが多く、KotlinがAndroid以外のPlatformでも使われてきているなと感じました。
本記事では自分が聴講したいくつかのセッションについてまとめたので簡単に紹介します!

Kotlin Festとは

Kotlin Festは日本Kotlinユーザーグループが主催しています。 「Kotlinを愛でる」をビジョンに、Kotlinに関する知見の共有と、Kotlinファンの交流の場を提供する技術カンファレンスです。
Android、Backend、FrontendでKotlinが使われることもあり、幅広い領域のエンジニアの方々が参加されています。 f:id:y-zumi:20190824182315p:plain

Kotlin コルーチンを理解しよう 2019 八木俊広

最初のセッションはKotlin1.1から登場したコルーチンについての説明でした。 セッションの中では、コルーチンの概要から、テスト手法、利用時の考え方まで広範囲をカバーしていました。 また、コルーチンの実行スレッドに関する説明も図を交えており、とてもわかりやすかったです。

  • コルーチンの良いところ
    • スレッドより軽量でスレッド内に複数のコルーチンを実行することができる
  • 並行処理をしたいときは分岐と合流の範囲を構造化し、処理とエラーの範囲を決める
    • 親スコープで並行処理を行った場合、エラーが発生すると親スコープ全体でのエラーとなりアプリケーションがクラッシュする
    • 親スコープ内に子スコープを作ることで、エラーの影響が子スコープ内にとどまりエラーハンドリングができるようになる

今からはじめる Android Kotlin あんざいゆき

このセッションは初級者向けに、ツールや開発のTipsなどを話されていました。 初心者向けということで、とても丁寧でわかりやすいセッションでした。 今から新規でAndrodアプリを作る方は積極的にKotlinを使っていきましょう 💪

サーバサイドKotlinでGraphQLをやってみよう 磯貝佳典

GraphQLの基本的なスキーマの説明から、Kotlinでの具体的な実装、デモまでテンポよく説明されていたのでとても聞きやすいセッションでした!
会場ではServerside Kotlinを使用した経験のある人は半数程度いましたが、Kotlin+GrapghQLを使用した経験のある人は数名しかいませんでした。

まとめ

今回はKotlin Fest 2019に参加してきました。大学のときはAndroidでKotlinを触っていたのですが、数年経つとAndroid以外にもBackendやFrontendで使われていることが印象的でした。
カンファレンスの進行はスムーズでゆとりのあるスケジュールだったので、気ままに色々なセッションを見ることができたので良かったです。

次回があれば、LT枠などで参加できればと思います!

Kubernetesの内部アーキテクチャについて整理してみた

はじめに

Kubernetes の勉強を始めた頃は Pod や Deployment などのリソースに目が行きがちですが、内部でどのような動きをしているのかを知ることも重要になってきます。
そこで、本記事では Kubernetes のコアな部分に絞って説明します。

Kubernetes Cluster とは

Kubernetes には KubernetesCluster というものがあります。 KubernetesCluster とは MasterNode と WorkerNode の集合のことを指します。
簡単に説明すると、 MasterNode は KubernetesCluster の管理を行い、 WorkerNode は Container の実行などを行います。以下の図ではクラスタ内に MasterNode が1台と、WorkerNode が3台が存在しています。

f:id:y-zumi:20190818225225j:plain
Kubernetes Cluster の簡略図

Master Node とは

MasterNode は ControlPlane とも呼ばれています。 MasterNode は KubernetesCluster に関する主要な処理を担っています。 Pod のスケジューリングや、 Kubernetes のリソースの状態の管理などが該当します。
MasterNode は Cluster を管理するために、以下の4つのコアとなるコンポーネントを含んでいます。

  • kube-apiserver
  • etcd
    • リソースの情報を保持するKey Value Store
  • kube-scheduler
    • Podなどを適切なNodeにスケジューリングする
  • kube-controller-manager
    • DeploymentやPodなどを設定したリソース状態に保つ

以下の図では簡単にそれぞれのコンポーネントの関係図を示しています。kube-apiserver が kube-scheduler と kube-controller-manager からリクエストを受け付けて、 etcdに何らかのCRUD操作を行っています。

f:id:y-zumi:20190818230751j:plain
MasterNodeのコンポーネント関係図

kube-apiserverについて

kube-apiserverはKubernetesへのAPIリクエスト(REST)を受け付けるコンポーネントです。開発者が行う kubectl を使ったリクエストも、kube-apiserverがリクエストを受け付けて処理を行っています。
リクエストを受けたkube-apiserverは、基本的にetcdのCRUD操作を行いリソースの情報を変更します。
以下の図のように、kube-apiserverはKubernetesClusterの各コンポーネントからのリクエストも受け付けるため、リクエストはkube-apiserverに集約されます。

f:id:y-zumi:20190818231359j:plain
kube-apiserverの関係図

etcd

etcd は一貫性のあるデータ格納を重視しているキーバリューストアです。すべてのリソース情報(Pod, Deployment など)と、Cluster の状態などは etcd に保存されます。
etcd へのアクセスは kube-apiserver で行うため、他のコンポーネントは kube-apiserver を介して間接的に etcd を変更することになります。

kube-scheduler

kube-scheduler は Pod が新たに作成された時に適切な WorkerNode に割り当てる役割をもっています。実際の動きの例として以下があります。

  1. kubectl から Deployment の作成のリクエストを受けたkube-apiserverがDeploymentを作成
  2. kube-apiserverから通知を受けて、DeploymentController が ReplicaSet を作成
  3. 同様に通知を受けて、ReplicaSetController が Pod を作成
  4. kube-scheduler が Podを適切なWorkerNodeにスケジューリング

3.の段階で作成された Pod はどの Node にも割り当てられていません。4.で kube-scheduler が適切な WorkerNode を選択し、pod.nodeName を更新します。Pod の更新を Node の kubelet が検知し、割り当てられた Node は kubelet を利用して Pod を作成するようになっています。

f:id:y-zumi:20190818234712j:plain
scheduling の流れ

kube-controller-manager

kube-controller-managerのコントローラはリソースを目的の状態(etcdに保存されているリソースのspec情報)にする役割を持っています。コントローラはほとんどのリソースに対して存在しています。一部を以下に列挙します。コントローラはkube-apiserverを介して各リソースの状態をウォッチしており、異常があれば新しいオブジェクトの作成や既存のオブジェクトの更新、削除を行います。

  • ReplicaSet controller
  • Deployment controller
  • Node Controller
  • others...

Worker Nodeとは

WorkerNodeはコンテナやサービスの実行と管理をおこなっています。WorkerNodeは以下の2つのコンポーネントを含んでいます。

  • kubelet
  • kube-proxy

kubelet

kubeletは実行中のPodを常に監視しており、ステータスやリソース消費などをkube-apiserverに送信する役割を持っています。また、Podの起動も担っており、DockerEngineなどを利用してコンテナイメージからコンテナを実行する命令なども行っています。

kube-proxy

kube-proxyはクライアントからのアクセスに対するルーティング機能の役割を持っています。現在のKubernetesのデフォルトの設定では、 iptables をもとにトラフィックをPodに正常に転送されるようにしています。

まとめ

今回はKubernetesアーキテクチャについての概要を説明しました。
コア部分のみの説明となりましたが、実際にKubernetesを利用した記事も書いていこうと思います。

参考文献

kubernetes.io

【Go】go-cmpを利用した構造体のテスト手法

はじめに

今回はGoでの構造体の比較について、 google/go-cmp を利用したテスト手法について紹介します。

今回のブログで使用したリポジトリ github.com

google/go-cmpとは

go-cmp とは値が等価かどうかを判別する package です。
等価を確認する方法に ==reflect.DeepEqual が存在しますが、go-cmpはこれらより安全で強力な手法として紹介されています。

go-cmpが提供する機能は以下の通りです。

  • 基本的な比較演算子ではない、カスタマイズされた等価機能を利用できる
  • 型に Equal メソッドがある場合、比較時に Equal メソッドを使用する
  • Equal メソッドがない場合、再帰的にプリミティブ型の値を比較する

再帰的な値の比較は reflect.DeepEqual と動作が似ていますが、go-cmp ではデフォルトでは非公開なフィールド (Unexported Field) を比較しません。
その代わりに cmpopts.IgnoreUnexportedAllowUnexported を使用して、非公開なフィールドを比較するか無視するかをデベロッパーが決めることができます。

go-cmpで構造体のテストを実装する

まず、比較の対象に使われる構造体として Item を定義します。
この構造体は全て公開されたフィールド (exported field) となっています。

type Item struct {
    ID   string
    Name string
}

次にテスト対象の関数として GetItem() を定義します。
実装は構造体を生成して返すだけの単純なものです。

func GetItem(id, name string) Item {
    return Item{
        ID:   id,
        Name: name,
    }
}

最後に go-cmp を使ってテストコードを実装します。
実装内容としては cmp.Diff に比較したい2つの値を渡すと、差分が返ってきます。差分がある場合は、2つの値は等価ではないということになるため、テストを失敗させています。差分がない場合は、等価であるためテストが成功します。

got := GetItem(tc.args.id, tc.args.name)
if diff := cmp.Diff(got, tc.want, opt); diff != "" {
    t.Fatalf("GetItem() = %v, want = %v", got, tc.want)
}

Unexported field を持つ構造体の場合

次に非公開なフィールド (unexported field) を持つ構造体のテストをしてみます。

まず、構造体に非公開のフィールドを追加する必要があるので ItemsecretCode を追加します。secretCode は非公開なフィールドであるためフィールド名の先頭が小文字になります。

type Item struct {
    ID         string
    Name       string
    secretCode int64
}

非公開フィールドを比較する場合(AllowUnexported)

テストをする際に unexported field を比較対象にするには AllowUnexported に非公開なフィールドを持つ構造体を渡します。
今回の例の場合 Itemを渡すとsecretCodeが比較の対象に含まれます。 Unexported fieldに対して比較対象にするか除外するかを設定しない場合、panicとなりテストが実行できません。

got := GetItem(tc.args.id, tc.args.name)
opt := cmp.AllowUnexported(got)
if diff := cmp.Diff(got, tc.want, opt); diff != "" {
    t.Fatalf("GetItem() = %v, want = %v", got, tc.want)
}

非公開フィールドを除外する場合(cmpopts.IgnoreUnexported)

比較対象から除外したい場合はcmpopts.IgnoreUnexportedを使用します。これで非公開なフィールドを無視して安全な比較を行うことができます。

got := GetItem(tc.args.id, tc.args.name)
opt := cmpopts.IgnoreUnexported(got)
if diff := cmp.Diff(got, tc.want, opt); diff != "" {
    t.Fatalf("GetItem() = %v, want = %v", got, tc.want)
}

【補足】reflect.DeepEqualでUnexported fieldを比較した場合

reflect.DeepEqual は unexported fieldをデフォルトで比較対象にしているので、テストは何事もなく通ってしまいます。そのため、デベロッパーが意図しない比較が行われる場合があり安全ではありません。

got := GetItem(tc.args.id, tc.args.name)
if !reflect.DeepEqual(got, tc.want) {
    t.Fatalf("GetItem() = %v, want = %v", got, tc.want)
}

まとめ

今回はgo-cmpによる構造体のテスト方法を紹介しました。reflect.DeepEqualでは非公開フィールドも比較してしまい予期せぬ結果をまねくことも考えられます。 そのため、今回紹介した go-cmp で構造体のテストをすることをおすすめします!

参考文献

godoc.org golang.org

DatadogをDaemonSetでデプロイする on GKE

はじめに

最近Kubernetesの勉強をしています。 Deploymentリソースはよく使うのですが、DaemonSetリソースは使う機会が少なかったため、本記事ではDaemonSetの利用例としてGKE+Datadogを紹介します。

Datadogについて

Datadogはサーバなどのモニタリング(監視)ができるサービスです。 今回紹介する例ではすべてのNodeにDatadogを常駐させることで、モニタリングを実現しています。

各Nodeに必ずDatadogのPodが存在していないといけないため、DeploymentではなくDaemonSetというリソースが好ましいとされています。

f:id:y-zumi:20190804205246j:plain
Worker Nodes に Datadog-Agent がデプロイされている状態

DaemonSetとは

DaemonSetはすべてのNodeにPodを配置するリソースです。以下のような特徴があります。

  • 各Nodeに1つずつPodが配置されることを保証します
  • Nodeに対して2つ以上のPodを配置することはできません
  • 新しいNodeが追加されるたびに、新しいPodをNodeに追加します
  • DaemonSetを削除した場合は、DaemonSetによって作成されたPodはすべて削除されます

DatadogでNodeを監視する場合は、各NodeにPodを作成しなければいけないので、DaemonSetとの相性が良いです。

【準備】GKE上にKubernetesClusterを作成

まず、GKE上にKubernetesClusterを作成します。

> gcloud container clusters create k8s \
  --cluster-version 1.12.8-gke.10 \
  --zone asia-northeast1-a \
  --num-nodes 3

Datadog-Agentの作成

Datadogのチュートリアルドキュメントに従って試していきます。
Datadogのアカウントがない場合は作成してください。

Datadog Get started

モニタリングをするためには、Datadog-Agentを各ノードにDeployする必要があります。
DatadogでNodeモニタリングするために必要なリソースは以下になります。

  • Role Based Access Controlに使用するリソース
    • ClusterRole
    • ServiceAccount
    • ClusterRoleBinding
  • DatadogのAPI_KEYを作成するリソース
    • Secret Generic
  • 各NodeにPodをデプロイするためのリソース
    • DaemonSet

それらを作成するコマンドは以下になります。

# Role Based Access Controlに使用するリソース
> kubectl apply -f "https://raw.githubusercontent.com/DataDog/datadog-agent/master/Dockerfiles/manifests/rbac/clusterrole.yaml"
> kubectl apply -f "https://raw.githubusercontent.com/DataDog/datadog-agent/master/Dockerfiles/manifests/rbac/serviceaccount.yaml"
> kubectl apply -f "https://raw.githubusercontent.com/DataDog/datadog-agent/master/Dockerfiles/manifests/rbac/clusterrolebinding.yaml"

# DatadogのAPI_KEYを作成するリソース
> kubectl create secret generic datadog-secret --from-literal api-key="<チュートリアルに記載されているKEYを入れてください>"

# 各NodeにPodをデプロイするためのリソース
> touch datadog-agent.yaml #チュートリアルにあるdatadog-agent.yamlをコピーする
> kubectl apply -f datadog-agent.yaml

次に、Role Based Access Controlについて簡単に説明していきます。

【解説】Role Based Access Control(RBAC)とは

RBACはアクセス制御をする方法の一つです。 ネットワークやリソースの制限をするRoleを各Userに割り当てることでアクセス制御を行っています。

RBACにはRole、User、RoleBindingの3つが使用されます。

  • Role
    • リソースやエンドポイントのアクセス権限の設定を記述します
  • User
    • ServiceAccountなどを指します
  • RoleBinding
    • Userに対して任意のRoleを紐付けてアクセス権限を付与します

以下の図では、DatadogとFluentdに特定のClusterRoleを紐付けています。

f:id:y-zumi:20190804205447j:plain
Role Based Access Control の簡易図

ClusterRoleの作成

> kubectl apply -f "https://raw.githubusercontent.com/DataDog/datadog-agent/master/Dockerfiles/manifests/rbac/clusterrole.yaml"
clusterrole.rbac.authorization.k8s.io/datadog-agent created

clusterrole.yamlをみてみると、rules内に利用できるリソースとそれに対する操作などが記述されていることがわかります。

piVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: datadog-agent
rules:
- apiGroups:
  - ""
  resources: # アクセスできるリソース一覧
  - services
  - events
  - endpoints
  - pods
  - nodes
  - componentstatuses
  verbs: # リソースに対して行うことができる操作
  - get
  - list
  - watch
# 省略

ServiceAccountの作成

> kubectl apply -f "https://raw.githubusercontent.com/DataDog/datadog-agent/master/Dockerfiles/manifests/rbac/serviceaccount.yaml"
serviceaccount/datadog-agent created

serviceaccount.yamlはシンプルでdatadog-agentという名前だけが定義されています。

kind: ServiceAccount
apiVersion: v1
metadata:
  name: datadog-agent
  namespace: default

ClusterRoleBindingの作成

> kubectl apply -f "https://raw.githubusercontent.com/DataDog/datadog-agent/master/Dockerfiles/manifests/rbac/clusterrolebinding.yaml"
clusterrolebinding.rbac.authorization.k8s.io/datadog-agent created

RoleBindingでServiceAccountにRoleを付与しています。
roleRefでRoleを指定し、subjectsで任意のUserを指定しています。Userは複数指定することが可能ですが、今回はdatadogだけでなので一つとなっています。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: datadog-agent
roleRef: # Roleの指定
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: datadog-agent
subjects: # Userの指定
- kind: ServiceAccount
  name: datadog-agent
  namespace: default

【解説】Datadog-Agentのデプロイ

Secret - Generic

デプロイする前に、API_KEYが必要なのでSecretを作成します。
今回は簡易的に--from-literalオプションでkubectlに直接値を渡して作成しています。

> kubectl create secret generic datadog-secret --from-literal api-key="<チュートリアルに記載されているKEYを入れてください>"
secret/datadog-secret created

DaemonSetによるデプロイ

> touch datadog-agent.yaml #チュートリアルにあるdatadog-agent.yamlをコピーする
> kubectl apply -f datadog-agent.yaml
daemonset.extensions/datadog-agent created

datadog-agent.yaml公式ドキュメントにあります。
datadog-agent.yamlはDaemonSetを使用しており、ServiceAccountとSecretを指定しています。

apiVersion: extensions/v1beta1
kind: DaemonSet # DeploymentではなくDaemonSetで作成
metadata:
  name: datadog-agent
spec:
  template:
    metadata:
      labels:
        app: datadog-agent
      name: datadog-agent
    spec:
      serviceAccountName: datadog-agent # Roleでアクセス権限を設定したServiceAccount
      containers:
      - image: datadog/agent:latest
        env:
          - name: DD_API_KEY
            valueFrom:
              secretKeyRef:
                name: datadog-secret # 作成したSecretのname
                key: api-key
# 省略

以上の操作でDatadogでモニタリングをするために必要な作業は完了しました。 最後にDatadogにログインしてNodeの状態を確認しましょう。

Datadog上で確認

Dashboard -> Kubernetes Overview でNodeの状態を確認することができます。

f:id:y-zumi:20190804230909p:plain
datadogによるmonitoring

まとめ

今回はGKE上のNodeをDatadogでモニタリングする手順を解説しました。
Datadogを試すだけだとチュートリアルに従えばできますが、リソース周りは自分で調べて整理する必要があったので記事を書いてみました。

Kubernetesは覚えることが多いので今後も記事を書いていこうと思います。

参考文献

Datadog Get started

Kubernetes 完全ガイド

kubernetes.io

GoのInterfaceの作法 "Accept Interfaces, Return structs"

はじめに

Goにおける抽象化の方法にインターフェースがあります。インタフェースをうまく使うことにより具体的な実装を隠し、疎結合な実装ができるようになります。
これにより、テストがやりやすくなったり、リファクタリングの影響範囲を小さくできるなどのメリットがあります。インターフェースがないコードは振る舞いで共通化することができないため、冗長かつ密結合なシステムになってしまいます。

今回はGoのインターフェースの基本的な考え方である "Accept interfaces, Return structs" について紹介します。

"Accept interfaces, Return structs" とは

直訳で 「インターフェースを受け入れて、構造体を返す」 ですが、この考え方はJack Lindamood氏が過去に提案したものです。
GoのCodeReviewのドキュメントには以下のように書かれています。

Go Code Review Comments - Interfaces
Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types.
(中略)
Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.

訳してみると次のようになります。
「一般的にGoのInterfaceは、構造体などの値を実装するpackageではなく値を使用するpackageに属します。値を実装しているpackageは、通常はポインターや構造体型を返す必要があります。 構造体などの値が他のpackageから使用される前に実装に対するInterfaceを定義しないでください。まだ使われていないときに実装したInterfaceが必要であるかは判断が難しいためです。」

簡単に箇条書きで整理してみます。

  • インターフェースは構造体を利用する側のパッケージに定義する
  • 引数をインターフェースで抽象化して受け取る
  • NewXxxx()などをするときは構造体やポインタなどの実態を返す
  • 構造体が使われる時になってからインターフェースを定義する

サンプル

以上を踏まえた上で、擬似コードを書いて感覚を掴んでいきます。今回のサンプルでは、以下の図にあるDatabaseMainInterfaceの3つを実装してみます。(APIはインタフェースによる共通化の例なのでコードでは紹介しません)

f:id:y-zumi:20190728023343j:plain

Database

ここではInterfaceを定義せず、Databaseの構造体と初期化処理、各種メソッドを定義しています。また、初期化処理のNewBookDBClientでは実態である構造体のポインタを返しています。

type BookDB struct {}

func NewBookDBClient() *BookDB {
  return new(BookDB)
}

// write系
func (*d BookDB) Add(book Book) { ... }
func (*d BookDB) Remove(id string) { ... }

// read系
func (*d BookDB) GetByID(id string) *Book { ... }
func (*d BookDB) GetByTitle(title string) *Book { ... }

Interface & Main

今回のmainパッケージで要求されるユースケースIDから本を検索するとしましょう。この場合、InterfaceはGetByID(string) *Bookのみを定義し、インターフェース分離の原則に則って、必要のない関数には依存しないようにします。これにより、Write系とID検索以外のRead系のメソッドは、このパッケージからは利用できないようになります。

// read系のGetByIDのみが使える
type BookFetcher interface {
  GetByID(string) *Book
}

func main() {
  fetcher := db.NewBookDBClient()
  bookID := "12345"

  book := SearchBook(fetcher, bookID)
}

// 引数をInterfaceにすることで、不必要な関数に依存しなくなる
func SearchBook(fetcher BookFetcher, bookID string) *Book { 
  return fetcher.GetByID(bookID)
}

まとめ

今回のサンプルで分かった "Accept Interfaces, Return structs" の良い点は以下のようなものがあります。

  • Interfaceで要求する関数が簡潔に定義されるため、mainでは必要のない関数について考えなくてもよくなる
  • Databaseのパッケージで新しい関数を追加しても、mainパッケージへの影響がない(今の実装の場合)

GoのInterfaceは軽量でとても使い勝手が良いのでちゃんと使っていきたいですね

参考文献

github.com

medium.com

qiita.com