Cloud RunでWeb APIサーバーとReactアプリを同一コンテナ・同一ドメインで動かす

# 本記事で行うこと

Web APIサーバーとReactアプリを、同じDockerイメージ内にビルドして、1つのCloud Runで同一ドメインで動かします。

これを行う動機は以下の通りです。
- CORSのプリフライトリクエスト（Preflight request）を回避したい
- 小さなアプリなら1つのCloud Runで十分かも

# 参考URL（感謝します）

- [Deploy React and Nginx to Cloud Run](https://cloud.google.com/community/tutorials/deploy-react-nginx-cloud-run)

# 構成

Reactアプリ配信用のnginxにリバースプロキシの設定を追加して、APIへのリクエストをWeb APIサーバーに振り分けます。
そのとき、Web APIサーバーのAPI名の前に/api/というプレフィックスを付けて、Reactアプリから「/api/{API名}」でリクエストしてもらうことにします。

- Web APIサーバー
    - PORT 8081
- Reactアプリ
    - PORT 8080

# ソースコード

## Web APIサーバー（Go言語）

用意したAPIは「GET /hello」1つで、レスポンスとして「Hello!」というテキストを返します。

```main.go

package main

import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello!")
}

func main() {
	http.HandleFunc("/hello", hello)
	err := http.ListenAndServe("localhost:8081", nil)
	if err != nil {
		panic(err)
	}
}
```

```go.mod

module server

go 1.20
```

## Reactアプリ

ボタンをクリックすると「GET /api/hello」APIを呼んで、レスポンスを画面表示させます。

```App.tsx

import {useState} from 'react';
import axios from "axios";

function App() {
    const [message, setMessage] = useState("");

    const handler = () => {
        (async() => {
            const url = window.location.origin.toString() + "/api/hello";
            const response = await axios.get(url);
            setMessage(response.data);
        })();
    };

    return (
        <div>
            <button onClick={handler}>
                APIを呼ぶ
            </button>
            <p>
                {message}
            </p>
        </div>
    );
}

export default App
```

Viteの設定
- PORTを8080に設定します。
- ローカルでもAPIサーバーへのリバースプロキシが効くように設定します。

```vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8081',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  }
})
```

```package.json

{
  "name": "client",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.4.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.27",
    "@types/react-dom": "^18.0.10",
    "@vitejs/plugin-react": "^3.1.0",
    "typescript": "^4.9.3",
    "vite": "^4.1.0"
  }
}
```

# ローカルで動作確認

- Web APIサーバーの動作確認

ブラウザで http://localhost:8080/api/hello にアクセスすると、「Hello!」と表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/4c9a5bd2-4994-0375-7f07-86a26f677391.png)

- Reactアプリの動作確認

ブラウザで http://localhost:8080 にアクセスして「APIを呼ぶ」ボタンをクリックすると、「Hello!」と表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/a13e8588-19ff-c87e-7587-cf1a2e3e983b.png)

# ローカルのDockerで動作確認

## Docker用のファイルを準備

新たに3つのファイルを用意します。
- nginx.conf
- cmd.sh
- Dockerfile

docker buildする場所のディレクトリ構成は、以下の通りです。

```

current directory
  ├── server/         → Web APIサーバー
  │     ├── main.go
  │     └── go.mod
  ├── client/         → Reactアプリ
  │     ├── src/
  │     │     ├── App.tsx
  │     │    （以下省略）
  │     ├── public/
  │     │     └── （省略）
  │     ├── node_modules/
  │     │     └── （省略）
  │     ├── index.html
  │     ├── package.json
  │     ├── package-lock.json
  │     ├── tsconfig.json
  │     ├── tsconfig.node.json
  │     └── vite.config.ts
  ├── nginx.conf
  ├── cmd.sh
  └── Dockerfile
```

### nginx.conf

- [Deploy React and Nginx to Cloud Run](https://cloud.google.com/community/tutorials/deploy-react-nginx-cloud-run) のnginx.confに、Web APIサーバーへのリバースプロキシの設定を追加した内容です。

```nginx.conf

server {
     listen       $PORT;
     server_name  localhost;

     location / {
         root   /usr/share/nginx/html;
         index  index.html index.htm;
         try_files $uri /index.html;
     }
     location /api/ {
         proxy_pass http://localhost:8081/;
     }

     gzip on;
     gzip_vary on;
     gzip_min_length 10240;
     gzip_proxied expired no-cache no-store private auth;
     gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
     gzip_disable "MSIE [1-6]\.";

}
```

### cmd.sh

- Reactアプリ配信の部分は、[Deploy React and Nginx to Cloud Run](https://cloud.google.com/community/tutorials/deploy-react-nginx-cloud-run) のDockerfileを参考にしました。
- Web APIサーバー、Reactアプリ配信の2つをバックグラウンドで動かします。そのうち1つでも終了したら、シェルスクリプト自体を終了させます。

```cmd.sh

#!/bin/sh
set -eo pipefail

echo "exec server."
exec /app/server &

echo "exec client."
export PORT=8080
envsubst '\$PORT' < /etc/nginx/conf.d/configfile.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;' &

# Exit immediately when one of the background processes terminate.
echo "wait."
wait -n
```

### Dockerfile

- Reactアプリの部分は、[Deploy React and Nginx to Cloud Run](https://cloud.google.com/community/tutorials/deploy-react-nginx-cloud-run) のDockerfileをアレンジしました。
- 「go build environment」で、Web APIサーバーをビルドします。
- 「react build environment」で、Reactアプリをビルドします。
- ビルド結果を「run environment」にコピーして実行環境とします。

```Dockerfile

# go build environment
FROM golang:1.20-alpine as go-build
WORKDIR /app
COPY ./server ./_server
RUN go build -o server ./_server/main.go

# react build environment
FROM node:18-alpine as react-build
WORKDIR /app
COPY ./client ./
RUN npm ci
RUN npm run build

# run environment
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/configfile.template

COPY --from=go-build /app/server /app/
COPY --from=react-build /app/dist /usr/share/nginx/html

EXPOSE 8080

COPY ./cmd.sh /app/
RUN chmod +x /app/cmd.sh
CMD ["/app/cmd.sh"]
```

## docker build 例

```sh

docker build ./ -t example
```

## docker run 例

```sh

docker run --rm --name example1 -p 8080:8080 example
```

## 動作確認

- Web APIサーバーの動作確認

ブラウザで http://localhost:8080/api/hello にアクセスすると、「Hello!」と表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/76335aaa-9821-d406-e48c-1ad96f999b0d.png)

- Reactアプリの動作確認

ブラウザで http://localhost:8080 にアクセスして「APIを呼ぶ」ボタンをクリックすると、「Hello!」と表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/64562e5f-9002-e05d-3184-55218e4c45b1.png)

# Cloud Runで動作確認

## ビルド例

```sh

gcloud builds submit --tag gcr.io/your_project/exercise:your_tag
```

## デプロイ例

```sh

gcloud run deploy exercise \
    --image gcr.io/your_project/exercise:your_tag \
    --platform managed \
    --cpu 1 \
    --max-instances 1 \
    --concurrency 1 \
    --memory 1024Mi \
    --timeout 3600 \
    --allow-unauthenticated \
    --region us-central1 \
    --service-account your_identity
```

## 動作確認

- Web APIサーバーの動作確認

ブラウザで https://your_service_domain/api/hello にアクセスすると、「Hello!」と表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/45018924-76d0-4d69-ca67-816735fdbc1d.png)

- Reactアプリの動作確認

ブラウザで https://your_service_domain にアクセスして「APIを呼ぶ」ボタンをクリックすると、「Hello!」と表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/4d3efe25-d8f3-97b4-954e-dd76dcf9969e.png)