はじめに
仕事の都合で GoogleCloudを触り始めた、GoogleCloud歴 4か月の駆け出しエンジニアです。
GoogleCloudのCloud Run Functionsを利用することが多くなってきたので、こちらのデプロイに挑戦してみました。
Cloud Run Functionsとは?
-
Goolge Cloud が提供するサーバーレスのクラウドコンピューティングサービスのひとつ。ユーザーは「サーバーの存在を意識することなく」クラウド上でコードを実行することができます。
AWSを利用している方は「Lambda」と類似したサービスだと捉えていいかと思います。 -
HTTPリクエストをトリガに起動する「HTTP関数」や、特定のイベントをトリガに起動する「イベントドリブン関数」があり、それぞれのトリガによって最長実行時間も変わります。
名称の整理
-
2024年8月22日に「Cloud Functions(第1世代)」から、「Cloud Run Functions(第2世代)」にリブランディングされました。
-
Cloud Run はコンテナ化したアプリを実行する のに対し、Cloud Run Functions はコード単位で実行 するという違いがあります。
実行基盤が Cloud Run のコンテナであるため、Cloud Run Functions も自動スケール機能を備えており、リクエストに応じてインスタンスが増減します。
アイドル状態が続くとインスタンスがスケールダウンして 0 になる点も Cloud Run と共通しています。 -
私個人の観測になりますが「Cloud Run Functions」を、従来の「Cloud Functions」という名称で呼んでる方もいるので、個人的に混乱しました。
世代 | 名称 | 備考 |
---|---|---|
第1世代 | Cloud Functions | 独自の実行基盤 |
第2世代 | Cloud Run Functions | Cloud Runを基盤に動作 |
GoogleCloudブログ:
Cloud Functions は Cloud Run Functions に変わり、1 つの統合サーバーレス プラットフォームでのイベントドリブン プログラミングが可能になりました
GUIの画面
- 現在GUIでは「Cloud Functions」の作成は、「Cloud Run」の画面から構築します。
左上の赤枠「Cloud Run」とありますが緑枠の「Functions」を選択すると青枠「ランタイム」が表示され作成することが可能です。
比較表
- Cloud Run FunctionsとLambdaを比較してみました。
項目 | Cloud Run Functions | Lambda |
---|---|---|
対応言語 | Node.js/Python/Go/Java/Ruby/PHP/.NET | Node.js/Python/Java/Ruby/.NET/OS専用ランタイム |
実行時間① | HTTP関数:60分(同期) | 15分(同期/非同期) |
実行時間② | イベントドリブン:9分(非同期) | – |
料金体系 | vCPU、メモリ、リクエスト数に基づく | リクエスト数とコード実行時間に基づく |
ハンズオン
- 今回は「To-Doリストアプリ」を構築していきます。
注意点
- 実行環境は CloudShellにて実施するため、停止すると保存内容は削除されます。保存したい場合は別途GCSとの連携を考慮ください。
構成
- 以下構成で構築をする。
├── main.py # Flask のアプリ
├── requirements.txt # 必要なライブラリ
└── templates
└── index.html # Flask でレンダリングするHTML
1.Cloud Shellでの準備
1.1.事前準備
-
マネジメントコンソール右上 [>.] アイコンより、Cloud Shellの起動し以下コマンドを実施する。
-
Cloud Shellでディレクトリ作成
mkdir ${フォルダ名} && cd ${フォルダ名} touch main.py touch requirements.txt mkdir templates && cd templates touch index.html
1.2.「main.py」について
1.2.1.処理フロー
- 以下の処理フローを実装する。
項番 | 処理内容 | 出力ファイル |
---|---|---|
1 | To-Do リストの画面表示 | Flask が index.html をレンダリングし、画面を表示 |
2 | 新規タスクの追加 | API(POST /add))を呼出し、保存 |
3 | タスク一覧の取得 | API(GET /list)で呼出し、一覧取得 |
4 | タスク状態の切替え | API(POST /update)を呼出し、True/False 切替え |
5 | タスクの削除 | API(POST /delete_completed)で呼出し、タスク削除 |
1.2.2.留意点
- 本構築では
/tmp
にデータを保存する。そのため永続化をする場合はGCS
などの保存を考えること。
1.2.3.「app.py」のなかみ
※生成AIにて構築
import os
import json
from flask import Flask, request, jsonify, render_template
import functions_framework
app = Flask(__name__)
TMP_FILE = "/tmp/todo_list.json"
def load_todo_list():
"""To-Do リストを `/tmp` からロード"""
if os.path.exists(TMP_FILE):
with open(TMP_FILE, "r") as f:
return json.load(f)
return []
def save_todo_list(todo_list):
"""To-Do リストを `/tmp` に保存"""
with open(TMP_FILE, "w") as f:
json.dump(todo_list, f)
@app.route('/')
def index():
return render_template("index.html")
@app.route('/add', methods=['POST'])
def add_todo():
if not request.is_json:
return jsonify({"error": "Invalid content type, expected 'application/json'"}), 415
data = request.get_json()
todo_list = load_todo_list()
todo_list.append({
"task": data.get("task", "No Task"),
"deadline": data.get("deadline", "未設定"),
"completed": False
})
save_todo_list(todo_list)
return jsonify({"message": "Task added"})
@app.route('/list', methods=['GET'])
def get_todo():
todo_list = load_todo_list()
return jsonify({"tasks": todo_list})
@app.route('/complete', methods=['POST'])
def complete_todo():
if not request.is_json:
return jsonify({"error": "Invalid content type, expected 'application/json'"}), 415
data = request.get_json()
index = data.get("index")
todo_list = load_todo_list()
if index is not None and 0 <= index < len(todo_list):
todo_list[index]["completed"] = not todo_list[index]["completed"]
save_todo_list(todo_list)
return jsonify({"message": "Task status updated"})
return jsonify({"error": "Invalid task index"}), 400
@app.route('/delete_completed', methods=['POST'])
def delete_completed():
todo_list = load_todo_list()
todo_list = [task for task in todo_list if not task["completed"]]
save_todo_list(todo_list)
return jsonify({"message": "Completed tasks deleted"})
@functions_framework.http
def main(request):
"""Cloud Functions のリクエストを Flask に適用"""
with app.test_request_context(request.path, method=request.method, json=request.get_json()):
return app.full_dispatch_request()
1.2.「requirements.txt」について
1.2.1.ライブラリ内容
項番 | 記載名 | 主な内容 |
---|---|---|
1 | flask | 軽量なWebフレームワーク。APIエンドポイントの作成と index.html のレンダリングを担当 |
2 | functions-framework | Cloud Functions で Flask を実行するためのフレームワーク |
1.2.2.「requirements.txt」のなかみ
flask
functions-framework
1.3.「index.html」について
1.3.1.「index.html」のなかみ
- L44: 「const API_URL = "https://asia-northeast1-${プロジェクトID}.cloudfunctions.net/todo-app”;」部分は、各自のプロジェクトIDを入力する。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>てつてつToDoリスト</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.task-item {
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.completed {
text-decoration: line-through;
background-color: #f8f8f8;
}
.controls {
margin: 20px 0;
}
input, button {
padding: 8px;
margin-right: 5px;
}
</style>
</head>
<body>
<h1>てつてつToDoリスト</h1>
<div class="controls">
<input type="text" id="taskInput" placeholder="タスクを入力">
<input type="date" id="deadlineInput">
<button onclick="addTask()">追加</button>
<button onclick="deleteCompleted()">完了済みを削除</button>
</div>
<ul id="taskList"></ul>
<script>
const API_URL = "https://asia-northeast1-${プロジェクトID}.cloudfunctions.net/todo-app";
// ページ読み込み時にタスク一覧を取得
document.addEventListener('DOMContentLoaded', () => {
loadTasks();
});
// タスク一覧を取得して表示する関数
async function loadTasks() {
try {
const response = await fetch(`${API_URL}/list`);
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const data = await response.json();
console.log("
タスク一覧:", data);
const taskList = document.getElementById('taskList');
taskList.innerHTML = '';
if (data.tasks && data.tasks.length > 0) {
data.tasks.forEach((task, index) => {
const li = document.createElement('li');
li.className = `task-item ${task.completed ? 'completed' : ''}`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = task.completed;
checkbox.onchange = () => toggleTaskComplete(index, !task.completed);
const taskText = document.createElement('span');
taskText.textContent = `${task.task} (期限: ${task.deadline || '未設定'})`;
li.appendChild(checkbox);
li.appendChild(taskText);
taskList.appendChild(li);
});
} else {
taskList.innerHTML = '<li>タスクがありません</li>';
}
} catch (err) {
console.error("
タスク取得エラー:", err);
document.getElementById('taskList').innerHTML =
'<li style="color: red;">タスクの取得に失敗しました。ネットワーク接続を確認してください。</li>';
}
}
// 新しいタスクを追加する関数
async function addTask() {
const taskInput = document.getElementById('taskInput');
const deadlineInput = document.getElementById('deadlineInput');
const task = taskInput.value.trim();
if (!task) {
alert('タスクを入力してください');
return;
}
const taskData = {
task: task,
deadline: deadlineInput.value || '未設定'
};
console.log("
タスク追加リクエスト:", taskData);
try {
const response = await fetch(`${API_URL}/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(taskData)
});
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
console.log("
タスク追加成功");
taskInput.value = '';
deadlineInput.value = '';
loadTasks(); // タスク一覧を再読み込み
} catch (err) {
console.error("
タスク追加エラー:", err);
alert('タスクの追加に失敗しました');
}
}
// タスクの完了状態を切り替える関数
async function toggleTaskComplete(index, completed) {
console.log(`
タスク ${index} の完了状態を ${completed ? '完了' : '未完了'} に変更`);
try {
const response = await fetch(`${API_URL}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
index: index,
completed: completed
})
});
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const data = await response.json();
console.log("
タスク更新成功:", data);
// タスク一覧を再読み込み(UI更新)
loadTasks();
} catch (err) {
console.error("
タスク更新エラー:", err);
alert('タスクの更新に失敗しました');
// エラーが発生した場合、UI上のチェックボックスを元の状態に戻す
loadTasks();
}
}
// 完了済みタスクを削除する関数
async function deleteCompleted() {
console.log("
`/delete_completed` へリクエスト送信:", `${API_URL}/delete_completed`);
try {
const response = await fetch(`${API_URL}/delete_completed`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
// 空のJSONを送信する場合は、空のオブジェクトを文字列化
body: JSON.stringify({})
});
console.log("
`/delete_completed` レスポンスステータス:", response.status);
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const data = await response.json();
console.log("
`/delete_completed` 成功:", data);
// タスク一覧を再取得
loadTasks();
} catch (err) {
console.error("
`/delete_completed` エラー:", err);
alert('完了済みタスクの削除に失敗しました');
}
}
</script>
</body>
</html>
2.Cloud Run Functionsへデプロイ
2.1.デプロイコマンド
2.1.1.コマンドの内容
- PoCのため「認証なしで誰でもアクセス可能」の設定です。こちらの設定でデプロイする場合、セキュリティの観点より構築確認後速やかに削除ください。
- 削除コマンドは
4.1.リソース削除コマンド
にて記載
項番 | 記載名 | 主な内容 |
---|---|---|
1 | –runtime python311 | Python 3.11 環境で実行 |
2 | –trigger-http | HTTP リクエストで実行(Web API 用) |
3 | –allow-unauthenticated | 認証なしで誰でもアクセス可能 |
4 | –entry-point main | Cloud Functions のエントリーポイントを main に指定 |
5 | –region asia-northeast1 | 東京リージョンにデプロイ |
2.1.2.コマンド実行
- 以下、デプロイコマンドを実施
gcloud functions deploy ${関数名(例:todo-app)} \ --runtime python311 \ --trigger-http \ --allow-unauthenticated \ --entry-point main \ --region asia-northeast1
-以下、デプロイ後表示されるURLを押下
url: https://asia-northeast1-{プロジェクトID}.cloudfunctions.net/todo-app
3.デプロイ画面
3.1.WEB画面
- URLアクセス直後の画面
3.2.WEB画面
- タスクを追加して、チェックを入れてタスク終了の画面(「完了済みを削除」押下で一覧からは削除される)
4.片づけ
4.1.リソース削除コマンド
gcloud functions delete ${関数名(例:todo-app)} --region=asia-northeast1
おわりに
得られた知見
- GoogleCloud ShellからのCloud Run Functionsのデプロイ方法
- Cloud Run FunctionsとLambdaの違い
- WEBアプリを通しての フロント-バックエンドの構築
今後の課題
- Cloud Run Functions と Cloud Runの差異について調査。
- AWS ECS(Fargate)などコンテナサービスについての調査。