この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
調査する機会があったので、ブログにまとめてみました。
gRPCでは、データ通信にProtocol Buffersとよばれるシリアライゼーション形式を利用します。
Protocl Buffersは、データのシリアライゼーション形式として知られる一方で、シリアライズするためのスキーマ定義用として.proto
ファイルというインタフェース定義言語(IDL)を持っています。
実際の開発においては、この.proto
ファイルを先に作成することでスキーマファーストな進め方ができるのが大きな特徴です。
今回は、.proto
ファイルの作成~認可処理の実装~AWS環境へのデプロイまで一気通貫で紹介したいと思います。
せっかちな人へ
GitHubリポジトリにすべて上げています。
構成図
ディレクトリ構成
$ tree -L 2
.
├── Dockerfile
├── go.mod
├── go.sum
├── proto
│ ├── gen
│ └── helloworld.proto
├── README.md
├── server
│ ├── auth
│ ├── server.go
│ └── server.go.back1
└── terraform
├── acm.tf
...
- proto
.proto
ファイルの保存先
- server
- サーバー側のソースコード保存先
- terraform
- AWSデプロイ用のTerraformのコード保存先
.protoファイルの作成
まず.proto
ファイルを作成していきます。
今回は、公式のサンプルを流用します。
- helloworld.protoファイルの作成
proto/helloworld.proto
proto:helloworld.proto
syntax = "proto3";
option go_package = "gen;gen";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
.proto
ファイルからインターフェースとなるGoのコードを自動作成
protoc -Iproto --go_out=plugins=grpc:proto proto/*.proto
gRPCサーバーを作成
gRPCサーバーのソースコードを書いていきます。
公式のサンプルを参考に進めます。
- Go version
$ go version
go version go1.15.5 linux/amd64
- 初期化
go mod init helloworld
- モジュールの取得
go get -u google.golang.org/grpc
- コード
server/server.go
package main
import (
"context"
"log"
"net"
pb "helloworld/proto/gen"
"google.golang.org/grpc"
)
const (
port = ":50051"
)
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
ローカルサーバー上での動作確認
サーバーをローカルで起動し、grpcurlというツールを使って動作確認をしていきます。
- サーバー起動
go run server/server.go
- リクエストを送信
$ grpcurl \
-plaintext \
-d '{"name": "arai"}' \
-import-path proto \
-proto helloworld.proto \
localhost:50051 helloworld.Greeter/SayHello
{
"message": "Hello arai"
}
Auth0にAPIを登録する
Auth0による認可機能を追加するため、Auth0ダッシュボードからAPIの登録を行っていきます。
- Auth0のダッシュボードよりAPIの作成
※ Identifier
にはAPIのエンドポイントURLを入力することが推奨されていますが、今回は適当に入力します。
- ClientIDを控えておきます
- アクセストークンも控えておきます
インターセプターに認可処理を追加する
Goのインターセプターの機能を使って、認可処理を実装していきます。
Auth0では、アクセストークンがjwt形式で提供されるためサーバー側で下記の処理が必要になります。(細かな点は公式ドキュメントを確認してください)
- 一般的なJWTの検証
aud
クレームがダッシュボードで指定したAPIのIdentifier
と一致しているか確認scope
クレームが設定されている場合は、適切なアクセスコントロールの処理
今回のケースでは、1と2をおこなう必要があります。
いい感じのモジュールがないかと調べましたが、結論から言うと本番では自前実装するの必要がありそうです。
今回は簡易的に、Auth0 Communityの方で作成されているauth0-goモジュールを利用しますが、Issueにもある通り今後メンテナンスする予定はなさそうです。
※ 本番利用を検討する際は、auth0-goやgo-jwt-middlewareのコードを参考に開発するのが良さそうです。
- モジュールの取得
go get -u github.com/grpc-ecosystem/go-grpc-middleware
go get github.com/auth0-community/go-auth0@b9b0f95be5688e006bed4a55a7aae9145cfc0370
# ログ用にzapも入れておく
go get -u go.uber.org/zap
- コード修正
server/server.go
package main
import (
"context"
"log"
"net"
pb "helloworld/proto/gen"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
auth0 "github.com/auth0-community/go-auth0"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
)
const (
port = ":50051"
// TODO: fill
auth0_url = "<your_auth0_url>"
audience = "<your_audience>"
)
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName() + " your id is " + ctx.Value("userId").(string)}, nil
}
func auth(ctx context.Context) (context.Context, error) {
token, err := grpc_auth.AuthFromMD(ctx, "Bearer")
if err != nil {
return nil, err
}
parsedToken, err := jwt.ParseSigned(token)
if err != nil {
return nil, grpc.Errorf(codes.Unauthenticated, "Cannot parse token because of", err)
}
client := auth0.NewJWKClient(auth0.JWKClientOptions{URI: "https://" + auth0_url + "/.well-known/jwks.json"}, nil)
configuration := auth0.NewConfiguration(client, []string{audience}, "https://"+auth0_url+"/", jose.RS256)
validator := auth0.NewValidator(configuration, nil)
if err := validator.ValidateToken(parsedToken); err != nil {
return nil, grpc.Errorf(codes.Unauthenticated, "Cannot validate token because of", err)
}
claims := make(map[string]interface{})
validator.Claims(parsedToken, &claims)
return context.WithValue(ctx, "userId", claims["sub"]), nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
zapLogger, err := zap.NewProduction()
if err != nil {
panic(err)
}
grpc_zap.ReplaceGrpcLogger(zapLogger)
s := grpc.NewServer(
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
grpc_zap.UnaryServerInterceptor(zapLogger),
grpc_auth.UnaryServerInterceptor(auth),
),
),
)
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
※ auth0_url
にはxxx.auth0.com
のようなテナントURL、audience
には先ほど控えたAPIのIdentifier
を入力してください。
Dockerで起動
今度はDockerコンテナ上で起動し、動作確認していきます。
- Dockerfileの作成
Dockerfile
FROM golang:1.15.5 as builder
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
WORKDIR /build
COPY . .
WORKDIR /build/server
RUN go build
FROM alpine:3.12.1
COPY --from=builder /build/server/server /opt/app/
ENTRYPOINT [ "/opt/app/server" ]
docker build -t go-grpc-server -f Dockerfile .
docker run --rm --name go-grpc -p 50051:50051 go-grpc-server
- 動作確認
$ grpcurl \
-plaintext \
-H "Authorization: Bearer <your-access-token>" \
-d '{"name": "arai"}' \
-import-path proto \
-proto helloworld.proto \
localhost:50051 helloworld.Greeter/SayHello
{
"message": "Hello arai your id is xxxxxxxxxx@clients"
}
※ <your-access-token>
は先ほど控えておいたアクセストークンに変えてください
AWSリソースの作成
Terraformを使ってAWSリソースを作成していきます。
本筋とは関係ないため、細かい部分は省きます。気になる方はGitHubリポジトリのREADMEを見てください。
イメージのプッシュ
docker-push.sh
を作成し実行
docker-push.sh
#!/usr/bin/env bash
# Version=latest
ACCOUNT_ID=$(aws sts get-caller-identity --output text --query 'Account')
ECR_PREFIX="${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com"
# echo $Version
echo $ACCOUNT_ID
aws ecr get-login-password | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com
# go grpc server
docker tag go-grpc-server:latest ${ECR_PREFIX}/go-grpc-server-test-ecr-repo/go-grpc-server:latest
docker push ${ECR_PREFIX}/go-grpc-server-test-ecr-repo/go-grpc-server:latest
AWS上での動作確認
- 動作確認
$ grpcurl \
-H "Authorization: Bearer ${access_token}" \
-d '{"name": "arai"}' \
-import-path proto \
-proto helloworld.proto \
go-grpc-server.cm-arai.com:50051 helloworld.Greeter/SayHello
{
"message": "Hello arai your id is xxxxxxxxxx@clients"
}
- ログ
まとめ
いかがだったでしょうか。
どなたかの役にたてば幸いです。