Terraform と ecspresso を使って CloudFront + ALB + ECS + gRPC な環境を構築してみた

Terraform と ecspresso を使って CloudFront + ALB + ECS + gRPC な環境を構築してみた

こんにちは!クラウド事業本部の枡川です。
少し前の話題となりますが、CloudFront が gRPC 通信を受け入れるようになっています。

What's New

https://aws.amazon.com/jp/about-aws/whats-new/2024/11/amazon-cloudfront-supports-grpc-delivery/

AWS ブログ

https://aws.amazon.com/jp/blogs/news/amazon-cloudfront-now-accepts-your-applications-grpc-calls/

CloudFront を挟むといっても、キャッシュを利用してトラフィックを捌くようなことはできません。

Because gRPC is used only for non-cacheable API traffic, your cache configurations won't affect gRPC requests.
Using gRPC with CloudFront distributions

単一リージョンで世界中にサービス展開している際に、早めに高帯域な AWS ネットワークに引き込めることなどがメリットになります。

CloudFront は、最も近いエッジへのインテリジェントなルーティング機能を備えた 600 以上のエッジロケーションで構成されたグローバルネットワークを提供します。エッジロケーションは TLS ターミネーションに加えて、静的コンテンツのキャッシュ (オプション) を提供します。CloudFront は、フルマネージド型、低レイテンシー、高帯域幅のプライベート AWS ネットワークを介してクライアントアプリケーションのリクエストを gRPC オリジンに転送します。
Amazon CloudFront がアプリケーションの gRPC 呼び出しを受け入れるようになりました

また、キャッシュが効かなくても DDoS 耐性を向上させる効果は得られるため、DDoS が気になる場面で挟むのも良いでしょう。

https://repost.aws/ja/questions/QU2cuGIIa0Tc6g8As0deiB6g/do-we-lose-any-ddos-protections-from-cloudfront-by-turning-caching-off-and-forwarding-all-headers-to-origin

今回は Terraform と ecspresso を使って、ECS + ALB + CloudFront + gRPC な環境を構築してみました。

Terraform を利用した AWS 環境構築

構築するアーキテクチャは下記のようになります。

grpc-cloudfront.png

各種バージョンは下記としており、mise を利用してインストールしました。

ツール名 バージョン
Terraform 1.11.3
ecspresso 2.4.6

コード類は以下のリポジトリに公開していますので、ご興味のある方はご参照下さい。

https://github.com/masutaro99/grpc-cloudfront

まず、AWS インフラから作成していきます。

cd terraform
terraform init
terraform apply

※ 上記リポジトリにあるコード類を利用する場合は、locals.tf で利用する Route53 のドメイン名を指定することと、backend.tf で S3 バックエンドの設定を行う必要があります。

CloudFront を作成する際はビヘイビア設定で、grpc_config.enabled = true とします。

resource "aws_cloudfront_distribution" "grpc_app" {
  aliases         = ["${local.cloudfront_sub_domain_name}.${local.hosted_zone_name}"]
  enabled         = true
  http_version    = "http2"
  is_ipv6_enabled = true
  price_class     = "PriceClass_All"
  default_cache_behavior {
    allowed_methods          = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cache_policy_id          = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" // Managed-CachingDisabled
    cached_methods           = ["GET", "HEAD"]
    compress                 = true
    default_ttl              = 0
    max_ttl                  = 0
    min_ttl                  = 0
    origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac" // Managed-AllViewerExceptHostHeader
    target_origin_id         = "${local.alb_sub_domain_name}.${local.hosted_zone_name}"
    viewer_protocol_policy   = "https-only"
    grpc_config {
      enabled = true
    }
  }
  origin {
    connection_attempts = 3
    connection_timeout  = 10
    domain_name         = "${local.alb_sub_domain_name}.${local.hosted_zone_name}"
    origin_id           = "${local.alb_sub_domain_name}.${local.hosted_zone_name}"
    custom_origin_config {
      http_port                = 80
      https_port               = 443
      origin_keepalive_timeout = 5
      origin_protocol_policy   = "https-only"
      origin_read_timeout      = 30
      origin_ssl_protocols     = ["TLSv1.2"]
    }
  }
  restrictions {
    geo_restriction {
      restriction_type = "whitelist"
      locations        = ["JP"]
    }
  }
  viewer_certificate {
    acm_certificate_arn            = module.cloudfront_acm.acm_certificate_arn
    cloudfront_default_certificate = false
    minimum_protocol_version       = "TLSv1.2_2021"
    ssl_support_method             = "sni-only"
  }
}

