クラウドサービスの選定において、サーバーレスアーキテクチャは運用負荷の削減やスケーラビリティの向上という観点からよく使う方もいるのではないでしょうか?ここでは、コンテナベースのCloud Runと関数単位実行を中心とするAWS Lambdaについて,各サービスの機能や構築方法、活用シーンを比較検討します.
CloudRunとLambdaを比べるのは果たしてどうなんですかね...
有名サービスの比較をした結果案外知らなかったことも見つかったので記事にしました
両サービスとも従量課金制であるため、コスト効率や運用の柔軟性についても考えてみましょう.
本記事は、Google CloudのCloud RunとAWSのLambdaについて、各サービスの概念、仕様、利用シーン及び構築方法を包括的に説明するとともに、それぞれの特徴に基づいた使い分けのポイントを解説するものである。各サービスの基本的な設計思想から運用上のメリット・デメリットまで、段階的に詳述する。
IAM AWS User クラウドサービスをフル活用しよう!
AWS(本妻?)のサービスの紹介・考察,使用事例の紹介,オンプレとの比較をするシリーズ
シリーズ AWS UserのGCP浮気日記
Day1 コンピュートとRDBMSのVPC内接続
Cloud Runの概要と特徴
Cloud RunはGoogle Cloudが提供するコンテナベースのサーバーレス実行環境である。Cloud RunはKnativeを基盤に動作しており、HTTPリクエストに応じた自動スケーリングが可能で、リクエスト数の増減に応じてインスタンスを迅速にスケールアウト、スケールダウンするという特徴がある。コンテナイメージを直接デプロイする仕組みのため、プログラミング言語やランタイムの制約がなく、アプリケーションの依存関係が複雑な場合やカスタムミドルウェアを用いる必要があるシーンで柔軟に対応できる。この特性により、既存のDockerコンテナを活用した移行や、マイクロサービスアーキテクチャへの採用が進んでいるのである。
Cloud Runは、HTTPをトリガーとするウェブアプリケーションやAPIサーバーに最適化されており、イベントドリブンなシステムにも適用可能であるが、基本的にはステートレスなサービスとして設計されるため、状態管理が求められる場合は外部データベースやキャッシュサービスと組み合わせる必要がある。さらに、サービスのオートスケーリング機能によって、突発的なトラフィックの急増にも柔軟に対応できる運用設計が成り立っているのである。
AWS Lambdaの概要と特徴
AWS LambdaはAmazon Web Servicesが提供する、イベントドリブンなサーバーレス実行環境である。Lambdaは関数単位でコードが管理され、各種AWSサービス(API Gateway、S3、CloudWatch Events、DynamoDBなど)からのトリガーに対応して動作する。そのため、短期間で完了する処理やリアルタイムのデータ処理、イベント応答型のシステムにおいて非常に有用である。Lambdaは各関数が特定のランタイム環境上で実行され、Node.js、Python、Java、Goなど複数の言語に対応しているが、実行時間やメモリ使用量、パッケージサイズに制約が設けられている。
Lambdaは、コードのデプロイが容易であり、コード更新時の短いデプロイサイクルが特徴である。関数を個別に管理する設計は、ビジネスロジックが明確に分離され、マイクロサービスアーキテクチャの一部としても利用しやすい。加えて、トリガーによる自動実行およびスケーリング機能により、負荷に応じた柔軟な処理能力を提供する。だが、Lambdaの実行環境はステートレスであり、長時間実行する必要がある場合や外部ライブラリの依存が大きい場合には、注意が必要である。
各サービスの構築方法
今回はフロントエンドとバックエンド双方のアプリケーションをdockerで立ち上げる形で両サービスを使用する方法を記載します.
ホスティングするフロントエンドアプリケーション
以下のコマンドでNext.jsアプリケーションを作成する
npx create-next-app@latest --ts
全体のディレクトリ構成は以下の通りです.CloudRunやLambdaにコンテナベースでホスティングする予定のためDoclerfile
も作成する.
.
├── Dockerfile
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── vercel.svg
│ └── window.svg
├── src
│ └── app
│ ├── layout.tsx
│ └── page.tsx
└── tsconfig.json
表示するページレイアウトは以下の通り.
"use client";
import React from 'react';
const Home: React.FC = () => {
return (
<div className="container">
<main>
<h1 className="title">Qiita Welcome!!</h1>
<p className="description">
Next.js フロントエンドを使ったサンプルアプリケーションです.
</p>
<button className="actionBtn">Get Started</button>
</main>
<style jsx>{`
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
main {
background-color: #ffffff;
padding: 3rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 600px;
width: 100%;
}
.title {
font-size: 3rem;
margin: 0;
color: #2c3e50;
letter-spacing: 0.05em;
}
.description {
margin: 1.5rem 0;
font-size: 1.25rem;
color: #7f8c8d;
}
.actionBtn {
background-color: #2ecc71;
border: none;
color: white;
padding: 1rem 2rem;
text-transform: uppercase;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.actionBtn:hover {
background-color: #27ae60;
}
@media (max-width: 600px) {
main {
padding: 2rem;
}
.title {
font-size: 2.5rem;
}
.description {
font-size: 1rem;
}
.actionBtn {
font-size: 0.9rem;
padding: 0.8rem 1.5rem;
}
}
`}</style>
</div>
);
};
export default Home;
Dockerfileは以下の通り.
FROM node:19-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "start"]
見た目
GET STARTEDを押しても何も起きません...
デプロイするバックエンドアプリケーション
以下のコマンドでgoアプリケーションを作成する.
go mod init backend
全体のディレクトリ構成は以下のようになっている.本当に必要最小限の構成である.
.
├── Dockerfile
├── go.mod
└── main.go
作成されるgo.mod
は以下の通りである.使用するパッケージは何もない.(そりゃそう)
module backend
go 1.23.4
単純にHello World!
を返すAPIを作成した.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World! from Go backend!")
})
log.Println("Starting Go server on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Error starting server: %v", err)
}
}
Dockerfileは以下の通り.
# ビルド用ステージ
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY main.go .
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o backend main.go
# 実行用ステージ
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/backend .
RUN chmod +x backend
EXPOSE 8080
CMD ["./backend"]
Cloud Runの構築方法
初めにCloud Runの構築方法を記載する.フロントエンドとバックエンド両方とも以下のようなアーキテクチャを想定しています.
ログインしているIAMユーザーがオーナー権限を持っていない場合は適切なIAMロールをアタッチする必要があります.
この操作をするのに必要なIAMロールは以下の通りです.
Cloud Run 関連
-
Cloud Run 管理者 (roles/run.admin)
Cloud Run のサービス作成、更新、削除など、管理操作を行うための権限が含まれている. -
サービスアカウント ユーザー (roles/iam.serviceAccountUser)
デプロイ時に指定するサービスアカウントを利用して Cloud Run サービスを動かす場合、このロールを対象に付与する必要がある.
Artifact Registry 関連
-
Artifact Registry ライター (roles/artifactregistry.writer)
コンテナイメージなどのアーティファクトを Artifact Registry にプッシュするために必要な最小限の権限です。 -
Artifact Registry 管理者 (roles/artifactregistry.admin)
リポジトリの作成や管理など、より広範な操作が必要な場合に使用します。
かなり簡単にしています
Artifact Registryにコンテナイメージをpushする
初めにArtifact Registryにリポジトリを作成する.まずArtifactRegistryのページを開き,「リポジトリの作成」を選択.
- 形式
- Docker
- モード
- 標準
- ロケーションタイプ
- リージョン
リージョンを指定した場合、アーティファクトは特定の地理的リージョン内に格納されるため、低レイテンシやデータ主権の観点で有利である。一方、マルチリージョンを選択すると、アーティファクトは複数のリージョンに自動的にレプリケートされ、グローバルな可用性とアクセス性が向上するが、その分、レプリケーションに伴うコストや地域ごとの細かな制御が難しくなる
- 暗号化
- Googleが管理する暗号化
リソースはデフォルトでGoogleが管理する鍵によって暗号化されるので、利用者は自ら鍵の管理を行わずとも暗号化の恩恵を受けることができる。しかし、自身で暗号化鍵管理を必要とする場合には、Cloud KMS鍵(顧客が所有する鍵)を使用でき、Autokey機能によりCloud KMS鍵の作成が自動化される。Google管理の鍵は運用がシンプルである一方、Cloud KMS鍵はより厳密なセキュリティポリシーを適用する場合に有用
- 不変のイメージタグ
- 無効
不変のイメージタグとは常に同じイメージダイジェストを指すものであり、これらのタグや対応するイメージを意図的に削除・上書きできなくする設定である。これにより、タグを参照する環境における一貫性と再現性が保証されるが、同じタグが既に別のバージョンに使用されている場合、新たなプッシュが拒否されるため、運用時の注意が必要である。
- クリーンアップポリシー
- テストを実行
リポジトリ内のアーティファクトを自動で整理・削除するための基準を定義するものである。ポリシーを有効にして「アーティファクトを削除」を選択すると、指定された条件に合致するアーティファクトが自動的に削除される。逆に「テストを実行」を選択すると、削除処理そのものは行われず、削除対象となるアーティファクトの情報が評価・記録され、Cloud Audit Loggingにテストイベントとして送信されるので、実際の削除を行う前に動作確認が可能となる。
- 脆弱性スキャン
- 有効
リポジトリにプッシュされるイメージを自動的にスキャンし、セキュリティ上の脆弱性が存在しないか検出する機能である。脆弱性スキャンが有効であれば、イメージに含まれる潜在的なリスク情報が早期に把握できるが、現時点ではDockerの標準リポジトリおよびリモートリポジトリに対してのみサポートされるため、その利用範囲には制限がある。
ここからはgcloudコマンドを使用します
macの場合ですが...gcloudコマンド導入方法brew install --cask google-cloud-sdk
上記コマンドが実行できたらsource '/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.zsh.inc' source '/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/completion.zsh.inc'
リポジトリが作成できたらローカル上にあるアプリケーションをdockerイメージとしてpushする.
まず,gcloudの認証をする
gcloud auth login
イメージを push する前に、Google Cloud CLI を使用して Artifact Registry に対するリクエストを認証する
リージョン us-central1 の Docker リポジトリの認証を設定する.
リージョンに関してはリポジトリ作成時に設定したリージョンを使用する
gcloud auth configure-docker us-central1-docker.pkg.dev
初めにフロントエンドのDocker imageのbuildを行う.フロントエンドのDockerfileがあるディレクトリに移動し,以下のコマンドを実行する.
docker buildx build --platform linux/amd64 -t qiita-frontend-sample-image:tag1 .
次にDocker イメージにレポジトリ名をタグ付けをする.
これでイメージを特定の場所に push するように docker push コマンドが構成される。
次のコマンドを実行して、イメージに qiita-sample-frontend/qiita-sample-frontend-image:tag1
としてタグ付けする.
qiita-sample-frontend
の部分はArtifact Registryで作成したリポジトリ名にすること
[PROJECTNAME]
には実際にプロジェクト名を入力すること.プロジェクト名はここに書いてある文字列のことです.
docker tag qiita-frontend-sample-image:tag1 us-central1-docker.pkg.dev/[PROJECTNAME]/qiita-sample-frontend/qiita-sample-frontend-image:tag1
最後に,イメージを Artifact Registry に push する.認証を構成してローカル イメージにタグ付けしたら、作成したリポジトリにイメージを push できる.
Docker イメージを push するには、次のコマンドを実行する.
docker push us-central1-docker.pkg.dev/[PROJECTNAME]/qiita-sample-frontend/qiita-sample-frontend-image:tag1
これでArtigfact Registryに以下のようにイメージがpushされていることが確認できる.
同じような操作をバックエンド側も行うと以下の通りになる.
Cloud Runの構築
Cloud Runのページを開き,「コンテナをデプロイ」を選択し,「サービス」を選択する.
- コンテナイメージのURL
- 先ほど作成したArtifact RegistryのDocker Image URL(「選択」から選択できます)
- サービス名
- 任意
- リージョン
- 任意
- 認証
- 未認証の呼び出しを許可(公開するAPIなので)
-
未認証の呼び出しを許可
- 公開する API やウェブサイトの場合、認証なしで誰でもサービスにアクセスできるように設定
-
認証が必要
- Cloud IAM を用いてアクセスできるユーザーを明示的に管理・認可
- 組織で
constraints/iam.allowedPolicyMemberDomains
が設定されている場合、未認証の呼び出しが禁止されることがある
- 課金
- リクエストベース(コストを抑えるため)
-
リクエスト ベース
- サービスがリクエストを処理する時だけ費用が発生.リクエストがないときは CPU の使用が制限され、コストがかからない仕組み
-
インスタンス ベース
- インスタンスのライフサイクル全体にわたって課金される.つまり、インスタンスが動いている期間中は常に CPU コストが発生する
- サービスのスケーリング
- 自動スケーリング
-
自動スケーリング
- トラフィックに応じて、サービスのインスタンス数が自動的に増減します。
- 「インスタンスの最小数」を0に設定すると、リクエストがないとインスタンスが完全に削除され、コストが削減されますが、1に設定すると常に少なくとも1つのインスタンスが動作しているため、コールドスタートが減少します。
-
手動スケーリング(プレビュー)
- 現在プレビュー中の機能で、ユーザーがインスタンス数を直接制御できるスケーリング方式です。
- ingress
- すべて
-
内部
- プロジェクト、共有 VPC、またはVPC Service Controls内からのみのアクセスを許可する
- 他のCloud Runサービスからの通信も、VPC経由でルーティングする必要がある
-
すべて
- インターネットなどの外部からも直接アクセスできる状態にする.公開サービスに利用されるオプション
Cloud Runの詳細を設定をするページの下部に「コンテナ、ボリューム、ネットワーキング、セキュリティ」がある.ここを開いて「コンテナポート」をDockerfileで指定したポートにする.
バックエンドも同じように構築する.この際にコンテナポートを8080にすること.
実際にcurlで両方のCloud Runを叩くと以下のような実行結果となる.
フロントエンドの実行結果は以下の通りである.
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-2f7b9ded6775055f.js"/><script src="/_next/static/chunks/4bd1b696-b5d598cf14dc4190.js" async=""></script><script src="/_next/static/chunks/587-4449b6aa6465fc85.js" async=""></script><script src="/_next/static/chunks/main-app-15095bc7e5f7c35a.js" async=""></script><script src="/_next/static/chunks/app/page-baf105e38b846e91.js" async=""></script><title>Next.js</title><meta name="description" content="Generated by Next.js"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div class="jsx-8abab21ff871dc18 container"><main class="jsx-8abab21ff871dc18"><h1 class="jsx-8abab21ff871dc18 title">Qiita Welcome!!</h1><p class="jsx-8abab21ff871dc18 description">Next.js フロントエンドを使ったサンプルアプリケーションです.</p><button class="jsx-8abab21ff871dc18 actionBtn">Get Started</button></main></div><script src="/_next/static/chunks/webpack-2f7b9ded6775055f.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[5244,[],\"\"]\n3:I[3866,[],\"\"]\n4:I[7033,[],\"ClientPageRoot\"]\n5:I[9809,[\"974\",\"static/chunks/app/page-baf105e38b846e91.js\"],\"default\"]\n8:I[6213,[],\"OutletBoundary\"]\nb:I[6213,[],\"ViewportBoundary\"]\nd:I[6213,[],\"MetadataBoundary\"]\nf:I[4835,[],\"\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"Oiej1_i-Xw4UwYGlL21Ca\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"$undefined\",[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L4\",null,{\"Component\":\"$5\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@6\",\"$@7\"]}],\"$undefined\",null,[\"$\",\"$L8\",null,{\"children\":[\"$L9\",\"$La\",null]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$1\",\"KO77Fl-ag8IRcLUCy0Awg\",{\"children\":[[\"$\",\"$Lb\",null,{\"children\":\"$Lc\"}],null]}],[\"$\",\"$Ld\",null,{\"children\":\"$Le\"}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$f\",\"$undefined\"],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"6:{}\n7:{}\n"])</script><script>self.__next_f.push([1,"c:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n9:null\n"])</script><script>self.__next_f.push([1,"a:null\ne:[[\"$\",\"title\",\"0\",{\"children\":\"Next.js\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Generated by Next.js\"}]]\n"])</script></body></html>%
バックエンドの実行結果は以下の通りである.
Hello World! from Go backend!
AWS Lambdaの構築方法
次にLambdaの構築方法を記載する.フロントエンドとバックエンド両方とも以下のようなアーキテクチャを想定しています.
Amazon API Gatewayとは
AWSが提供するフルマネージドなAPI管理サービスである.RESTful API, HTTP API, WebSocket APIの構築と公開を迅速に実現する仕組みである. このサービスは, APIのセキュリティや認証・認可, アクセス制御を柔軟かつ効率的に提供するための高度な機能を有している. APIとAWS Lambdaや他のAWSサービスとの統合が容易であり, サーバーレスアーキテクチャの構築と運用を大幅に簡素化するものである. 加えて自動スケーリング機能によりトラフィックの変動に即応し, CloudWatchとの連携で運用状況の監視やログ管理を実現することができる.
Amazon ECRとは
AWSが提供するフルマネージドなコンテナイメージレジストリである.Dockerなどのコンテナイメージを安全かつ効率的に格納し, 管理するためのサービスである.このサービスは, 高い可用性とスケーラビリティを備え, 認証やアクセス制御の機能によりイメージへの不正アクセスを防止し, またイメージの暗号化やライフサイクルポリシーの設定により, 不要なイメージの自動整理ができる.
ECRにコンテナイメージをpushする
ECRのページに移動する.その後「リポジトリを作成」を選択する.
作成したリポジトリを選択して,「プッシュコマンドを表示」を選択する.
ここで表示されたコマンドに従ってECRへのログインやdocker build や docker pushを行う.buildのみ以下のコマンドで行うこと.
docker buildx build --platform linux/amd64 -t qiita-sample-frontend .
AWS CLI や Dockerのインストールをしてから実行してください.
pushに成功すると以下のようにイメージがpushされていることがわかる.
バックエンド側も同じようにリポジトリ作成から行うこと.
ビルドは以下のコマンドで
docker buildx build --platform linux/amd64 -t qiita-sample-backend .
Lambdaでフロントエンドのホスティング
コンテナイメージのURIは先ほど作成したECRのURIを選択する.
API Gatewayの構築
Lambdaの画面でAPI GatewayのURLが発行されるのでこれをクリックする.
すると以下のように「service unavailable」となる
CloudWatchのログを見ると以下のようになっている.
2025-03-05T00:46:03.933Z
> frontend@0.1.0 start
2025-03-05T00:46:03.933Z
> next start -p 3000
2025-03-05T00:46:13.775Z
▲ Next.js 15.2.0
2025-03-05T00:46:13.775Z
- Local: http://localhost:3000
2025-03-05T00:46:13.775Z
- Network: http://169.254.76.1:3000
2025-03-05T00:46:13.775Z
✓ Starting...
2025-03-05T00:46:21.513Z
✓ Ready in 13.5s
2025-03-05T00:46:55.033Z
INIT_REPORT Init Duration: 60063.19 ms Phase: invoke Status: timeout
なぜLambdaではフロントエンドの実行ができないのか
next start で起動する Next.js のサーバーは、Node.js の HTTP サーバーとして永続的に待ち受けるように設計されている.一方、Lambda はイベント駆動で短命な処理を行うため,長時間待ち受けるサーバーをそのまま走らせるのは適していない.
初回のコールドスタートや SSR の実行時に初期化処理が重く,Lambda の最大実行時間に届いてしまっている可能性がある.
Cloud Runでは構築できてLambdaでは構築できない理由
-
常時実行の仕組み
Cloud Run:
コンテナベースのサービスであり、アプリケーションはコンテナ内で常に起動状態となり、指定したポート(例: 3000)で待ち受けるHTTPサーバーとして動作します。リクエストが来るたびに既に起動しているサーバーが応答する.
AWS Lambda:
イベント駆動で、各リクエストに対して一度だけ関数が起動し、処理終了後はコンテナがシャットダウンまたはアイドル状態となります。このため、永続的なHTTPサーバーとして動作させるのは難しく、タイムアウトや初期化遅延の問題が発生しやすい. -
実行時間の制約
Lambda:
最大実行時間の制限(例えば60秒など)があるため、Next.jsの起動やサーバーサイドレンダリングなどの初期化処理がその制限内に収まらなければタイムアウトエラーになりがち.
Cloud Run:
インスタンスがリクエストごとに再起動するのではなく、コンテナが継続して起動しているため、初回の起動コストが抑えられ、その後のリクエストにも即座に対応できる. -
リソース割り当て・スケーリング
Cloud Run:
コンテナ単位でリソース(CPUやメモリ)を柔軟に割り当て、かつスケールアウトが自然に行える仕組み.これにより、常時起動しているフロントエンドアプリケーションのパフォーマンスと応答性が改善される.
Lambda:
関数実行のたびに必要なリソースが割り当てられますが、毎回の起動時にコールドスタートが発生するリスクや、関数ごとのメモリ・CPUの制約が大きく影響するため、長時間のサーバー起動には不向き. -
アーキテクチャの違い
Cloud Run:
従来のサーバーや仮想マシンに近い動作をするため、Next.jsの「next start」による起動や常駐サーバーの挙動と親和性がある.
AWS Lambda:
サーバーレスに特化しており、他の仕組み(例: API GatewayのLambdaプロキシ統合)を取り入れた場合でも、関数としての動作を前提とするため、Next.jsの起動プロセスとの整合性が取りにくい場合がある.
フロントエンドホスティング代替手段
-
AWS App Runner
AWS App Runnerは、コードまたはコンテナをデプロイするだけでウェブアプリケーションを自動でスケーリングし、常駐型の環境で提供してくれる.Next.jsのようなNode.jsアプリケーションのホスティングに適している. -
静的サイトホスティング (Amazon S3 + CloudFront)
Next.jsの静的サイト生成(SSG)やnext buildを利用して、静的コンテンツとして構築する方法.SSRが不要な場合には、静的ホスティングの方が高速かつコスト効果に優れている.
静的サイトホスティング (Amazon S3 + CloudFront)の方法は以下の記事を参考にしてほしい.
今回はApp Runnerを使ってフロントエンドのホスティングを行ってみる.App Runnerの画面で「サービスの作成」を選択する.
ECRのコンテナイメージのURIを選択し,ECRアクセスロールを選択する.
サービス名,インスタンスの強さ,ポート(3000)を指定する.
これで,最後に確認をして,作成をすれば完成.少し待つと以下のように「デフォルトドメイン」が出るのでこれにアクセスする.
バックエンドアプリケーションをLambdaにデプロイ
これで作成されたAPI GatewayのURLを実行すると以下のような結果になる
2025-03-05T01:17:54.356Z
2025/03/05 01:17:54 Starting Go server on port 8080...
2025-03-05T01:18:03.576Z
INIT_REPORT Init Duration: 10009.37 ms Phase: init Status: timeout
2025-03-05T01:18:03.623Z
2025/03/05 01:18:03 Starting Go server on port 8080...
2025-03-05T01:18:06.596Z
INIT_REPORT Init Duration: 3003.34 ms Phase: invoke Status: timeout
2025-03-05T01:18:06.596Z
START RequestId: e6812799-4c30-460f-9a94-747440befe38 Version: $LATEST
2025-03-05T01:18:06.601Z
2025-03-05T01:18:06.600Z e6812799-4c30-460f-9a94-747440befe38 Task timed out after 3.01 seconds
2025-03-05T01:18:06.601Z
END RequestId: e6812799-4c30-460f-9a94-747440befe38
2025-03-05T01:18:06.601Z
REPORT RequestId: e6812799-4c30-460f-9a94-747440befe38 Duration: 3007.99 ms Billed Duration: 3000 ms Memory Size: 128 MB Max Memory Used: 3 MB
2025-03-05T01:18:06.624Z
2025/03/05 01:18:06 Starting Go server on port 8080...
このようにtimeoutとなる.
なぜlambdaでは立ち上がらないのか
1. Lambdaの実行モデルとの不整合
-
イベント駆動型の設計
AWS Lambdaは、リクエストごとに関数が起動し、短時間で処理を終了することを想定しています。ログで「Task timed out after 3.01 seconds」や「INIT_REPORT ... timeout」とあるように、Lambdaは設定されたタイムアウト以内に処理が完了しないと強制終了される. -
永続的なHTTPサーバーの不適合
ログに「Starting Go server on port 8080...」とあるが、通常のGoサーバーは永続的にHTTPリクエストを待ち受けるために設計されている.Lambdaは常駐してリクエストを受け続ける仕組みではなく、イベントに応じて起動し処理が終われば終了する短命な環境であるため、サーバーとしての常駐型アプリケーションを動かすのには適していない.
2. 実装方法の不整合
-
Lambdaハンドラーとしての実装が必要
AWS LambdaでGoを使う場合は、公式の aws-lambda-go ライブラリなどを利用して、Lambdaが期待するハンドラー関数を定義する必要がある。直接HTTPサーバーを起動し、サーバーがポート8080でリッスンする形では、Lambdaのライフサイクルやイベント処理に合致せず、タイムアウトエラーに繋がる。 -
初期化と処理のタイムアウト
初回もしくはウォーム状態でのサーバ起動に時間がかかるため、Lambdaの初期化フェーズでタイムアウトが発生してしいる。Lambdaは短時間でレスポンスを返すことが求められるため、長い初期化処理は致命的な問題となる.
バックエンドアプリケーションをLambdaにデプロイするための対応
以下のようにgithub.com/aws/aws-lambda-go
を使用してLambdaハンドラーとしての実装をする.
package main
import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
return events.APIGatewayProxyResponse{
StatusCode: 200,
Body: "Hello World from Go Backend!",
}, nil
}
func main() {
lambda.Start(handler)
}
これでdocker buld,docker push,lambdaへの再デプロイをすれば正しくサーバーは立ち上がる.
ただこれだとHandlerを作成しているのみなのでEchoのようなフレームワークを用いたい場合は以下のセクションようなコードに変更する必要がある.
Echoフレームワークを用いたWebサーバーをLambdaにデプロイ
echoパッケージを追加
go get github.com/labstack/echo/v4
main.go
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/", hello)
e.Logger.Fatal(e.Start(":8080"))
}
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello World from Go Backend!")
}
Dockerfile
# ビルド用ステージ
FROM golang:1.23-alpine AS builder
WORKDIR /app
# 必要なファイルのコピー
COPY main.go .
COPY go.mod go.sum ./
RUN go mod download
# ソース全体をコピー(必要に応じて)
COPY . .
# Cloud Run は amd64 なので、GOARCH=amd64 を指定
RUN GOOS=linux GOARCH=amd64 go build -o backend main.go
# 実行用ステージ
FROM alpine:latest
WORKDIR /app
# CA証明書のインストール(必要なランタイムパッケージ)
RUN apk add --no-cache ca-certificates
# Lambda Web Adapter を Docker イメージで使用するために必要な1行を追加
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
# ビルドステージから実行ファイルをコピー
COPY --from=builder /app/backend .
# 実行権限を明示的に付与
RUN chmod +x backend
# Lambda Web Adapter 用の環境変数の設定
ENV PORT=8080
ENV AWS_LWA_ASYNC_INIT=true
# Cloud Run の場合、EXPOSE は例えば 8080 として指定することも可能です
EXPOSE 8080
CMD ["./backend"]
これでdocker build,docker tag,docker pushをしてlambdaに再デプロイをする.
今回はAPI Gatewayを構築する際にパスの設定が必要になるのでlambdaからAPI Gatewayを構築するのではなく,API GatewayからLambdaを繋ぐように構築する.
ルートを設定する.
あとは何も設定することはないので「次へ」や「作成」をクリックしていく.
API Gatewayの構築が完了したらLambda関数の画面に以下のようにトリガーができている.
Cloud RunとAWS Lambdaの違いと適用シーン
Cloud RunとAWS Lambdaはいずれもサーバーレス環境として、開発者に運用負荷の軽減や自動スケーリングを提供するが、その実装アーキテクチャや利用シーンに違いがある.Cloud Runはコンテナイメージをベースとするため、複雑な依存関係を含むアプリケーションや、特定のライブラリ・ミドルウェアを必要とするケースに最適である。一方、Lambdaは関数単位での速やかな処理に焦点を当て、イベントドリブンなタスクや短時間の処理に重点を置いている。そのため、アプリケーションの性質や要求される処理時間、依存関係の規模に応じた選択が必須である。加えて、Cloud Runはコンテナ技術を活用するため、既存のオンプレミス環境からのアプリケーション移行や、ポータブルな実行環境としての採用が進んでおり、Lambdaは個々のタスクを軽く、かつ迅速に実行するユースケースにおいてその力を発揮する。
運用上の留意点とコスト面の比較
運用上の留意点として、Cloud Runはリクエストベースの従量課金であり、急激なリクエスト増加時でも自動的にスケールアウトするが、同時にコンテナの起動遅延が発生する可能性がある。Lambdaもまた従量課金制を採用しており、特に極短時間で実行されるタスクにおいて費用対効果が高いとされる。しかし、Lambdaは実行時間に制限があるため、長時間の処理が必要な場合や大きなパッケージを持つ場合には、工夫が求められる。いずれのサービスにおいても、適切なモニタリングとログ管理が求められ、クラウドネイティブな設計思想に基づいた運用体制の整備が不可欠である。
総じて、Cloud RunとAWS Lambdaはそれぞれ異なるアプローチでサーバーレス実行環境を提供しており、アプリケーションの仕様や運用上の要件に応じた選択が求められる。Cloud Runはコンテナ技術を活用することで、柔軟かつ高度なカスタマイズが可能であり、既存のコンテナ化されたアプリケーションの移行にも適している。一方、Lambdaはシンプルな関数単位の処理に最適化され、イベントドリブンなタスクを迅速に実行するためのツールとして、特にリアルタイムなデータ処理や短期タスクの自動化において優位性を有する。利用者はシステム全体のアーキテクチャや処理要件、開発・運用のフローを考慮し,これら2つのサービスを適材適所に組み合わせたクラウドネイティブなソリューションの構築を進められると良いだろう.