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";
interface GrpcAppStackProps extends cdk.StackProps {
acmCertificateArn: string;
}
export class GrpcAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: GrpcAppStackProps) {
super(scope, id, props);
const asset = new assets.DockerImageAsset(this, 'GrpcApiImage', {
directory: path.join(__dirname, '../../../app-grpc'),
});
const certificateArn = props?.acmCertificateArn;
if (!certificateArn) {
throw new Error('ACM証明書ARNが提供されていません。');
}
const taskDefinition = new ecs.FargateTaskDefinition(this, 'GrpcTaskDef',{
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64,
},
});
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),
},
});
const grpcPort = 8080;
container.addPortMappings({
containerPort: grpcPort,
protocol: ecs.Protocol.TCP,
});
const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', certificateArn);
const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'GrpcService', {
taskDefinition: taskDefinition,
desiredCount: 2,
certificate: certificate,
targetProtocol: elbv2.ApplicationProtocol.HTTP,
protocolVersion: ApplicationProtocolVersion.GRPC,
});
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,
});
scalableTarget.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
});
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
value: fargateService.loadBalancer.loadBalancerDnsName,
description: 'Load balancer DNS name',
});
}
}