電通総研 テックブログ

電通総研が運営する技術ブログ

Googleが提供しているmicroservices-demoの一部をRust実装に置き換えてAmazon EKSにデプロイしてみた

こんにちは。金融ソリューション事業部の多田です。
本記事は 電通国際情報サービス Advent Calendar 2023 22日目の記事となります。
前日の記事は星野将吾さんの「若手こそ受けておきたい!IPA システムアーキテクト試験」でした。

はじめに

みなさん、microservices-demoをご存知でしょうか。
Googleが提供しているクラウドを前提としたマイクロサービスのデモアプリケーションです。
今回はこのmicroservices-demoの一部をRust実装に置き換えてAmazon EKS(以降、EKS)にデプロイしてみました。

筆者は社内ワーキンググループの活動としてソフトウェアアーキテクチャの技術調査などを行っており
その一環としてRust/gRPC/GraphQLを用いたマイクロサービスアーキテクチャを検討しております。
本記事はその検討にあたって実施した内容の一部をまとめたものになります。

microservices-demoについて

ECサイトを模しており、商品の閲覧やカートへ入れる、購入するといったことが可能です(決済などはモックです)。

技術的にはKubernetes、GKE、Istio、Stackdriver、gRPCなどを使用しています。
少し設定を加えればGKEなどに作成したKubernetesクラスター上にデプロイ可能となっています。

アーキテクチャと各サービスの言語、役割は以下のようになっています。
今回はこの中のproductcatalogserviceをRust実装に置き換えます。
(役割は筆者の意訳です。詳細はREADMEをご確認ください)

アーキテクチャ概要図

サービス 言語 役割
frontend Go HTTPサーバ。ユーザーと直接やり取りするのはココ
cartservice C# 商品の保存や取得(ショッピングカート)
productcatalogservice Go 商品一覧と各商品情報(商品名、概要)の提供、検索
currencyservice Node.js 各通貨の変換
paymentservice Node.js クレジットカード(モック)で決済
shippingservice Go 送料の計算と住所(モック)への発送
emailservice Python 購入確認メール(メールアドレスはモック)の送信
checkoutservice Go カートの情報を元に注文準備や支払い、発送、メール通知を調整
recommendationservice Python カートにある商品を元に別の商品を推薦
adservice Java テキスト広告の提供
loadgenerator Python/Locust EC2の利用フローを模したリクエストを継続的に実施(おそらく負荷テスト用)

productcatalogserviceをRustで実装する

productcatalogserviceの内容を確認

サービス同士の通信にはgRPCを使用しています。
Protocol Buffersは既にあるため、それを元にサービスを実装します。
demo.proto

productcatalogserviceのインターフェースは以下です。
(Productなどのメッセージ定義は省略しています)

service ProductCatalogService {
    rpc ListProducts(Empty) returns (ListProductsResponse) {}
    rpc GetProduct(GetProductRequest) returns (Product) {}
    rpc SearchProducts(SearchProductsRequest) returns (SearchProductsResponse) {}
}

microservices-demo上ではGoで実装されています。
コードを見てみるとproducts.jsonに商品情報があり、これを元に商品一覧や各商品情報、検索機能を提供しているようです。
この実装を参考にRustで実装します。

準備

以下を参考にRustのインストールをお願いします。
https://www.rust-lang.org/ja/tools/install

tonicについて

tonicというクレートを使用して実装します。
tonicはRustネイティブのgRPC実装です。用途によりコンポーネントが用意されており、それらを組み合わせて使用します。
今回はtonic-buildというコンポーネントを使用してprotoファイルを元にスケルトンとスタブを生成し、それを元にgRPCサーバーとクライアントを実装しました。
デモアプリケーション上ではクライアントは不要ですがテストのために実装しました。

tonicを利用するにはprotocのインストールが必要になります。
お使いの環境にあわせてインストールしてください。
https://github.com/hyperium/tonic?tab=readme-ov-file#dependencies

Windowsの場合、上記に加えてPROTOCという環境変数を用意しprotoc.exeへのパスを設定する必要があります。
(筆者はここで「コンパイルできない(; ;)」と30分ほど無駄にしました)

Rustによる実装

コードの全量は以下になります。
productcatalogservice

構成は以下のとおりです。

productcatalogservice
  |-- Cargo.toml
  |-- Dockerfile ... EKSへデプロイするためのイメージ作成で使用(後述)
  |-- build.rs ... スケルトン、スタブ生成用のコード
  |-- pb
  |   |-- demo.proto ... 前述のproductcatalogserviceのインターフェースを定義
  |-- products.json ... 商品情報リスト
  |-- src
      |-- client.rs ... gRPCクライアントのコード
      |-- server.rs ... gRPCサーバーのコード

本記事ではbuild.rsserver.rsについて解説します。
その他は実際のコードをご確認ください。

