そこに仁義はあるのか(仮)

略してそこ仁!

gRPCアプリをAWS CDKでECSとFargateにデプロイする

gRPCを利用したGoアプリケーションをAWS上でデプロイするためのCDKを作成したので、その構成や実装のポイントについて紹介します。

今回デプロイするgRPCサーバーの実装は、作ってわかる! はじめてのgRPCを参考にしました。
この記事では、Terraformを使って環境を構築していたので、CDKを使ってAWSのインフラを定義し、Fargate上でgRPCサーバーを動かす構成を作成しました。

ポイントは以下の通りです。

  • Route 53とACMを利用してHTTPS対応のgRPCエンドポイントを構築(これだけ手動で実行)
  • DockerイメージをCDKでビルドし、ECRにアップロード
  • ALBを活用してgRPCリクエストをルーティングし、ターゲットグループで適切なヘルスチェックを設定
  • ECS Fargateのタスク定義とオートスケーリングの設定
  • デプロイ後、ALBのDNSを出力し、サービスが正しく稼働していることを確認

📂 ディレクトリ構成

プロジェクトのディレクトリ構成の重要な部分を抜粋すると以下のようになります。app-grpc フォルダはZennの記事通りの構成で、infraフォルダにCDKのプロジェクトを作りました。

.
├── README.md
├── app-grpc
│   ├── Dockerfile  # gRPCアプリのDockerイメージ定義
│   └── src
│       ├── api  # gRPCサービスの定義
│       ├── cmd  # エントリーポイント
│       ├── go.mod  # Goの依存管理
│       ├── go.sum  # 依存関係の固定
│       └── pkg  # サーバーのロジック
└── infra
    ├── cdk.json  # CDKの設定ファイル
    ├── package.json  # CDKの依存関係管理
    ├── tsconfig.json  # TypeScriptの設定
    ├── .env  # 環境変数の設定
    └── src/  # CDKのソースコード
        ├── bin/  # エントリーポイント
        │   └── app.ts
        └── lib/  # スタック定義
            └── grpc-app-stack.ts

🛠 事前準備

本CDKを利用する前に、以下の準備をしました。

1. Route 53でのドメイン取得

gRPCサービスを公開するためのドメインをRoute 53で取得しました。

2. ACM (AWS Certificate Manager)での証明書発行

HTTPS対応のため、ACMでSSL/TLS証明書を発行しました。
証明書のARNをメモしておき、CDKで利用するために ↓ の .env ファイルに記載しました。
.gitignore.env を追加して、Gitリポジトリに含まれないようにしています。

.env ファイルのサンプルは以下の通りです。

# ACM Certificate ARN for HTTPS
ACM_CERTIFICATE_ARN=arn:aws:acm:REGION:ACCOUNT_ID:certificate/CERTIFICATE_ID

3. 環境変数を利用したARNの設定

CDKのapp.tsで.envファイルから環境変数を読み込み、ACM証明書のARNを設定しています。

import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as dotenv from 'dotenv';
import { GrpcAppStack } from '../lib/grpc-app-stack';

dotenv.config();

const app = new cdk.App();

const acmCertArn = process.env.ACM_CERTIFICATE_ARN;
if (!acmCertArn) {
  throw new Error('環境変数ACM_CERTIFICATE_ARNが設定されていません。.envファイルを確認してください。');
}

new GrpcAppStack(app, 'GrpcApp', {
  acmCertificateArn: acmCertArn,   // 設定をスタックに渡す
});

🏗 インフラ構成

本CDKでは、以下のAWSサービスを使用しています。

  • ECS (Fargate): gRPCサーバーをコンテナとして動作させる
  • ALB (Application Load Balancer): gRPCリクエストのルーティング
  • ECR: コンテナイメージの管理
  • ACM (AWS Certificate Manager): HTTPS対応の証明書管理
  • Auto Scaling: 負荷に応じたスケーリング

CDKで作成する構成は以下のようになります。

