【API】バックエンド初心者によるCloud Runを使った爆速サーバーレスAPI開発

この記事は [レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)の12日目の記事となります。

## はじめに

こんにちは、藏重です。

株式会社レコチョクで主にiOSアプリの開発に携わっていますが、他にもWebフロントやAPI、バックエンド、挙げ句の果てにはスクラムマスターなどにも手を出している5年目エンジニアです。

最近はウマ娘のキャラソンにハマっていて、チームカノープスのキャラソンを車で聞き続けています。
(ナイスネイチャ最推し)

今回が技術記事の初投稿ということもあり、至らぬ点があるかもしれませんが、どうぞご容赦下さい。

---

さて前日の記事[【iOS】Kotlin MultiplatformでApp Clip対応アプリを2か月で作ったので振り返る](https://qiita.com/shumpei_nagata/items/7dcbd065ffbcf7ca94da)でも紹介のあった[P!TNE](https://pitne.jp)の開発に私も携わっており、
APIやインフラを含むバックエンド全般とWebフロントの実装をほぼ1人で担当することになりました。

バックエンド未経験の私にとって一番の課題は「サーバーをどこに、どうやって構築するか」でした。

限られた期間で複数のアプリケーションを開発しなければならない都合上、のんびりと各種サーバーを構築している暇はありません。

また、完全な新規サービスのため利用率の推定が難しく、コストパフォーマンスの観点からも最初は小規模に始めたいと考えていました。

そこで目をつけたのが、Cloud Runというプロダクトでした。

今回はそんなCloud Runの紹介と、「いかに早く動くものを作れるか」ということをテーマに、実際に簡易的なAPIを作ってデプロイしてみたいと思います。

## Cloud Runとは
Cloud RunはGoogle Cloud Platform(以下GCP)が提供する、サーバーレスでコンテナの実行環境を構築できるプロダクトです。

さらにCloud Runはサービスとジョブという2つの機能に別れており、利用目的に応じて選択することができます。

### Cloud Run サービスとは
Cloud Run サービス(以下サービス)はHTTPリクエストに応答して動作するいわゆるリクエスト応答型であり、WebアプリケーションやAPIなどを動かすのに適しています。

今回はAPIを作りたいので、このCloud Run サービスを使っていきます。

### Cloud Run ジョブとは
Cloud Run ジョブ(以下ジョブ)は、スケジュールや手動で実行され、指定したタスクを短時間で完了させることを目的としています。

定期的なスクリプトの実行やバッチ処理に適しており、タスク実行型の特徴を持っています。

こちらは今回使わないので、あまり詳しくは触れません。

### Cloud Run　サービスの特徴
以下の3つはサービス、ジョブともに共通する大きな特徴ですが、今回は主にサービスを対象にして簡単に解説します。

1. サーバーレスである
2. 従量課金制である
3. デプロイが簡単

### 1. サーバーレスである
サーバーレスを簡単に言うなれば「インフラの管理はGoogleさんにお任せしまーす！」ということです。

つまり、ユーザーはサーバーの構築や運用、保守を考える必要がなく、Googleが管理するインフラ上で動くアプリケーションの開発に勤しむことができるのです。

インフラの構築と管理はそれだけでも大変なお仕事です。

その上、そこで動くアプリケーションも開発するとなれば、初心者の私にはとても手が負えません。
(やりたくないとは言ってない)

インフラの構築と管理を気にしなくてよいCloud Runは、今回のようにミニマムにサービスをスタートさせたい上では、非常に魅力的でした。

### 2. 従量課金制である
GCP内のクラウドサービスは基本的に従量課金制であり、Cloud Runも例に漏れず従量課金制です。

ただし厳密にはCloud Run上で動かすサービスにCPUを割り当てる方法が2つあり、どちらを選ぶかによって料金の計算式が変わってきます。

具体的には、以下のコンテナインスタンスのライフサイクルに対して、どの間にCPUを割り当てるかを選択するかの違いになります。

1. コンテナインスタンス起動
2. リクエスト応答開始
3. リクエスト応答終了
4. コンテナインスタンスシャットダウン

#### リクエストの処理中のみCPUを割り当てる(リクエストベース方式)
こちらは上記の2〜3までの間にCPUを割り当て、その稼働時間によって課金されます。

コンテナインスタンスが起動していても、リクエストがない間はCPUの稼働時間もないので課金されません。

また、リクエスト数が月の無料枠を超えると、一定数ごとに課金されていきます。

レスポンスを返した後はCPUを解放してしまうので、応答後の非同期処理を実行することはできません。

#### CPUを常に割り当てる(インスタンスベース方式)
対してこちらは上記のライフサイクルの1〜4までの間にCPUを割り当て、その稼働時間によって課金されます。

リクエストがない間もコンテナインスタンスが起動している間は課金されることになります。

リクエスト数での課金は発生しませんが、その分スペックあたりの単価は、前述のリクエストベース方式より抑えられています。

また、コンテナインスタンスが生きている限りは常にCPUが割り当てられるので、応答後も非同期処理やバックグラウンドタスクを実行することができます。

どちらの料金体系を選択するかは、実行させたいアプリケーションの性質や、ユースケースによって左右されることでしょう。

例えば以下のような要素を持つアプリケーションの場合は、リクエストベース方式がお得です。
 - 特定の時間のみのアクセスが予想される
    - 例1: 夕方にのみアクセスされる
    - 例2: 日曜日だけアクセスされる
 - 処理自体が高速で完了する(非同期処理が必要ない)
 - リクエスト数が少ない

一方で、
- 恒常的なアクセスが予想される
    - 例: 全時間帯で平均的にアクセスされる
- 処理完了までに時間がかかる(もしくは応答後も非同期処理をする必要がある)
- リクエスト数が多い

といった場合には、インスタンスベース方式がお得になる場合もあります。

また、上記のCPU割り当て設定はいつでも変更可能なため、規模の拡大に合わせたり、一時的に設定を変更する、といった対応も可能です。

※詳細な料金計算については、こちらの[Cloud Runの料金](https://cloud.google.com/run/pricing?hl=ja)にてご確認ください。
加えて、稼働前にはこちらの[料金計算ツール](https://cloud.google.com/products/calculator?hl=ja)で事前に試算しておくことをオススメします。(クラウド破産にご注意ください)

### 3. デプロイが簡単
Cloud Runへのコンテナイメージのデプロイ方法は大きく分けて2つあります。

#### 方法その1: Dockerイメージを作成してデプロイする
この方法で開発者が行うべきことは、ざっくりと4ステップです。
1. デプロイしたいアプリを用意する
2. Dockerfileを作って、アプリをDockerイメージにする
3. Google Container Registryにイメージをアップロード
4. イメージを指定してCloud Runにアプリをデプロイ

慣れている方にとっては、これだけでも十分簡単に思われるかもしれませんが、

バックエンド未経験の私からすると、「Dockerfile？イメージ？なんそれ？」となってしまいました。
(後でちゃんと勉強しました)

また、今回は「いかに早く動くものを作れるか」ということがテーマですので、こちらの方法の詳細は割愛させていただきます。

#### 方法その2: ソースコードからのデプロイ
こちらの方法で開発者が行うべきことは、なんと2ステップだけです。
1. デプロイしたいアプリを用意する
2. Cloud Runへソースコードから直接アプリをデプロイ

実際には方法その1のステップ2〜4を、GCPがよしなにやってくれているだけなのですが、学習コストや環境構築に割く時間を短縮することができ、実行手順も簡単なので、今回はこちらの方法を使っていくことにします。
(詳細は後述します)

## APIを作って、Cloud Runへデプロイする
さて、前置きが長くなってしまいましたが、いよいよAPIを作って、Cloud Runへデプロイしていきたいと思います。

具体的には以下のステップに沿って開発していきます。
1. GCPの設定
2. 開発環境構築
2. Node.js + ExpressでAPI実装
3. Cloud Runへデプロイ

やればできる！

## 1. GCPの設定
といっても、実はコンソールからポチポチするだけで終わります。

### 1-1. プロジェクト作成
まずは支払い先を設定済みのGoogleアカウントを用意し、[GCPのコンソール](https://console.cloud.google.com)からGCPプロジェクトを作成します。

今回は例として仮想のプロジェクト名を `advent-calender-2023` にします。

![GCPプロジェクト作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/de18424e-c5d3-ab86-f0c7-f68904f96e91.png)

### 1-2. APIの有効化
続いて、デプロイに使用する以下の3つのAPIを有効化します。
- `Cloud Run Admin API`
- `Cloud Build API`
- `Artifact Registry API`

`Cloud Build API`と`Artifact Registry API`をなぜ有効化するのかは、本記事の後半で明らかになるので、一旦飛ばします。

また、どのAPIが有効化されているかは、[APIとサービス](https://console.cloud.google.com/apis/dashboard?hl=ja)にて確認できます。

![APIとサービス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/af922d4d-c576-855f-c015-1a59101b28d7.png)

## 2. 開発環境構築
続いて、ローカル環境にアプリの開発環境を構築していきます。

### 2-1. npmでプロジェクト作成
今回はNode.js + Expressで開発していくので、npmを使ってセットアップしていきます。

環境は以下を使用しています。
- npm: 10.2.3
- Node.js: v20.10.0

適当なディレクトリを用意し、以下を実行します。

1. `npm init`
2. `npm i express --save`
3. `package.json` を必要に応じて修正

```json
{
  "name": "sample_api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "engines": {
    "node": ">=16.0.0"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2"
  }
}
```

他に必要なものがないので、`package.json`は基本的に初期値のままです。

### 2-2. Google Cloud CLIのインストール
続いてデプロイ時に使用するGoogle Cloud CLIをインストールします。

1. [こちら](https://cloud.google.com/sdk/docs/install?hl=ja)の手順に従って、対象OS用のSDKをインストールしてください。
2. インストール後 `gcloud --version` で以下のようにバージョンが表示されればOKです。

```zsh
gcloud --version
Google Cloud SDK 447.0.0
bq 2.0.98
core 2023.09.15
gcloud-crc32c 1.0.0
gsutil 5.25
Updates are available for some Google Cloud CLI components.  To install them,
please run:
  $ gcloud components update
```

### 2-3. CLIでGCPにログインする
1. `gcloud auth login` を実行します。
2. ブラウザが立ち上がるので、先ほどプロジェクトを作成したGoogleアカウントを選択して認証を行って下さい。

認証が完了すると、以下のように表示されます。

```zsh
You are now logged in as [xxxx.xxxxx@gmail.com].
Your current project is [xxx-xxx].  You can change this setting by running:
  $ gcloud config set project PROJECT_ID
```

一応これで、Google Cloud SDKから対象のGoogleアカウントへのアクセス認証が完了しました。

### 2-4. ターゲットのプロジェクトを指定する

アカウントの認証は終わりましたが、実はどのプロジェクトに対して操作を行うのかをまだ正しく指定できていません。

というのも、GCPプロジェクトは1つのGoogleアカウントで複数作成することができます。

そのため、ターゲットを間違えると意図しないプロジェクトを操作することになるので注意が必要です。

`gcloud config set project [PROJECT_ID]` でターゲットを指定することができるので、

今回は先ほど作成した `advent-calender-2023` を設定します。

```zsh
gcloud config set project advent-calender-2023
Updated property [core/project].
```

ちなみに `gcloud projects list` でアカウントの持つ全てのプロジェクトを表示させることができます。

```zsh
gcloud projects list 
PROJECT_ID            NAME                  PROJECT_NUMBER
advent-calender-2023  advent-calender-2023  xxxxxxxxxxxx
```

一旦ここまでで、ローカル環境のセットアップは完了となります。

## 3. Node.js + ExpressでAPI実装

次は実際のAPIの処理部分を実装していきます。

以下のコードを `index.js` に記述します。

```javascript
const express = require('express')

const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.get('/get_sample', (req, res) => {
    console.log('ナイスネイチャ！')

    const param = req.query.text

    res.status(200).json(
        {
            param: param ?? ''
        }
    )
})

app.post('/post_sample', (req, res) => {
    console.log('ツインターボ！')

    const body1 = req.body.body1
    const body2 = req.body.body2

    res.status(200).json(
        {
            body1: body1 ?? '',
            body2: body2 ?? ''
        }
    )
})

// 404
app.use((req, res) => {
    console.log('マチカネタンホイザ！')
    res.status(404).send('Not Found')
})

app.listen(8080, () => {
    console.log(`app: listening on port 8080`)
})
```

特筆することもないですが、`get_sample` ではクエリパラメータの内容を、`post_sample` ではbodyの内容を返却するだけのAPIになります。

呼び出し時に適当なログが出るようにして、エンドポイントが見つからない場合は404を返すようにしておきます。

## 4. Cloud Runへデプロイ
さて、大変お待たせしました。

ようやくここからCloud Runへデプロイしていこうと思います。

### 4-1. デプロイコマンドを実行する

といっても、やることは以下のコマンドを叩くだけです。

```zsh
gcloud run deploy [サービス名] --source [SOURCE_PATH or URL] 
```

今回はCloud Runのサービス名を、わかりやすく`api`にしておきます。

また、初回の場合はCLI上でいくつかの質問に答える必要があります。

```zsh
Deploying from source requires an Artifact Registry Docker repository to store built containers. A repository named [cloud-run-source-deploy] in region 
[asia-northeast1] will be created.

Do you want to continue (Y/n)?  
```

1つ目は「コンテナイメージを保存するArtifact Registry Dockerリポジトリが必要なんだけど、無いから東京リージョン内に `cloud-run-source-deploy` っていう名前で作っていい？」と聞いてきています。

かまわんよ。

```zsh
This command is equivalent to running `gcloud builds submit --pack image=[IMAGE] .` and `gcloud run deploy api --image [IMAGE]`

Allow unauthenticated invocations to [api] (y/N)?  
```

2つ目は「このコマンドは、`gcloud builds submit --pack image=[IMAGE]` および `gcloud rundeploy api --image [IMAGE]` を実行するのと同じやで。
apiへの未認証の呼び出しを許可してもええんか？」と聞いてきています。

ちなみに未認証の呼び出しを許可しない場合、権限を持ったユーザー or サービスアカウントからの呼び出し以外受け付けなくなります。

今回は一般に公開するAPIということにして、YESにしておきます。

回答が終わると、コンテナイメージのビルドとCloud Runへのデプロイが始まります。

```zsh
gcloud run deploy api --source .
This command is equivalent to running `gcloud builds submit --pack image=[IMAGE] .` and `gcloud run deploy api --image [IMAGE]`

Building using Buildpacks and deploying container to Cloud Run service [api] in project [advent-calender-2023] region [asia-northeast1]
✓ Building and deploying... Done.                                                                                                                                     
  ✓ Uploading sources...                                                                                                                                              
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/xxxxxxxxx].    
  ✓ CreatingRevision...                                                                                                                                              
  ✓ Routing traffic...                                                                                                                                                
Done.                                                                                                                                                                 
Service [api] revision [api-00002-rmg] has been deployed and is serving 100 percent of traffic.
Service URL: https://hoge.run.app
```

上記のように出力されれば、正常にデプロイ完了です。

そしてこの `Service URL` が作成したAPIのエンドポイントURLになります。

### 4-2. 結果の確認

無事デプロイできたので、実際に `https://hoge.run.app/get_sample` にリクエストしてみます。

![get_sampleのレスポンス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/3bf68508-e648-7b5d-4141-48e8d5ed8a51.png)

ちゃんとレスポンスが返ってきてました。

せっかくなので、Cloud Loggingでログも確認してみます。

![get_sampleのログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/8775af93-6fa4-8f6f-8a58-ce436e7a052a.png)

愛しのナイスネイチャが出てますね。

今度は存在しない `https://hoge.run.app/delete_sample` にリクエストしてみます。

![delete_sampleのレスポンス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/62d24e4a-acbd-ddeb-abad-e16b88c312b6.png)

これも想定通り`Not Found`になりました。

こちらもログも確認してみます。

![delete_sampleのログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/a0e9d365-4ed1-c03d-7a37-83c148f20d57.png)

ちゃんとステータスが404で、マチカネタンホイザがログに出てますね。

( `/post_sample` も同様に実行されていることを確認しましたが割愛します。すまんな、ツインターボ)

--- 

## 結局 gcloud run deploy [サービス名] --source [SOURCE_PATH or URL] とは何なのか？

### Cloud Runの確認
動作も確認できたので、今度はGCPのコンソールでCloud Runを確認してみます。

![Cloud Runのサービス一覧](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/0f787b68-7bc0-6d5f-5046-fb7bf62684d9.png)
![Cloud Runのサービス詳細](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/c23e6d06-1c9d-584d-4114-cb6ceee83f1e.png)

ちゃんとおるぞ！

### Cloud Buildの確認
ついでに先ほどAPIを有効化した、Cloud BuildとArtifact Registryも確認してみましょう。

![Cloud Buildのビルド履歴](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/6b83a7e6-c10a-e27a-c7d8-5a10435a5f0b.png)

ん？なにやらビルドした履歴がありますね。

### Artifact Registryの確認

![Artifact Registry内のリポジトリ一覧](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/554fcb55-4236-22d1-7310-5eb2e3d87f80.png)

その名前は先ほどプロンプトで聞かれた`cloud-run-source-deploy`ではないか？

![cloud-run-source-deploy内のコンテナイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/a6a7be83-20b3-035d-02aa-45a19619eeea.png)

コンテナイメージらしきものが格納されているのを発見しました。

### まとめ
ここまでで、既にお分かりかと思いますが、`gcloud run deploy [サービス名] --source [SOURCE_PATH or URL]` とは
- 指定したソースコードをコンテナイメージにビルドする(Cloud Build)
- ビルドしたイメージをリポジトリに保存する(Artifact Registry)
- 保存したコンテナイメージを対象のCloud Runサービスへデプロイする

上記の3ステップを一貫して行ってくれるコマンドだったのです。

そのためにCloud BuildとArtifact RegistryのAPIを有効化する必要があったというわけです。

そしてこのコマンドのおかげで私は、Dockerfileを作成したり、イメージをビルドする作業をしなくて済んだのです。
(やりたくなかったわけではないのです。決して。)

## おまけ
さきほどデプロイしたCloud Runサービスですが、何もオプションを指定しなかったので、CPUやメモリが初期設定のままになっています。

ソースコードに変更がなければ、コンソールから変更してもいいのですが、せっかくなのでオプションを指定して再度デプロイしてみます。

```zsh
gcloud run deploy api --source . \
  --execution-environment gen2 \ #実行環境を最新の第2世代に指定
  --region asia-northeast1 \ #東京リージョン
  --cpu 1 \ #CPUの台数
  --memory 512Mi \ #メモリのスペック
  --max-instances 5 \ #最大インスタンス数
  --min-instances 0 \ #最小インスタンス数 = アイドル状態にしておくインスタンス数
  --concurrency 100 \ #1インスタンスあたりの同時処理可能なリクエストの数
  --cpu-throttling \ #CPUの割り当てをリクエスト処理中のみに制限
  --timeout 30s \ #タイムアウト
  --allow-unauthenticated \ #未認証の呼び出しを許可
  --ingress all \ #Ingressを許可
  --no-traffic #デプロイ後の自動反映をしない
```

### 結果
![Cloud Runの設定反映結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621544/0b3da7d6-20bf-444c-2d41-eb6d47bec5cf.png)

ちゃんと設定が反映されてました。

ちなみに2回目からは、指定していないオプションは前回の設定が引き継がれますので、
```zsh
gcloud run deploy api --source .
```
だけでも問題ありません。

## おわりに
今回はCloud Runの紹介と「いかに早く動くものを作れるか」ということをテーマに、簡易的なAPIを作ってデプロイするまでの手順を紹介しました。

全く畑違いの私でも簡単に作ることができたので、興味のある方はぜひ、Cloud Runを使ってAPIやWebアプリケーションを作ってみてはいかがでしょうか？

GCPには各種の無料枠もあり、個人利用ならよほどの事がなければ無料で使えるのも嬉しいところです。
(私も自分の結婚式のWeb招待状の開発に、Cloud Runを使用しました)

実際にP!TNEのバックエンド開発では、他にも多くの発見や学びがあったのですが、それはまた別の機会でご紹介しようと思います。

長くなってしまいましたが、最後まで読んでいただき、ありがとうございます！

明日の[レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)は13日目、【デザイン】新規サービス開発でデザイントークンを導入してみました！です。お楽しみに！

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/9931