Cloud Run Servicesの実践

# **はじめに**

この記事ではいくつかの演習を通じてCloud Run Servicesの実践的な使い方を紹介します。

なおCloud Runは有料のサービスなので料金については事前に[公式サイト](https://cloud.google.com/run/pricing?hl=ja)で確認することをお勧めします。

また「Cloud RunとかCloud Run Servicesって何？」となってる方は[過去の記事「怖くないCloud Run」](https://qiita.com/__SATO__/items/d8228b27147dc4c580cb)を読んで見ていただけると幸いです。



# **演習の準備**


## **プロジェクトの作成**

演習に使用するプロジェクトがない場合は以下の手順でプロジェクトを作成してください。

1. [**Google Cloud ダッシュボード**](https://console.cloud.google.com/projectselector2/home/dashboard?hl=ja)を開きます  
2. 「プロジェクトを作成」を選択します  
3. プロジェクトの作成画面が開いたら任意のプロジェクト名を入力して「作成」をクリックしてください

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/474d9f85-b48d-d1ea-df22-cfa776c30f5c.png)

   ※ 本記事では以降プロジェクト名は「cloud-run-drill」とします
   ※ プロジェクト名は任意ですが、以降の手順で必要になるのは"プロジェクトID"の方です
   ※ プロジェクトIDは世界中で一意である必要があります
   ※ 以後"cloud-run-drill"というプロジェクトIDは各自の環境に読み替えてください


## **Google Cloud APIの有効化**

演習で必要になるGoogle Cloud APIを有効化していきます。

- Artifact Registry APIの有効化
  [**Artifact Registry APIの詳細ページ**](https://console.cloud.google.com/apis/api/artifactregistry.googleapis.com/metrics?hl=ja)を開いて「有効にする」を選択してください

- Cloud Run Admin APIの有効化
  [**Cloud Run Admin APIの詳細ページ**](https://console.cloud.google.com/apis/api/run.googleapis.com/metrics?hl=ja)を開いて「有効にする」を選択してください


## **メンバーへの権限付与**

プロジェクトのオーナー以外のメンバーに作業してもらうには、該当メンバーに権限を付与する必要があります。

1. [**「IAMと管理」の「IAM」**](https://console.cloud.google.com/iam-admin/iam?hl=ja)を開いてください。  
2. 「アクセス権を付与」を選択してください

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/abf4e810-dec3-add9-ba58-f9260faf7c74.png)
   <br>

3. 「新しいプリンシパル」欄にメンバーのメールアドレスを入力してください。（複数記載可能）  
4. 「ロールを選択」または「別のロールを追加」を選択して次のロールを追加してください

    - Artifact Registry 管理者  
    - Cloud Run デベロッパー  
    - Cloud Run 起動元  
    - Big Query データ編集者  
    - Big Query ジョブユーザ  
    - ストレージ フォルダ 管理者  
    - サービス アカウント ユーザ  
    - ログ 閲覧者
    <br>

5. ロールの追加が完了したら「保存」をクリックしてください



## **サービスアカウントの設定**

Cloud Runではデフォルトでジョブやサービスを実行するためのサービスアカウントが作成され、アプリの実行中はそのサービスアカウントとしてBigQueryの呼び出しなどが行われます。

今回のサービスはBigQueryを使用するので一連の処理（ワークロード）を実行するサービスアカウントにBigQueryを利用する権限を付与する必要があります。

次の手順でサービスアカウントに権限を付与してください。

1. 権限の管理画面を開く
    [「IAMと管理」の「IAM」](https://console.cloud.google.com/iam-admin/iam?hl=ja)を開いてください。
    
1. サービスアカウントを選択
    プリンシパル（アカウント）の一覧から「名前」欄に「Default compute service account」と書かれているアカウントを探して右横にある編集アイコンをクリックしてください。

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/b79c5c2a-f5ae-6e5d-477f-233476a87baa.png)
    <br>
    
1. ロールを付与
    「ロールを追加」または「別のロールを追加」を選択して次のロールを付与してください。

    - Big Query ジョブ ユーザー
    - Big Query データ 編集者


## **アプリのインストール**

本演習では[**Cloud Shell Editor**](https://shell.cloud.google.com/?hl=ja&show=ide&environment_deployment=ide)を利用するので不要になりますが、ローカル環境で演習を進めたい方は次のアプリをインストールしてください。

- [**Google Cloud CLI**](https://cloud.google.com/sdk/docs/install?hl=ja)：
    コマンドラインからGoogle Cloudのリソースを操作するのに使います

- [**Docker Desktop**](https://docs.docker.jp/desktop/install.html)：
    コンテナイメージのビルドやプッシュに使います


## **Artifact Registryの作成**

Cloud Runで実行するコンテナを保管する場所としてArtifact Registryのリポジトリを用意します。

1. アカウントを認証する

   Google Cloud CLI (gcloud) で次のコマンドを実行します

   ```sh
   gcloud init
   ```
   <br>

2. リポジトリを作成する

    ```sh
    gcloud artifacts repositories create sample-repo \
    --repository-format=docker \
    --location=asia-northeast1 \
    --project="cloud-run-drill" \
    --description="Cloud Run Sample Repository"
    ```   
    ※ create sample-repo: "sample-repo"という名前のリポジトリを作成
    ※ \--location: コンテナイメージのアップロード先リージョン
    ※ \--project: コンテナイメージのアップロード先プロジェクト
    ※ \--description: リポジトリの説明文(省略可)
    <br>

3. リポジトリへの接続を認証する
    ```sh
    gcloud auth configure-docker asia-northeast1-docker.pkg.dev
    ```

## **テーブルの作成**

演習ではBigQueryのテーブルを使います。次の操作を実行してください。

なお、本記事ではBigQueryを採用したのは単にこの記事を執筆している時点で使用していたのがBigQueryだったからというだけです。  
Cloud Run ServicesはWebサービスのデプロイなどもできるため、作りたいものに応じたDBの選定を推奨します。

1. データセットの作成

	データセットは一般的なデータベースで言うところのスキーマにあたるものです。  
	データセットは[**BigQueryStudio**](https://console.cloud.google.com/bigquery?hl=ja)でブラウザから操作することも可能ですが、
    簡単のため次のコマンドで作成します。

    ```sh
    bq --location=asia-northeast1 mk --dataset crd
    ```
    <br>

2. テーブルの作成 

   [**BigQueryStudio**](https://console.cloud.google.com/bigquery?hl=ja)を開いて次のSQLを実行してください。

   ```sql
   -- 商品マスタの作成
   CREATE TABLE crd.m_product (
   id STRING NOT NULL,
   name STRING NOT NULL,
   price FLOAT64 NOT NULL
   );

   INSERT INTO crd.m_product
   (id, name, price)
   VALUES
   ('P001', 'apple', 120),
   ('P002', 'orange', 80),
   ('P003', 'banana', 90);

   -- 販売マスタの作成
   CREATE TABLE crd.m_sale (
   id STRING NOT NULL,
   buyer STRING,
   sale_date DATE DEFAULT (CURRENT_DATE()),
   product_id STRING NOT NULL,
   sale_quantity INT64 NOT NULL
   );
   
   INSERT INTO crd.m_sale
   (id, buyer, sale_date, product_id, sale_quantity)
   VALUES
   ('S001', 'Newton', DATE '2024-07-01', 'P001', 8),
   ('S002', 'Gorilla', DATE '2024-07-01', 'P003', 5),
   ('S003', NULL, DATE '2024-07-01', 'P002', 10),
   ('S004', 'Newton', DATE '2024-07-02', 'P001', 6),
   ('S005', 'Newton', DATE '2024-07-02', 'P002', 2),
   ('S006', 'Gorilla', DATE '2024-07-02', 'P003', 5);

   -- ログテーブルの作成
   CREATE TABLE crd.visit (
   visited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
   );
   ```


## **バケットの作成**

Google Cloud CLIで次のコマンドを実行してください。

```sh
gsutil mb -l asia-northeast1 gs://crd-temporary
```

※ gsutilはGoogle Cloud CLIのコマンドの一種です  
※ mb: make bucketの略  
※ -l: バケットのリージョン(location)を指定  
※ バケット名は世界中でユニークである必要があります  
※ 以後"crd-temporary"というバゲット名は各自の環境に読み替えてください



# **サービスをデプロイする**
まずは一番シンプルなジョブを作ってみましょう。


## **プロジェクトの構成**
任意の場所に次のような作業ディレクトリを作成しましょう。

```text
deploy-service/  
├─ Dockerfile  
├─ main.py  
└─ requirements.txt
```

## **ファイルの編集**

### **Dockerfile**

プロジェクトルートのDockerfileを次のように編集してください。

```dockerfile
FROM python:3.11-slim

COPY . .

RUN pip install --upgrade pip  
RUN pip install --no-cache-dir -r requirements.txt

ENV TZ=Asia/Tokyo

ENTRYPOINT ["python", "main.py"]
```

### **requirements.txt**

requirements.txtにはアプリが必要とするライブラリを記述します。次のように編集してください。

```text
Flask==3.0.3  
gunicorn==22.0.0  
Werkzeug==3.0.3
```

### **main.py**

プロジェクトルートのmain.pyを次のように編集してください。

```python
import os  
from flask import Flask

app = Flask(__name__)

@app.route("/")  
def hello_world():  
    name = os.environ.get("NAME", "World")  
    return f"Hello {name}!\n"

if __name__ == "__main__":  
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
```


## **デプロイ**

### **コンテナイメージのプッシュ**

1. コンテナイメージの作成

   プロジェクトルートで次のコマンドを実行してコンテナイメージを作成します。

   ```sh
   docker build -t deploy-service .
   ```
   <br>   

2. イメージにタグをつける
   ```sh
   docker tag deploy-service:latest \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/deploy-service:latest
   ```
   <br>   

3. イメージをプッシュする

    ```sh		  
    docker push \
    asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/deploy-service:latest
    ```

### **サービスの作成**

次のコマンドでサービスを作成します。

```sh
gcloud run deploy deploy-service \
--image asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/deploy-service:latest \
--allow-unauthenticated \
--region=asia-northeast1
```
※ deploy deploy-service: "deploy-service"という名前のサービスを作成  
※ \--image: コンテナイメージのURIを指定  
※ \--region: サービスをデプロイするリージョンを指定  
※ \--allow-unauthenticated: 未認証のユーザのアクセスを許可


## **動作確認**

1. エントリポイントの確認

   つぎのコマンドでサービスのURLを確認します。

   ```sh
   gcloud run services describe deploy-service --region=asia-northeast1
   ```
   <br>

2. エントリポイントをたたく

	次のコマンドを実行して「Hello World」と返ってくることを確認します。

    ```sh
    curl https://deploy-sample-xxxxxxxxxx.a.run.app
    ```
    <br>

3. 環境変数を設定してみる

	main.pyにはNAMEという環境変数を参照する部分があったので環境変数NAMEを定義してみます。

    ```sh
    gcloud run services update deploy-sample \
    --set-env-vars NAME=Sugar \
    --region=asia-northeast1
    ```
    <br>

4. 再度サービスを実行してみる   

   環境変数NAMEが定義された状態でアプリの挙動が変わることを確認します。

   ```sh
   # サービスのURLを確認
   gcloud run services describe deploy-sample --region=asia-northeast1
   # サービスを実行
   curl https://deploy-sample-xxxxxxxxxx.a.run.app
   ```

### **クリーンアップ**

1. サービスの削除

   ```sh
   gcloud run services delete routing --project cloud-run-drill --region asia-northeast1
   ```
   <br>   

2. イメージの削除

   ```sh
   gcloud artifacts docker images delete \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/routing
   ```



# **RESTful API**

Cloud Run ServiceはRESTful APIの開発にも利用できます。


## **プロジェクトの構成**

任意の場所に次のような作業ディレクトリを作成しましょう。

```text
root/  
├─ Dockerfile  
├─ main.py  
└─ requirements.txt
```


## **ファイルの編集**

### **Dockerfile**

プロジェクトルートのDockerfileを次のように編集してください。

```dockerfile
FROM python:3.11-slim

COPY . .

RUN pip install \--upgrade pip  
RUN pip install \--no-cache-dir \-r requirements.txt

ENV TZ\=Asia/Tokyo

ENTRYPOINT \["python", "main.py"\]
```

### **requirements.txt**

requirements.txtにはアプリが必要とするライブラリを記述します。次のように編集してください。

```text
Flask\==3.0.3  
gunicorn\==22.0.0  
Werkzeug\==3.0.3
```

### **main.py**

プロジェクトルートのmain.pyを次のように編集してください。

```python
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

tasks = [
    {"id": 1, "title": "Task 1"},
    {"id": 2, "title": "Task 2"}
]

@app.route("/tasks")
def get_all_tasks():
    return jsonify(tasks)

@app.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id: int):
    for task in tasks:
        if task["id"] == task_id:
            return jsonify(task)
    return jsonify({"error": "Task not found"}), 404

@app.route("/tasks", methods=["POST"])
def create_task():
    if not request.json or not "title" in request.json:
        return jsonify({"error": "Bad Request"}), 400
    new_task_id = len(tasks) + 1 or 1
    new_title = request.json["title"]
    new_task = {"id": new_task_id, "title": new_title}
    tasks.append(new_task)
    return jsonify(new_task), 201

@app.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id: int):
    global tasks
    if not request.json:
        return jsonify({"error": "Bad Request"}), 400
    for task in tasks:
        if task["id"] == task_id:
            task["title"] = request.json.get("title", task["title"])
            return jsonify(task)
    return jsonify({"error": "Task not found"}), 404

@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id: int):
    global tasks
    for task in tasks:
        if task["id"] == task_id:
            tasks = [t for t in tasks if t["id"] != task_id]
            return jsonify({"result": f"Task(id: {task_id}) is deleted"})
    return jsonify({"result": f"Task not found"}), 404

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
```


## **デプロイ**

### **コンテナイメージのプッシュ**

1. コンテナイメージの作成

   プロジェクトルートで次のコマンドを実行してコンテナイメージを作成します。

   ```sh
   docker build -t restful .
   ```   

2. イメージにタグをつける

   ```sh
   docker tag restful:latest \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/restful:latest
   ```
   <br>
   

3. イメージをプッシュする

   ```sh
   docker push asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/restful:latest
   ```

### **サービスの作成**

次のコマンドでサービスを作成します。

```sh
gcloud run deploy restful \
--image asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/restful:latest \
--region=asia-northeast1 \
--allow-unauthenticated
```  
※ deploy restful: "restful"という名前のサービスを作成
※ --image: コンテナイメージのURIを指定
※ --region: サービスをデプロイするリージョンを指定
※ --allow-unauthenticated: 未認証のユーザのアクセスを許可


## **動作確認**

1. エントリポイントの確認

   つぎのコマンドでサービスのURLを確認します。

   ```sh
   gcloud run services describe restful --region=asia-northeast1
   ```
   <br>

2. すべてのタスクが返ってくるか確認

	次のコマンドを実行してすべてのタスクが返ってくることを確認します。

    ```sh
    curl https://restful-xxxxxxxxxx.a.run.app/tasks
    ```
    <br>

3. 特定のタスクが返ってくるか確認

	次のコマンドで指定したタスクが返ってくることを確認します。

    ```sh
    curl https://restful-xxxxxxxxxx.a.run.app/tasks/1
    ```
    <br>

4. タスクを追加できるか確認

   次のコマンドでタスクを追加できるか確認します。

   ```sh
   curl -X POST https://restful-eejfhtafva-an.a.run.app/tasks \
   -H "Content-Type: application/json" \
   -d '{"title": "New Task"}' 

   curl https://restful-xxxxxxxxxx-an.a.run.app/tasks
   ```
   <br>

5. タスクを更新できるか確認

   次のコマンドでタスクを更新できるか確認します。

   ```sh
   curl -X PUT https://restful-eejfhtafva-an.a.run.app/tasks/3 \
   -H "Content-Type: application/json" \
   -d '{"title": "Task3"}'

   curl https://restful-xxxxxxxxxx-an.a.run.app/tasks
   ```
   <br>

6. タスクを削除できるか確認 

   次のコマンドでタスクを削除できるか確認します。

   ```sh
   curl -X DELETE https://restful-eejfhtafva-an.a.run.app/tasks/1   

   curl https://restful-xxxxxxxxxx-an.a.run.app/tasks
   ```

### **クリーンアップ**

1. サービスの削除

   ```sh
   gcloud run services delete restful --project cloud-run-drill --region asia-northeast1
   ```
   <br>   

2. イメージの削除 

   ```sh
   gcloud artifacts docker images delete \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/restful
   ```
   <br>



# **Webサイトのホスティング**

Cloud Run ServicesはURLをグローバルに公開できるのでWebサイトのホスティングにも利用できます。

ここでは話を簡単にするためシンプルな静的サイトを作ってみましょう。

## **プロジェクトの構成**

任意の場所に次のような作業ディレクトリを作成しましょう。

```text
root/  
├─ src/  
│　├─ static  
│　│  ├─ css  
│　│  │  └─ style.css  
│　│  └─ js  
│　│     └─ script.js  
│　├─ templates  
│　│  ├─ about.html  
│　│  └─ index.html  
│　└─ app.py  
├─ Dockerfile  
├─ requirements.txt  
└─ workflow.md
```


## **ファイルの編集**

### **Dockerfile**

プロジェクトルートのDockerfileを次のように編集してください。

```dockerfile
FROM python:3.11-slim

COPY requirements.txt .

RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt

COPY src /src

WORKDIR /src

ENV TZ=Asia/Tokyo

ENTRYPOINT ["python", "app.py"]
```

### **requirements.txt**

requirements.txtにはアプリが必要とするライブラリを記述します。次のように編集してください。

```text
Flask\==3.0.3
```

### **app.py**

srcディレクトリ直下のapp.pyを次のように編集してください。

```python
from flask import Flask, render_template

app = Flask(__name__)


@app.route('/')
def index():
    return render_template('index.html')

@app.route('/about')
def about():
    return render_template('about.html')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)
```

### **CSS**
src/static/jsディレクトリのstyle.cssを次のように編集してください。

```css
body {
    font-family: Arial, sans-serif;
    background-color: #0f0f0f;
    color: #f0f0f0;
}

h1 {
    color: #EEE;
}

button {
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
}
```

### **JavaScript**
src/static/jsディレクトリのstyles.cssを次のように編集してください。

```javascript
document
    .getElementById('alert-button')
    .addEventListener('click', function () {
        alert('ボタンがクリックされました！');
    });
```

### **HTML**

src/templatesディレクトリのindex.htmlを次のように編集してください。  

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Web Site</title>
    <link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}">
</head>
<body>
    <h1>Sample Web Page</h1>
    <p>visite <a href="/about">more about this page</a></p>
</body>
</html>
```

また、src/templateディレクトリのabout.htmlは次のように編集してください。  

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Web Site</title>
    <link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}">
</head>
<body>
    <h1>Sample Web Page</h1>
    <p>This is a Flask app powered by Cloud Run Services!</p>

    <button id="alert-button">click here</button>

    <script src="{{url_for('static', filename='js/script.js')}}"></script>
</body>
</html>
```


## **デプロイ**

### **コンテナイメージのプッシュ**	
1. コンテナイメージの作成  

   プロジェクトルートで次のコマンドを実行してコンテナイメージを作成します。

   ```sh
   docker build -t sample-website .
   ```
   ※ ローカルテストは「docker run \-p 8080:8080 sample-website」で実行できます
   <br>

2. イメージにタグをつける

   ```sh
   docker tag sample-website:latest \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/sample-website:latest
   ```
   <br>   

3. イメージをプッシュする

   ```sh
   docker push asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/sample-website:latest
   ```
   <br>

### **サービスの作成**

次のコマンドでサービスを作成します。

```sh
gcloud run deploy sample-website \
--image asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/sample-website:latest \
--region=asia-northeast1 \
--allow-unauthenticated
```
※ deploy sample-website: "sample-website"という名前のサービスを作成
※ --image: コンテナイメージのURIを指定
※ --region: サービスをデプロイするリージョンを指定
※ --allow-unauthenticated: 未認証のユーザのアクセスを許可（※世界中にサービスが公開されます）


## **動作確認**

1. エントリポイントの確認
   つぎのコマンドでサービスのURLを確認します

   ```sh
   gcloud run services describe sample-website --region=asia-northeast1
   ```
   <br>

2. ブラウザでページを確認
    サービスのURLをブラウザに入力してページが問題なく表示されるか確認してください。  

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/1362bab8-c556-b3eb-6bec-1417d871edea.png)
    <br>

3. ページを移動できるか確認 

   ページ上の「more about this page」をクリックして「About Page」というタイトルのページに移動できるか確認してください。

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/fe8d9594-8101-c8ad-c5d4-80e56acad11e.png)
   <br>


4. CSSやJavaScriptが効いていることを確認

	CSSが効いていることは見た目から明らかですが、JavaScriptも効いているか確認するため「click here」ボタンを押してみてください。  
		  
	正常に動作していれば「ボタンがクリックされました！」というアラート画面が表示されるはずです。  
	
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/3bed94b4-03e7-5664-08c3-4646dd49b1a8.png)
    <br>

### **クリーンアップ**

1. サービスの削除

   ```sh
   gcloud run services delete sample-website \
   --project cloud-run-drill \
   --region asia-northeast1
   ```
   <br>

2. イメージの削除   

   ```sh
   gcloud artifacts docker images delete \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/sample-website
   ```
   <br>

# **３層アーキテクチャ**

WebアプリやWebサイトのシステムは次のような層に分けて作られることが多く、このようなアーキテクチャは一般的に3層アーキテクチャと呼ばれています。

- プレゼンテーション層（≒ブラウザ）  
- アプリケーション層（≒Webサーバ）  
- データ層（≒DBサーバ）

本記事では最後にWebを中心にしたシステムの大定番である3層アーキテクチャの実装を試してみましょう。

## **プロジェクトの構成**

任意の場所に次のような作業ディレクトリを作成しましょう。

```text
root/  
├─ src/  
│　├─ static  
│　│  ├─ css  
│　│  │  └─ style.css  
│　│  └─ js  
│　│     └─ script.js  
│　├─ templates  
│　│  ├─ about.html  
│　│  └─ index.html  
│　└─ app.py  
├─ Dockerfile  
├─ requirements.txt  
└─ workflow.md
```


## **ファイルの編集**

### **Dockerfile**

プロジェクトルートのDockerfileを次のように編集してください。

```dockerfile
FROM python:3.11-slim

COPY requirements.txt .

RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt

COPY src /src

WORKDIR /src

ENV TZ=Asia/Tokyo

ENTRYPOINT ["python", "app.py"]
```

### **requirements.txt**

requirements.txtにはアプリが必要とするライブラリを記述します。次のように編集してください。

```text
Flask==3.0.3
google-cloud-bigquery==3.25.0
pytz==2024.1
```

### **app.py**

srcディレクトリ直下のapp.pyを次のように編集してください。

```python
from datetime import datetime

import pytz
from flask import Flask, render_template, request, jsonify
from google.cloud import bigquery
from google.cloud.bigquery import ScalarQueryParameter

PROJECT_ID = 'cloud-run-drill'
DATASET_NAME = 'crd'
TABLE_NAME = 'visit'

app = Flask(__name__)


@app.route('/')
def index():
    add_visit_log()
    return render_template('index.html')

@app.route('/api/visit_logs')
def visit_logs():
    logs = get_visit_logs()
    return jsonify(logs)

def add_visit_log():
    db_client = bigquery.Client()
    sql = f"""
    INSERT INTO {PROJECT_ID}.{DATASET_NAME}.{TABLE_NAME}
    (visited_at) 
    VALUES 
    (@visit_timestamp)
    ;
    """
    current_time = get_current_time_at_tokyo()
    params = [
        ScalarQueryParameter(
            'visit_timestamp', 'TIMESTAMP', current_time
        )
    ]
    config = bigquery.QueryJobConfig(query_parameters=params)
    job = db_client.query(sql, job_config=config)
    db_client.close()
        
def get_visit_logs():
    db_client = bigquery.Client()
    sql = f"""
    SELECT * 
        FROM {PROJECT_ID}.{DATASET_NAME}.{TABLE_NAME}
        WHERE visited_at >= @start_of_today
    ;
    """
    start_of_today = get_current_time_at_tokyo().replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    params = [
        ScalarQueryParameter(
            'start_of_today', 'TIMESTAMP', start_of_today
        )
    ]
    config = bigquery.QueryJobConfig(query_parameters=params)
    job = db_client.query(sql, job_config=config)
    result = job.result()
    logs = []
    for res in result:
        for key, value in res.items():
            logs.append(
                {key: str(value)}
            )
    db_client.close()
    return logs

def get_current_time_at_tokyo():
    tz = pytz.timezone('Asia/Tokyo')
    return datetime.now(tz)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)
```

### **CSS**

src/static/cssディレクトリのstyle.cssを次のように編集してください。

```css
body {
    font-family: Arial, sans-serif;
    background-color: #0f0f0f;
    color: #f0f0f0;
}

h1 {
    color: #EEE;
}

button {
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
}
```

### **JavaScript**

src/static/jsディレクトリのstyles.cssを次のように編集してください。

```javascript
const API_URI = '/api/visit_logs';

document
    .addEventListener('DOMContentLoaded', load_visit_logs);

document
    .getElementById('update-button')
    .addEventListener('click', () => {
        document.querySelector('#counter emb').textContent = '~~~';
        load_visit_logs();
    });

async function load_visit_logs() {
    try {
        const response = await fetch(API_URI);
        const logs = await response.json();
        const logCount = logs.length;
        const logCountStr = String(logCount);
        document.querySelector('#counter emb').textContent = logCountStr;
    } catch(error) {
        console.error('Error fetching logs: ', error);
    }
}
```

### **HTML**

src/templatesディレクトリのindex.htmlを次のように編集してください。  

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Web Site</title>
    <link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}">
</head>
<body>
    <h1>Sample Web Page</h1>
    
    <p id="counter">
        <emb>~~~</emb> requests!
    </p>

    <button id="update-button">update count</button>

    <script src="{{url_for('static', filename='js/script.js')}}"></script>
</body>
</html>
```


## **デプロイ**

### **コンテナイメージのプッシュ**

1. コンテナイメージの作成  

   プロジェクトルートで次のコマンドを実行してコンテナイメージを作成します。

   ```sh
   docker build \-t three-tier-app .
   ```    

2. イメージにタグをつける

   ```sh
   docker tag three-tier-app:latest \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/three-tier-app:latest
   ```   

3. イメージをプッシュする

   ```sh
   docker push asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/three-tier-app:latest
   ```

### **サービスの作成**

次のコマンドでサービスを作成します。

```sh
gcloud run deploy three-tier-app \
--image asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/three-tier-app:latest \\
--region=asia-northeast1 \
--no-allow-unauthenticated
```
※ deploy three-tier-app: "three-tier-app"という名前のサービスを作成
※ --image: コンテナイメージのURIを指定
※ --region: サービスをデプロイするリージョンを指定
※ --no-allow-unauthenticated: 認証済みのユーザのみ許可します


## **動作確認**

1. エントリポイントの確認

   つぎのコマンドでサービスのURLを確認します。

   ```sh
   gcloud run services describe three-tier-app --region=asia-northeast1
   ```
   <br>

2. プロキシサーバを立てる

   認証済みユーザのみ許可する場合、ページを開くにはWebサーバにユーザの認証情報を提供する必要があるのですが、これは結構面倒な作業です。だからここでは話を簡単にするためGoogle Cloud CLIのホスティング機能を利用します。   

   Google Cloud CLIを使えば次のコマンドを実行するだけで簡単にプロキシサーバを立てられます。

   ```sh
   gcloud run services proxy three-tier-app \
   --project cloud-run-drill \
   --region asia-northeast1
   ```  
   
   コマンドを実行すると `http://127.0.0.1:8080` のようなローカルホストのURLが表示されるのでポート番号等を控えてください。
   <br>

3. ブラウザでページを確認

	Cloud Shell Editorを使っている場合、ローカルホストのブラウジングは画面右上の「ウェブでプレビュー」を選択してプロキシサーバが開放しているポートを設定して「ポートXXXXでプレビュー」を選択します。

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/58de84af-a3ad-6263-2f89-210001b9df95.png)  
		  
	また、自分の端末で実行している場合は単純にブラウザからプロキシサーバのURLを開けばページが表示されるはずです。  
		  
	![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/04c00bb0-853b-c1f7-3f6b-fa187894a836.png)


### **クリーンアップ**

3. サービスの削除

   ```sh
   gcloud run services delete three-tier-app \
   --project cloud-run-drill \
   --region asia-northeast1
   ```
   <br>

4. イメージの削除   

   ```sh
   gcloud artifacts docker images delete \
   asia-northeast1-docker.pkg.dev/cloud-run-drill/sample-repo/three-tier-app
   ```
   <br>