graph TD
    Client[Client] --> gRPC[gRPC API]
    gRPC --> ALB[Application Load Balancer]
    ALB --> ECS[ECS Fargate Cluster]
    ECS --> ECR[ECR Repository]

    subgraph "gRPC Stack"
        gRPC
        ALB
        ECS
        ECR
    end

    classDef aws fill:#F7931E,stroke:#232F3E,color:#000000,font-weight:bold;
    classDef outer fill:#fdf9e6,stroke:#333,stroke-width:2px;
    class gRPC,ALB,ECS,ECR aws;
    class gRPC,ALB,ECS,ECR outer;

📝 CDKの実装

CDKを使ってgRPCアプリケーションをFargateにデプロイするための各コンポーネントを、パーツごとに説明します。記事の最後に全体のCDKを記載します。

1. Dockerイメージの定義とECRアップロード

まず、gRPCアプリケーションのDockerイメージをCDKでビルドし、ECRにアップロードします。

const asset = new assets.DockerImageAsset(this, 'GrpcApiImage', {
  directory: path.join(__dirname, '../../../app-grpc'),
});

これにより、app-grpc/ディレクトリ内のDockerfileとソースコードを元に、ECRにコンテナイメージがアップロードされます。

2. タスク定義とコンテナ設定

Fargate上で動作するgRPCアプリのタスク定義を作成し、コンテナの設定を行います。
ちなみに、この runtimePlatform については こちらの記事に詳しく書きました。

const taskDefinition = new ecs.FargateTaskDefinition(this, 'GrpcTaskDef', {
  runtimePlatform: {
    cpuArchitecture: ecs.CpuArchitecture.ARM64,
  },
});

ARM64アーキテクチャを指定しているのは、M1 Macでビルドしたコンテナを動作させるためです。

次に、コンテナをタスク定義に追加し、ヘルスチェックを設定します。
gRPCアプリのヘルスチェックには、grpc_health_probe を利用しています。
(ヘルスチェックは、タスクに対してと、LBの2箇所。LBは後述。)

const grpcPort = 8080;

const container = taskDefinition.addContainer('grpc-api-container', {
  image: ecs.ContainerImage.fromDockerImageAsset(asset),
  logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'grpc-service' }),
  healthCheck: {
    command: ['CMD-SHELL', '/bin/grpc_health_probe -addr=:8080 || exit 1'],
    startPeriod: cdk.Duration.seconds(5),
  },
});

container.addPortMappings({ containerPort: grpcPort, protocol: ecs.Protocol.TCP });

3. ALBとターゲットグループの設定

ALB (Application Load Balancer) を作成し、gRPCアプリをターゲットグループに登録します。
L3 ConstructのApplicationLoadBalancedFargateServiceを使うことで、設定がめちゃくちゃ楽になりました。
ACM証明書をcertificate でALBに設定し、gRPCプロトコルを使用できるようにしています。この設定でprotocol のデフォルトプロトコルが HTTPS に設定されます。
また、Application Load Balancer (ALB) を使用し、protocolVersion: ApplicationProtocolVersion.GRPC を設定することで、gRPC特有のHTTP/2通信を適切に処理しています。

const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', acmCertArn);

const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'GrpcService', {
  taskDefinition,
  desiredCount: 2,
  certificate,
  targetProtocol: elbv2.ApplicationProtocol.HTTP,
  protocolVersion: ApplicationProtocolVersion.GRPC,
});

ターゲットグループのヘルスチェックを設定します。

const targetGroup = fargateService.targetGroup;
targetGroup.configureHealthCheck({
  path: '/grpc.health.v1.Health/Check',
  healthyThresholdCount: 2,
  healthyGrpcCodes: '0',
});

4. オートスケーリングの設定

負荷に応じたオートスケーリングを設定し、サービスの可用性を向上させます。

const scalableTarget = fargateService.service.autoScaleTaskCount({
  minCapacity: 2,
  maxCapacity: 10,
});

scalableTarget.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 70 });

最小2、最大10のタスク数でスケールするよう設定し、CPU使用率70%を超えるとスケールアウトするようにしました。

5. デプロイ結果の確認

最後に、作成したALBのDNS名をCDKの出力として表示し、デプロイ結果を確認できるようにします。

