サイトアイコン 協栄情報ブログ

Cloud Run Functions でサーバーレスアプリをデプロイハンズオン

はじめに

仕事の都合で GoogleCloudを触り始めた、GoogleCloud歴 4か月の駆け出しエンジニアです。

GoogleCloudのCloud Run Functionsを利用することが多くなってきたので、こちらのデプロイに挑戦してみました。

Cloud Run Functionsとは?

名称の整理

世代 名称 備考
第1世代 Cloud Functions 独自の実行基盤
第2世代 Cloud Run Functions Cloud Runを基盤に動作

GoogleCloudブログ:
Cloud Functions は Cloud Run Functions に変わり、1 つの統合サーバーレス プラットフォームでのイベントドリブン プログラミングが可能になりました

GUIの画面

比較表

項目 Cloud Run Functions Lambda
対応言語 Node.js/Python/Go/Java/Ruby/PHP/.NET Node.js/Python/Java/Ruby/.NET/OS専用ランタイム
実行時間① HTTP関数:60分(同期) 15分(同期/非同期)
実行時間② イベントドリブン:9分(非同期)
料金体系 vCPU、メモリ、リクエスト数に基づく リクエスト数とコード実行時間に基づく

ハンズオン

注意点

構成

├── main.py             # Flask のアプリ
├── requirements.txt    # 必要なライブラリ
└── templates
    └── index.html      # Flask でレンダリングするHTML

1.Cloud Shellでの準備

1.1.事前準備

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.留意点
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」のなかみ
<!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.コマンドの内容
項番 記載名 主な内容
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.コマンド実行

3.2.WEB画面

4.片づけ

4.1.リソース削除コマンド

gcloud functions delete ${関数名(例:todo-app)} --region=asia-northeast1

おわりに

得られた知見

今後の課題

モバイルバージョンを終了