まずはbuild.rsです。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        .type_attribute(".", "#[derive(serde::Deserialize)]")
        .type_attribute(".", "#[serde(rename_all = \"camelCase\")]")
        .compile(&["./pb/demo.proto"], &["./pb"])?;
    Ok(())
}

build.rsはビルド時にprotoファイルを読み込んでスケルトンとスタブのコードを生成します。
type_attribute(...)は生成されるスケルトンとスタブに属性を付与したい場合に使用します。
今回はproduct.jsonから自動生成されたコード(商品の構造体)に直接変換するため、そのための属性を付与しました。
(実開発においては直接変換ではなく間にDTOなどを挟むと思います)
ビルドに成功するとtarget配下にコードが生成されます。

続いてserver.rsです(一部省略しています)。

// 生成されたコードの読み込み
pub mod hipstershop {
    // demo.protoのpackageを指定
    tonic::include_proto!("hipstershop");
}

// 省略

#[derive(Default)]
pub struct ProductCatalogServiceImpl {}

// サーバ用のコードを実装
#[tonic::async_trait]
impl ProductCatalogService for ProductCatalogServiceImpl {
    async fn list_products(
        &self,
        _request: Request<Empty>
    ) -> Result<Response<ListProductsResponse>, Status> {
        let products = read_catalog_file().await;
        let reply: ListProductsResponse = ListProductsResponse{ products: products };
        Ok(Response::new(reply))
    }

    async fn get_product(
        &self,
        request: Request<GetProductRequest>
    ) -> Result<Response<Product>, Status> {
        //省略
    }