new cdk.CfnOutput(this, 'LoadBalancerDNS', {
  value: fargateService.loadBalancer.loadBalancerDnsName,
  description: 'Load balancer DNS name',
});

📂 まとめ

本記事では、AWS CDKを活用してgRPCアプリをECS Fargate上にデプロイする方法を解説しました。主要なポイントは以下の通りです。

  • Route 53とACMを利用してHTTPS対応のgRPCエンドポイントを構築
  • DockerイメージをCDKでビルドし、ECRにアップロード
  • ALBを活用してgRPCリクエストをルーティングし、ターゲットグループで適切なヘルスチェックを設定
  • ECS Fargateのタスク定義とオートスケーリングの設定
  • デプロイ後、ALBのDNSを出力し、サービスが正しく稼働していることを確認

👀 参考

CDKスタックの全体はこんな感じです。

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import { Construct } from 'constructs';
import * as assets from 'aws-cdk-lib/aws-ecr-assets';
import * as path from 'path';
import { ApplicationProtocolVersion } from "aws-cdk-lib/aws-elasticloadbalancingv2";

// GrpcAppStackのプロパティを拡張
interface GrpcAppStackProps extends cdk.StackProps {
  acmCertificateArn: string;
}

export class GrpcAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: GrpcAppStackProps) {
    super(scope, id, props);

    // Docker イメージを CDK でビルド & ECR にアップロード
    const asset = new assets.DockerImageAsset(this, 'GrpcApiImage', {
      directory: path.join(__dirname, '../../../app-grpc'),
    });

    // 証明書ARNの確認
    const certificateArn = props?.acmCertificateArn;
    if (!certificateArn) {
      throw new Error('ACM証明書ARNが提供されていません。');
    }

    // 最初にタスク定義を作成
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'GrpcTaskDef',{
      runtimePlatform: {
        cpuArchitecture: ecs.CpuArchitecture.ARM64,  // M1 Mac でビルドしたので、ランタイムを指定
      },
    });

    // ヘルスチェック付きでコンテナを追加
    const container = taskDefinition.addContainer('grpc-api-container', {
      image: ecs.ContainerImage.fromDockerImageAsset(asset),
      logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'grpc-service' }),
      healthCheck: {
        command: [
          'CMD-SHELL',
          '/bin/grpc_health_probe -addr=:8080 || exit 1'
        ],
        startPeriod: cdk.Duration.seconds(5),
      },
    });

    // gRPC ポート
    const grpcPort = 8080;
    
    // コンテナポートのマッピング
    container.addPortMappings({
      containerPort: grpcPort,
      protocol: ecs.Protocol.TCP,
    });

    // 証明書オブジェクトの作成
    const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', certificateArn);

    // ApplicationLoadBalancedFargateServiceを使用して、事前作成したタスク定義を利用
    const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'GrpcService', {
      taskDefinition: taskDefinition, // 事前に作成したタスク定義を使用
      desiredCount: 2,
      certificate: certificate,  // 証明書が設定されていればHTTPSに設定される
      targetProtocol: elbv2.ApplicationProtocol.HTTP,  // GRPCプロトコルを指定(HTTPベース)
      protocolVersion: ApplicationProtocolVersion.GRPC,  // GRPCプロトコルを指定(GRPCベース)
    });

    // Configure target group with HTTP health check
    const targetGroup = fargateService.targetGroup;
    targetGroup.configureHealthCheck({
      path: "/grpc.health.v1.Health/Check",
      healthyThresholdCount: 2,
      healthyGrpcCodes: "0",
    });

    // オートスケーリングの設定
    const scalableTarget = fargateService.service.autoScaleTaskCount({
      minCapacity: 2,
      maxCapacity: 10,
    });

    // CPU使用率に基づくスケーリング
    scalableTarget.scaleOnCpuUtilization('CpuScaling', {
      targetUtilizationPercent: 70,
    });

    // Outputs
    new cdk.CfnOutput(this, 'LoadBalancerDNS', {
      value: fargateService.loadBalancer.loadBalancerDnsName,
      description: 'Load balancer DNS name',
    });
  }
}