Skip to main content

gRPCでセキュアなAPIを構築する

著者
Headshot of Vitalis Ogbonna

Vitalis Ogbonna

hero build api grpc

2022年8月25日

0 分で読めます

Googleのリモートプロシージャコール(gRPC)は、リモートプロシージャコール(RPC)フレームワークのGoogleによるオープンソース版です。HTTP/2とProtocol Buffers(protobuf)技術を活用した通信プロトコルです。gRPCを使うと、リモートのクライアントやサーバーは、受信側サーバーの関数をローカルにあるかのように呼び出すだけで、そのサーバーと通信できます。これにより、分散システムでクライアントとサーバー間の通信や大規模なデータセットの転送が簡単になります。

他のRPCシステムと同様に、gRPCではサービスを定義します。サービスのメソッドと戻り値の型は、Googleのシリアライズ/デシリアライズプロトコルであるprotobufを使って指定します。これにより、サービスを簡単に定義し、クライアントライブラリを自動生成できます。gRPCでは現在バージョン3のこのプロトコルを、インターフェース定義言語およびシリアライズツールセットとして使用します。

ほとんどの最新アプリケーションにとって、gRPCはあらゆるデータ型を優れた形でサポートする選択肢です。ストリーミングデータなど、大量のデータを扱う用途に最適です。一方、大規模なデータ転送があまり重要でないシンプルなアプリケーションでは、オーバースペックになる可能性があります。

この記事では、2つのNode.jsアプリケーション間でクライアントとサーバーのような通信を行い、gRPCを使用する方法を紹介します。また、サービスの通信手段としてgRPCを使う際の安全対策についても解説します。

チュートリアルの前提条件

このチュートリアルを進めるには、PCにOpenSSLとNode.js(バージョン4.0以降)がインストールされている必要があります。Node.jsとJavaScriptの基本的な知識も必要です。また、作業環境で管理者権限があることを確認してください。

Node.jsプロジェクトのセットアップ

まず、アプリケーションのフォルダー構成を設定します。event-app-node-grpcというフォルダーを作成し、次のコマンドを入力してnpmでNode.jsプロジェクトを初期化します。

#bash
$ mkdir event-app-node-grpc
$ cd event-app-node-grpc
$ npm init -y

アプリケーションを初期化したら、次のフォルダー構成を作成します。このチュートリアルで使用する動作するコード一式はGitHubで確認できます。

Event-app-node-grpc
client
app.js
index.js
server
index.js
scripts
generate-certs.sh
events.proto
README.md

パッケージのインストール

ターミナルでアプリケーションのルートディレクトリに移動します。次のコードスニペットのように、npm installコマンドを使って以下のパッケージをインストールします。

#bash
$ npm install express @grpc/grpc-js @grpc/proto-loader

先ほどのコードスニペットでインストールしたパッケージを確認しましょう。

  • Expressは、アプリケーションのHTTPサーバーです。

  • @grpc/grpc-jsはNode.js用のgRPCライブラリです。Node.jsランタイムでgRPCサービスを作成できます。

  • @grpc/proto-loaderは、gRPCで使用するprotobufファイルの読み込みに必要なパッケージです。protobuf.jsのバージョン3パッケージを使用します。

上記のパッケージをインストールしたら、package.jsonファイルを開き、次のコードスニペットのようにscriptsタグへ追加の設定を記述します。

#package.json

"scripts": {
   "start": "node server/index.js",
   "generate:certs": "./scripts/generate-certs.sh"
 },

上のコードスニペットに示した追加設定は、アプリケーションの実行時設定とSSL証明書の生成に使用します。これらの設定を追加すると、更新後のpackage.jsonファイルは次のコードスニペットのようになります。

#package.json(updated)

{
 "name": "event-app-node-grpc",
 "version": "1.0.0",
 "description": "A CRUD application to demonstrate the use of gRPC with NodeJS",
 "main": "server/index.js",
 "scripts": {
   "start": "node server/index.js",
   "generate:certs": "./scripts/generate-certs.sh"
 },
 "author": "(author name here)",
 "license": "ISC",
 "dependencies": {
   "@grpc/proto-loader": "^0.6.12",
   "express": "^4.18.1",
   "@grpc/grpc-js": "^1.6.12
 }
}

上のコードスニペットは、アプリケーションの実行時設定とSSL証明書の生成コマンドをscriptsタグに追加した後のpackage.jsonファイルです。

Protocol Buffersの定義