    async fn search_products(
        &self,
        request: Request<SearchProductsRequest>
    ) -> Result<Response<SearchProductsResponse>, Status> {
        //省略
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 省略

    let addr = "0.0.0.0:3550".parse().unwrap();
    let product_catalog_service = ProductCatalogServiceImpl::default();

    println!("HealthServer + ProductCatalogServiceServer listening on {}", addr);

    Server::builder()
        .add_service(health_service)
        .add_service(ProductCatalogServiceServer::new(product_catalog_service))
        .serve(addr)
        .await?;

    Ok(())
}

pub mod hipstershop {...}では自動生成されたコードをモジュールとして定義しています。
tonic::include_proto!("hipstershop")は自動生成されたコードを読み込むためのマクロです。
引数にはprotoファイルのpackageを指定します。

impl ProductCatalogService for ProductCatalogServiceImpl {...}はサービスの実装です。
自動生成されたProductCatalogServiceトレイトを実装しています。
product.jsonから商品情報を読み込み、それをそのまま返したり、中身を検索したりしています。

main()はgRPCサーバーを起動するための実装です。
ポートやサービスの設定などをしています。

gRPCサーバーの動作確認

サーバーは以下のコマンドで起動します。

cargo run --bin server

クライアントを実行して動作確認をしてみます。

cargo run --bin client

成功すると以下のような結果が表示されます。

RESPONSE=Response { metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Tue, 19 Dec 2023 08:02:30 GMT", "grpc-status": "0"} }, message: ListProductsResponse { products: [Product { id: "OLJCESPC7Z", name: "Sunglasses", description: "Add a modern touch to your outfits with these sleek aviator sunglasses.", picture: "/static/img/products/sunglasses.jpg", price_usd: Some(Money { currency_code: "USD", units: 19, nanos: 990000000 }), ...省略

これでRustによる実装ができましたので、次はEKSへデプロイします。

EKSへのデプロイ

準備

EKSへデプロイするためにAWSアカウントの作成とAWS CLIのインストール、認証情報の設定をしてください。
また、イメージ作成にはDockerを使用しますのでお使いの環境にあわせてインストールしてください。
https://docs.docker.jp/desktop/install.html

コンテナイメージの準備 & ECRへの登録

EKSへデプロイするためにproductcatalogserviceのイメージを作成しECRへイメージを登録します。
まず以下を参考にAWSコンソールからECRにプライベートリポジトリを作成します(リポジトリ名はproductcatalogservice)。
https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/repository-create.html

次にイメージを作成しECRへpushするため、以下のコマンドを実行します。
AWSのアカウント番号は適宜置き換えてください。
リージョンは東京リージョンにしています。

# イメージ作成
docker build -t productcatalogservice .

# タグ付けする
docker tag productcatalogservice:latest <AWSのアカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com/productcatalogservice:latest

# dockerクライアントの認証
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <AWSのアカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com

# push
docker push <アカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com/productcatalogservice:latest

AWS CloudShellの準備

EKSへのデプロイにはAWS CloudShellを利用します。

まずはkubectlを使えるようにします。
kubectlはkubernetes用のコマンドラインツールです。
マイクロサービスのデプロイに使用します。

curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.28.3/2023-11-14/bin/linux/amd64/kubectl
chmod +x ./kubectl
mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin
echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc
kubectl version --client

続いてeksctlを使えるようにします。
eksctlはEKS上でKubernetesクラスターを作成・管理するためのコマンドラインツールです。

curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
sudo mv /tmp/eksctl /usr/local/bin
eksctl version

AWS上にEKSクラスター/ノードグループを作成する

以下のコマンドでクラスターを作成できます(10分ほどで完了します)。
nameonlineboutiqueregionは東京リージョンとしています。

eksctl create cluster --name onlineboutique --region ap-northeast-1 --without-nodegroup

クラスターの作成が完了したらノードグループを作成します(2分ほどで完了します)。
clusteronlineboutique、ノード数は4としています。

eksctl create nodegroup --cluster onlineboutique --region ap-northeast-1 --nodes 4 --nodes-min 4 --nodes-max 4

microservices-demoのデプロイ

クラスターが作成できたらいよいよデプロイです。
microservices-demoをクローンし、ECRへpushしたイメージを使うようマニフェストを修正します。
★でproductcatalogserviceのimageを以下のように修正してください。

  • 修正前: gcr.io/google-samples/microservices-demo/productcatalogservice:<バージョン>
  • 修正後:<アカウント番号>.dkr.ecr.ap-northeast-1.amazonaws.com/productcatalogservice:latest

その後、kubectl applyでデプロイします。

# microservices-demoをクローン
git clone https://github.com/GoogleCloudPlatform/microservices-demo.git

# リポジトリへ移動
cd microservices-demo

# 書き換え(★)
vi ./release/kubernetes-manifests.yaml

# デプロイ
kubectl apply -f ./release/kubernetes-manifests.yaml

サービスの一覧を確認するとfrontendにELBのDNS名が割り当てられています。

kubectl get svc

NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)        AGE
adservice               ClusterIP      10.100.8.99      <none>                                                                         9555/TCP       2m34s
cartservice             ClusterIP      10.100.167.3     <none>                                                                         7070/TCP       2m35s
checkoutservice         ClusterIP      10.100.155.33    <none>                                                                         5050/TCP       2m35s
currencyservice         ClusterIP      10.100.81.172    <none>                                                                         7000/TCP       2m34s
emailservice            ClusterIP      10.100.116.249   <none>                                                                         5000/TCP       2m35s
frontend                ClusterIP      10.100.23.148    <none>                                                                         80/TCP         2m35s
frontend-external       LoadBalancer   10.100.167.89    a0f6456281124412b8f095f94c6e242a-2020525330.ap-northeast-1.elb.amazonaws.com   80:31538/TCP   2m35s
kubernetes              ClusterIP      10.100.0.1       <none>                                                                         443/TCP        17m
paymentservice          ClusterIP      10.100.154.183   <none>                                                                         50051/TCP      2m35s
productcatalogservice   ClusterIP      10.100.20.37     <none>                                                                         3550/TCP       2m35s
recommendationservice   ClusterIP      10.100.90.209    <none>                                                                         8080/TCP       2m35s
redis-cart              ClusterIP      10.100.32.58     <none>                                                                         6379/TCP       2m34s
shippingservice         ClusterIP      10.100.194.29    <none>                                                                         50051/TCP      2m34s

ブラウザでアクセスして画像のような画面が表示されれば成功です。

後始末

削除は以下の順でコマンドを実行します。
グループ名はマネジメントコンソールなどでご確認ください。

# podを削除
kubectl delete -f ./release/kubernetes-manifests.yaml

# ノードグループを削除
eksctl delete nodegroup --cluster=onlineboutique --region ap-northeast-1 --name=<グループ名> --wait

# クラスターを削除
eksctl delete cluster --name onlineboutique --region ap-northeast-1 --wait

まとめ

Googleが提供しているmicroservices-demoの一部をRustに置き換えてEKSにデプロイしてみました。

tonicを用いることで簡単にRustで実装できました。
他のサービスもRustで置き換える場合、共通部分をライブラリクレートとして抜き出して各サービスで利用する(自動生成されたコードのモジュールを定義している部分など)、といったことも検討できそうです。

今回はRustを利用しましたが、gRPCが利用できれば他の言語でも可能ですのでご興味あればぜひお試しください。
デプロイ環境についてもKuberntesクラスターであればどの環境でもデプロイ可能です。
microservices-demoのクイックスタート開発ガイドにGKEやローカルを利用する方法が載っていますので、こちらもぜひお試しください。

ここまでお読みいただきありがとうございました。

私たちは一緒に働いてくれる仲間を募集しています!

募集職種一覧

執筆:@tada.keisuke、レビュー:@takeda.hideyuki
Shodoで執筆されました