CloudFront のオリジンとしては ALB に設定したドメインをカスタムドメインとして登録しました。
VPC Origin は対応していないので、注意が必要です。
(最初設定しようとして CloudFront ログに OriginConnectError と記録されました...)

CloudFront の Lambda@Edge を使用した WebSocket、gRPC トラフィック、およびオリジンの書き換えは、VPC オリジンではサポートされていません。
VPC オリジンを使用したアクセス制限

ターゲットグループ設定でも、protocol_version = "GRPC" を指定します。

resource "aws_lb" "alb" {
  name                       = "${local.resource_prefix}-alb"
  load_balancer_type         = "application"
  internal                   = "false"
  idle_timeout               = 60
  enable_deletion_protection = false

  subnets = module.vpc.public_subnets

  security_groups = [
    aws_security_group.alb.id
  ]
}

resource "aws_security_group" "alb" {
  vpc_id = module.vpc.vpc_id
  name   = "${local.resource_prefix}-sg-alb"

  ingress {
    description = "HTTP from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_lb_listener" "grpc" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  certificate_arn   = module.alb_acm.acm_certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.grpc_app.arn
  }
}

resource "aws_lb_target_group" "grpc_app" {
  name             = "${local.resource_prefix}-tg-grpc-app"
  target_type      = "ip"
  port             = 50051
  protocol         = "HTTP"
  protocol_version = "GRPC"
  vpc_id           = module.vpc.vpc_id
}

イメージビルドと ecspresso を利用した ECS 関連リソースの作成

作成した ECR に対してコンテナイメージを push していきます。
アプリケーションコードは grpc 公式リポジトリのサンプルを活用した簡単なものを利用しました。

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"

    pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

type server struct{
    pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.Name)
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    reflection.Register(s)
    log.Printf("serving on :50051\n")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

app ディレクトリに移動してコンテナイメージをビルドします。

export IMAGE_NAME="grpc-app"
export AWS_ACCOUNT_ID="xxxxxxxxxxxx"
export COMMIT_HASH=$(git rev-parse --short HEAD)
docker build \
  --platform=linux/x86_64 \
  -t $IMAGE_NAME:$COMMIT_HASH \
   -f Dockerfile --no-cache .

コンテナイメージを ECR に push します。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com
docker tag $IMAGE_NAME:$COMMIT_HASH $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/$IMAGE_NAME:$COMMIT_HASH
docker push $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/$IMAGE_NAME:$COMMIT_HASH

続いて ECS 関連のリソースを作成します。

ecspresso deploy --config=./.ecspresso/config.yaml

※ ecspresso deploy を行う前に、ecs-task-def.json でイメージタグを push したものに置き換える必要があります。また、config.yaml で tfstate ファイルの場所を指定する必要があります。

動作確認

一通り構築が完了したので grpcurl でリクエストを送ってみます。

% grpcurl -vv -d '{"name": "kentaro"}' www.example.com:443 helloworld.Greeter.SayHello

Resolved method descriptor:
rpc SayHello ( .helloworld.HelloRequest ) returns ( .helloworld.HelloReply );

Request metadata to send:
(empty)

Response headers received:
content-type: application/grpc
date: Sun, 20 Apr 2025 04:04:53 GMT
server: CloudFront
via: 1.1 5f18cdf7ce4383d3046c528d1ee9da8a.cloudfront.net (CloudFront)
x-amz-cf-id: w4gEZazRSvwxG4QnQ2w38wg_GNHI8de5HmlODB41ON-WnFOL2NfxhA==
x-amz-cf-pop: NRT20-P4
x-cache: Miss from cloudfront

Estimated response size: 15 bytes

Response contents:
{
"message": "Hello kentaro"
}

Response trailers received:
(empty)
Sent 1 request and received 1 response
Timing Data: 383.45875ms
Dial: 252.740667ms
TLS Setup: 13.334µs
BlockingDial: 252.697459ms
InvokeRPC: 113.586458ms

CloudFront に対して gRPC リクエストを送信できました!

まとめ

ECS + ALB + CloudFront で gRPC アプリケーションを公開する構成を Terraform と ecspresso で構築してみました。
VPC オリジンや標準アクセスログ強化と合わせて、CloudFront が直近かなり強化されたなぁと感じております。
気になる方は是非試してみて下さい!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.