このチュートリアルでは、シンプルなイベント追跡アプリケーションでgRPCを使う方法を紹介します。このデモアプリケーションでは、イベントの詳細を受け取り、インメモリデータベースに保存します。また、イベントデータの更新、取得、削除もできます。

gRPCアプリケーションでは、異なるアプリケーション間の通信を可能にするため、サービスインターフェースと必要なペイロードをprotobufファイルに記述します。プロジェクトのセットアップスキーマに示すように、protobufファイルの拡張子は.protoです。

次に、アプリケーションのルートディレクトリにevents.protoファイルを作成し、以下のコードを追加します。参考として、先ほど定義したプロジェクト構造のスキーマを確認してください。

#events.proto

syntax = "proto3";

service EventService {
   rpc GetAllEvents (Empty) returns (EventList) {}
   rpc GetEvent (EventId) returns (Event) {}
   rpc CreateEvent (Event) returns (Event) {}
   rpc UpdateEvent (Event) returns (Event) {}
   rpc DeleteEvent (EventId) returns (Empty) {}
}

message Empty {}

message Event {
   string id = 1;
   string name = 2;
   string description = 3;
   string location = 4;
   string duration = 5;
   int32 lucky_number = 6;
   string status = 7;
}

message EventList {
   repeated Event events = 1;
}

message EventId {
   string id = 1;
}

上記のproto定義のコードスニペットでは、まずsyntax = "proto3"という定義でProtocol Buffersのバージョンを指定し、続いてプロトコルサービスを定義しました。

次に、プロトコルのイベントサービスの説明でEventServiceというサービスを作成しました。このサービス内にrpc関数を作成し、必要なパラメーターと期待される戻り値も指定しました。アプリケーションの要件に応じてサービスをいくつでも定義できますが、ここではシンプルにするため1つだけ定義します。

また、EventServiceの定義内でrpc関数のデータ型と戻り値を定義し、gRPC固有のフィールド番号を指定しました。これはエンコード時に使用されるバイト数を表します。詳しくはprotobufの公式ドキュメントをご覧ください。

gRPCサーバーの作成

上記のフォルダー構成に従い、アプリケーションのルートディレクトリにserverフォルダーを作成し、その中にindex.jsファイルを作成します。新しく作成したserver/index.jsファイルに、次のコードスニペットを貼り付けます。

#server/index.js

const PROTO_PATH = "./events.proto";

let grpc = require("@grpc/grpc-js");
let protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

let eventsProto = grpc.loadPackageDefinition(packageDefinition);

上記のコードスニペットでは、先ほど定義したevents.protoファイルをインポートし、変数PROTO_PATHに格納しました。次に、protoLoaderライブラリのloadSyncメソッドを使って読み込みました。その後、すべてのproto定義を格納するeventsProto変数に、proto定義を保存しました。

次に、先ほど定義したserver/index.jsファイルで、eventsProto変数の直後に以下のコードスニペットを追加します。

const { randomUUID } = require("node:crypto");

const events = [
   {
       id: "34415c7c-f82d-4e44-88ca-ae2a1aaa92b7",
       name: "Birthday Party",
       description: "27th Birthday in Paris",
       location: "Paris France",
       duration: "All Day",
       lucky_number: 27,
       status: "Pending"
   },
];

const server = new grpc.Server();

上記のコードスニペットでは、node:cryptoパッケージとそのrandomUUID関数を読み込みました。この関数はイベントID用のランダムで一意な文字列を生成します。このチュートリアルではインメモリデータベースを使うため、イベント一覧を保存する配列を定義し、新しいgrpc.Serverメソッドを呼び出してサーバーインスタンスを設定します。

次に、アプリケーションサービスを登録します。そのために、以下のコードスニペットを追加してください。上記のコードスニペットにあるserver変数の直後に配置します。

server.addService(eventsProto.EventService.service, {

   getAllEvents: (_, callback) => {
       callback(null, { events });
   },

   getEvent: (call, callback) => {
       let event = events.find(n => n.id == call.request.id);

       if (event) {
           callback(null, event);
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   },

   createEvent: (call, callback) => {
       let event = call.request;

       event.id = randomUUID();
       events.push(event);
       callback(null, event);
   },

   updateEvent: (call, callback) => {
       let existingEvent = events.find(n => n.id == call.request.id);

       if (existingEvent) {
           existingEvent.name = call.request.name;
           existingEvent.description = call.request.description;
           existingEvent.location = call.request.location;
           existingEvent.duration = call.request.duration;
           existingEvent.lucky_number = call.request.lucky_number;
           existingEvent.status = call.request.status;
           callback(null, existingEvent);
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   },

   deleteEvent: (call, callback) => {
       let existingEventIndex = events.findIndex(
           n => n.id == call.request.id
       );

       if (existingEventIndex != -1) {
           events.splice(existingEventIndex, 1);
           callback(null, {});
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   }
});

上記のコードスニペットでは、gRPCサーバーインスタンスでaddServiceメソッドを呼び出して、アプリケーションサービスを登録しました。これは基本的に、イベントの作成、読み取り、更新を行う処理です。

アプリケーションサーバーを起動できるようにするには、上記のコードスニペットにあるaddServiceメソッドの直後に以下のコードスニペットを貼り付けます。

server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), (error, port) => {
console.log(`Server listening at http://127.0.0.1:${port}`);
server.start();
});

gRPCクライアントの作成

上記のフォルダー構成に従い、アプリケーションのルートディレクトリにclientフォルダーを作成します。作成したclientフォルダー内にindex.jsファイルとapp.jsファイルを作成し、client/app.jsファイルに以下のコードスニペットを貼り付けます。

#client/app.js

const PROTO_PATH = "../events.proto";
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

const EventService = grpc.loadPackageDefinition(packageDefinition).EventService;
const client = new EventService("127.0.0.1:50051", grpc.credentials.createInsecure());
module.exports = client;

上記のコードスニペットでは、先ほど作成したproto定義をインポートし、protoLoaderで読み込みました。また、grpcクライアントをサーバーアプリケーションのIPアドレスに接続し、clientという変数名でイベントサービスをエクスポートしました。さらに、クライアントとサーバー間の通信を認証・暗号化するため、クライアントにSSL証明書を設定しました。

次に、client/index.jsファイルに以下のコードスニペットを貼り付けます。

#client/index.js

const client = require("./app");

const express = require("express");
const app = express();
app.disable('x-powered-by');
app.use(express.json());
app.use(express.urlencoded());

app.get("/", (req, res) => {
   client.getAllEvents(null, (err, data) => {
       if (!err) {
           res.status(200).send({
               data
           });
       }
   });
});

app.post("/createEvent", (req, res) => {

   let newEvent = {
       name: req.body.name,
       description: req.body.age,
       location: req.body.address,
       duration: req.body.address,
       lucky_number: req.body.address,
       status: req.body.status
   };

   client.insert(newEvent, (err, data) => {
       if (err) throw err;
       res.status(200).send({
           data,
           message: 'Event created successfully'
       });
   });
});

app.post("/updateEvent", (req, res) => {
   let updateEvent = {
       name: req.body.name,
       description: req.body.age,
       location: req.body.address,
       duration: req.body.address,
       lucky_number: req.body.address,
       status: req.body.status
   };

   client.update(updateEvent, (err, data) => {
       if (err) throw err;

       res.status(200).send({
           data,
           message: 'Event updated successfully'
       });
   });
});

app.delete("/deleteEvent", (req, res) => {
   client.remove({ id: req.body.eventId }, (err, _) => {
       if (err) throw err;

       res.status(200).send({
           message: 'Event deleted successfully'
       });
   });
});

const PORT = process.env.PORT || 50050;
app.listen(PORT, () => {
   console.log("Client Server listening to port %d", PORT);
});

上記のコードスニペットでは、client/app.jsファイルからevent-serviceをインポートしています。次に、gRPCを使ってサーバーアプリケーションをリモートで呼び出し、イベントのcreation、update、fetch、deleteを行うシンプルなエンドポイントを備えたExpressサーバーを設定しました。

サーバーとクライアントのアプリケーションをテストする

ここまでできたら、作業が正しく進んでいるか確認するためにテストしましょう。

サーバー

ターミナルでプロジェクトのルートディレクトリに移動し、以下のコマンドを実行します。

$bash
$ npm run start

サーバーアプリケーションがhttp://localhost:50051で起動します。

% npm run start
> event-app-node-grpc@1.0.0 start
> node server/index.js

Server listening at https://127.0.0.1:50051

クライアント

新しいターミナルウィンドウを開き、アプリケーションのルートディレクトリからclientフォルダーに移動して、以下のコマンドを実行します。

$bash
$ node index

アプリケーションがhttp://localhost:50050で起動します。

$ event-app-node-grpc  cd client
$ event-app-node-grpc/client node index.js

Client Server listening to port 50050

テストするには、ブラウザーでlocalhost:50050にアクセスするか、PostmanなどのAPIテストツールを使用します。最初にevents配列へ追加したデフォルトのイベントが表示されます。次のスクリーンショットと同じレスポンスが返されるはずです。

Postmanのインターフェースに、localhostへのGETリクエストと、誕生日パーティーのイベント詳細を含むJSONレスポンスが表示されています。

gRPC APIの認証とセキュリティ保護

gRPCプロトコルはさまざまな認証方式をサポートしており、新規システムにも既存システムにも簡単に適応できます。gRPCのクライアントとサーバー間の通信には、Googleのトークンベース認証を併用するかどうかにかかわらず、SSLやTLSなどの推奨方式を使って認証を実装できます。また、gRPCに組み込まれた認証関数を拡張するだけで、カスタム認証を構築することも可能です。

gRPCには、デフォルトで次の認証方式が用意されています。

  • SSLとTLS:サーバーを認証し、クライアントとサーバー間でやり取りするデータを暗号化

  • ALTS(Googleが設計した相互トランスポート認証プロトコル):Google Cloud Platform(GCP)で稼働するアプリケーションのRPC通信を保護

  • 汎用トークンベース認証:メタデータベースの認証情報をリクエストとレスポンスに付加

チュートリアルの導入部で触れたとおり、ここではSSLを使った認証を実装します。その後、この変更に対応するため、client/app.jsファイルとserver/index.jsファイルのコードを修正します。

OpenSSLでSSL証明書を生成する

まず、OpenSSLを使ってSSL証明書を生成します。この手順ではOpenSSLがインストールされている必要があります。また、bashスクリプトを実行する権限も必要です。権限エラーを避けるために、これらを確認しておきましょう。

フォルダー構成にscriptsフォルダーを作成し、その中にgenerate-certs.shというファイルを作成します。以下のコードスニペットをそのファイルに貼り付けます。

#scripts/generate-certs.sh

echo "Creating certs folder ..."
mkdir certs && cd certs

echo "Generating certificates ..."

openssl genrsa -passout pass:1111 -des3 -out ca.key 4096

openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Test/CN=localhost"

openssl genrsa -passout pass:1111 -des3 -out server.key 4096

openssl req -passin pass:1111 -new -key server.key -out server.csr -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Server/CN=localhost"

openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

openssl rsa -passin pass:1111 -in server.key -out server.key

openssl genrsa -passout pass:1111 -des3 -out client.key 4096

openssl req -passin pass:1111 -new -key client.key -out client.csr -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Client/CN=localhost"

openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

openssl rsa -passin pass:1111 -in client.key -out client.key

上記のコードは、サーバーとクライアントのアプリケーション間でセキュアに暗号化された接続を確立するために必要なSSL証明書を生成します。実行するとcertsフォルダーが作成され、OpenSSLを使ってサーバーとクライアントのSSL証明書が生成されて、certsフォルダーに保存されます。これらの設定の詳細や役割については、OpenSSLのウェブサイトをご覧ください。

npmでアプリケーションのSSL証明書を生成する

次に、アプリケーションのルートディレクトリでターミナルを開き、以下のコマンドを実行してスクリプトからアプリケーション用のSSL証明書を生成します。

$bash
$ npm run generate:certs

生成されたSSL証明書を含むcertsフォルダーが作成されます。

管理者権限が必要です。スクリプトの実行時に権限エラーが発生した場合は、以下のコマンドでスクリプトに実行権限を付与してから、もう一度試してください。

$bash
$ cd scripts
$ chmod u+r+x generate-certs.sh
$ ./generate-certs.sh

ターミナルに次のような出力が表示されます。

> event-app-node-grpc@1.0.0 generate:certs
> ./scripts/generate-certs.sh

Creating certs folder…
Generating certificates…
Generating RSA private key, 4096 bit long modulus
………………………………………………..++
……………..++

e is 65537 (0x10001)
Generating RSA private key, 4096 bit long modulus
…………………++
………………………………………….….….….….….………..…………..++
e is 65537 (0x10001)
Signature ok
subject=/C=CL/ST=RM/L=Santiago/0=Test/OU=Server/CN=localhost
Getting CA Private Key
writing RSA key
Generating RSA private key, 4096 bit long modulus
………………………………………………..….….….…………..++
.…………..++
e is 65537 (0x10001)
Signature ok
subject=/C=CL/ST=RM/L=Santiago/0=Test/OU=Client/ON=localhost
Getting CA Private Key
writing RSA key

client/app.jsファイルとserver/index.jsファイルの更新

ここまでで、gRPC APIの認証に必要なSSL証明書を生成しました。次に、生成した証明書を使えるようにclient/index.jsファイルとserver/index.jsファイルを修正します。

以下に示す更新後のclient/app.jsファイルでは、生成された証明書を読み込むためにfsモジュールを追加しました。続いて、その証明書を使ってgRPCのSSL認証情報を作成し、最後にその認証情報をgRPCサービスに適用しました。

#client/app.js(updated)

const PROTO_PATH = "../events.proto";
const fs = require('fs');
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

const credentials = grpc.credentials.createSsl(
   fs.readFileSync('../certs/ca.crt'),
   fs.readFileSync('../certs/client.key'),
   fs.readFileSync('../certs/client.crt')
);

const EventService = grpc.loadPackageDefinition(packageDefinition).EventService;
const client = new EventService("localhost:50051",credentials);
module.exports = client;

以下に示す更新後のserver/index.jsファイルにも、生成された証明書を読み込むためにfsモジュールを追加しました。その証明書を使ってgRPCのSSL認証情報を作成し、サーバーに適用しました。

#server/index.js(updated)

const PROTO_PATH = "./events.proto";
const fs = require('fs');

let grpc = require("@grpc/grpc-js");
let protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

let eventsProto = grpc.loadPackageDefinition(packageDefinition);

const server = new grpc.Server();

let credentials = grpc.ServerCredentials.createSsl(
   fs.readFileSync('./certs/ca.crt'), [{
   cert_chain: fs.readFileSync('./certs/server.crt'),
   private_key: fs.readFileSync('./certs/server.key')
}], true);

----------

----------

server.bindAsync("0.0.0.0:50051", credentials, (error, port) => {
console.log(`Server listening at http://0.0.0.0:${port}`);
server.start();
});

サーバーとクライアントのアプリケーションを実行する

gRPCの仕様を使ったイベント管理ソリューションを実装できました。エンドポイントをテストするには、以下の手順に沿ってターミナルからアプリケーションを起動します。

サーバー

ターミナルでプロジェクトのルートディレクトリに移動し、次のコマンドを実行します。

$bash
$ npm run start

実行すると、サーバーアプリケーションが http://0.0.0.0:50051 で起動します。

event-app-node-grpc % npm run start

> event-app-node-grpc@1.0.0 start
> node server/index.js

Server listening at http://0.0.0.0:50051

クライアント

新しいターミナルウィンドウを開き、アプリケーションのルートディレクトリから client フォルダに移動して、次のコマンドを実行します。

$bash
$ node index

実行すると、クライアントアプリケーションが http://localhost:50050 で起動します。

> event-app-node-grpc % cd client
> client % node index
Client Server listening to port 50050

アプリケーションをテストするには、ブラウザーで localhost:50050 にアクセスするか、Postman などの API テストツールを使用します。最初に events 配列に追加したデフォルトのイベントが表示されるはずです。レスポンスは、以下のスクリーンショットと同じになるはずです。

Postmanに、localhostへのGETリクエストと、パリで開催予定の誕生日イベントを示すJSONレスポンスが表示されています。

アプリケーションに追加したほかのエンドポイントもテストし、すべてが想定どおりに動作することを確認しましょう。

gRPCでセキュアなAPIを構築しました!

このチュートリアルでは、Node.jsを使ってgRPCでシンプルなAPIを構築し、その動作の仕組みと、APIセキュリティを高めるエンドツーエンドの認証・暗号化を実現するHTTP/2やSSL/TLSなど、数多くのメリットを紹介しました。

こうしたメリットがある一方で、gRPCにはブラウザーのサポートが限られていること、人間が読めないデータ形式、学習曲線の急さ、エッジキャッシュのサポートが不十分なことなど、弱点もあります。しかし、こうした制約があるにもかかわらず、優れたパフォーマンスと多言語対応により、内部マイクロサービス間の通信にはgRPCが最適です。gRPCプロトコルは優れており、2016年8月のリリース以来、業界で大きく普及してきました。今後も成長を続けていくでしょう。

gRPCでできることは、ほかにもたくさんあります。このチュートリアルの例は、gRPCで実現できることのほんの一部にすぎません。ドキュメントを確認してgRPCに関する知識を深め、アプリケーションの通信プロセスやgRPCセキュリティを維持するための戦略を強化しましょう。

APIに関するその他のリソース: