はじめに
こんにちは。
Google Cloudを業務で使い初めて、覚えることがたくさんありますが、勉強の一環で簡単なことでもなるべくGoogle Cloudのサービスを使うようにしています。
今回は作成したアプリをGoogle CloudのCloud Run functionsのサーバレス環境において共有する仕組みを利用したいと思います。
デプロイするアプリ
デプロイするアプリですが、React.jsを使ってアプリを作ってみました。趣味や仕事でアプリを作っていたのが5年くらい前のため、久しぶりのコーディングで、だいぶ仕様が変わった気がしますが、Geminiにアドバイスをもらいながら作りました。
昔コーディング練習でよく作っていたソートアルゴリズムをビジュアライズするものです。ソート自体は数字の並べ替えの仕組みなので、それをビジュアライズするといったものです。コーディングの練習として楽しいのでおすすめです。今回は久しぶりなので実装しやすいマージソートを選択しました。バブルソートが一番簡単そうですが、アルゴリズムがシンプルで、ビジュアライズしたときの様子も地味なのでバブルソートはやめました。
マージソートとは、データを小さな要素に分割し、それぞれをソート済みリストとして統合する「分割統治法」に基づくソートアルゴリズムです。具体的には、データを繰り返し2分割して要素が1つになるまで細分化し、その後、ソート済みになった小さなリスト同士を比較・結合(マージ)していくことで、最終的に全体を一つのソート済みリストにします。
詳細はWikipediaをご参照ください。また、以下のWikipediaページの右側の「マージソートの様子」というGIFアニメーションがビジュアライズのイメージとなります。
ソートにおけるビジュアライズは処理の途中に一時停止して、ブラウザに情報を反映する形となります。実装としてはジェネレータ関数およびyield関数を使いました。
以下は、マージソートアルゴリズム部分を記載します。これはCloud Run functionにデプロイするときにApp.jsxとして利用します。全体はReactJSで実装しています。
〜省略
function* mergeSortGenerator(inputArray) {
const a = inputArray.slice();
const aux = a.slice();
let comparisons = 0;
let writes = 0;
let maxDepth = 0;
let step = 0;
function* merge(lo, mid, hi, depth) {
for (let k = lo; k <= hi; k++) aux[k] = a[k];
let i = lo;
let j = mid + 1;
for (let k = lo; k <= hi; k++) {
let chosenIdx = -1;
if (i > mid) {
a[k] = aux[j++];
chosenIdx = j - 1;
} else if (j > hi) {
a[k] = aux[i++];
chosenIdx = i - 1;
} else if (aux[j] < aux[i]) {
comparisons++;
a[k] = aux[j++];
chosenIdx = j - 1;
} else {
comparisons++;
a[k] = aux[i++];
chosenIdx = i - 1;
}
writes++;
step++;
yield {
array: a.slice(),
highlights: {
lo,
mid,
hi,
i,
j,
k,
chosenIdx,
},
stats: { comparisons, writes, depth, maxDepth: Math.max(maxDepth, depth), step },
event: "write",
};
}
}
function* sort(lo, hi, depth) {
maxDepth = Math.max(maxDepth, depth);
if (lo >= hi) {
yield {
array: a.slice(),
highlights: { lo, mid: lo, hi, i: lo, j: hi, k: lo, chosenIdx: lo },
stats: { comparisons, writes, depth, maxDepth, step },
event: "base",
};
return;
}
const mid = Math.floor(lo + (hi - lo) / 2);
yield* sort(lo, mid, depth + 1);
yield* sort(mid + 1, hi, depth + 1);
// Merge start marker
yield {
array: a.slice(),
highlights: { lo, mid, hi, i: lo, j: mid + 1, k: lo, chosenIdx: -1 },
stats: { comparisons, writes, depth, maxDepth, step },
event: "mergeStart",
};
yield* merge(lo, mid, hi, depth);
// Merge end marker
yield {
array: a.slice(),
highlights: { lo, mid, hi, i: hi, j: hi, k: hi, chosenIdx: -1 },
stats: { comparisons, writes, depth, maxDepth, step },
event: "mergeEnd",
};
}
yield* sort(0, a.length - 1, 0);
}
省略〜
Cloud Run functionsへデプロイ
Cloud Run functionsにデプロイします。サーバレスなのでその他の設定は自動で実行されます。以下は、デプロイ向けに必要な資材とその中身です。
(1) ディレクトリ構成
merge-sort-visualizer-fn/
├─ package.json
├─ vite.config.js
├─ index.html
├─ postcss.config.js
├─ tailwind.config.js
├─ src/
│ ├─ main.jsx
│ ├─ App.jsx ← 作成したReactJSのコード
│ └─ index.css
└─ index.js ← Functions 入口(Express)
(2) 各資材の中身
package.json
{
"name": "merge-sort-visualizer-fn",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"gcp-build": "vite build",
"start": "functions-framework --target=app"
},
"dependencies": {
"@google-cloud/functions-framework": "^3.4.0",
"express": "^4.19.2",
"framer-motion": "^11.2.6",
"lucide-react": "^0.468.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"vite": "^5.4.2"
}
}
index.js
import functions from '@google-cloud/functions-framework';
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const distDir = path.join(__dirname, 'dist');
app.use('/assets', express.static(path.join(distDir, 'assets'), {
immutable: true, maxAge: '1y',
}));
app.use(express.static(distDir));
app.get('*', (_req, res) => {
res.sendFile(path.join(distDir, 'index.html'));
});
functions.http('app', app);
vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: '/merge-sort-viz-fun/'
build: { outDir: 'dist' }
});
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Merge Sort Visualizer</title>
</head>
<body class="min-h-screen">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html","./src/**/*.{js,jsx,ts,tsx}"],
theme: { extend: {} },
plugins: []
};
src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root { color-scheme: dark; }
src/main.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<React.StrictMode><App /></React.StrictMode>
);
(3) Cloud Run functionsへのデプロイ
準備ができましたので、これらの資材をCloud Run functionsへデプロイします。
# Google Cloud のプロジェクト
gcloud config set project <PROJECT_ID>
# Functions (Gen2) にデプロイ
gcloud functions deploy merge-sort-viz-fn \
--gen2 \
--region=asia-northeast1 \
--runtime=nodejs20 \
--entry-point=app \
--trigger-http \
--allow-unauthenticated
--set-build-env-vars NPM_CONFIG_PRODUCTION=false
デプロイ中に npm install → gcp-build(= vite build)が自動で実行され、dist/ が生成されます。
完了後に表示される URL にアクセスすれば、ブラウザでアプリが動作します。
ソート開始前
ソート中
ソート終了後
終わりに
今回はCloud Run functionsで簡単にデプロイし、実行することできました。少ない手順で動作でき、実行環境を気にせず、アプリ実装など自分の関心事に集中できました。
引き続き楽しみながらGoogle Cloudを勉強していきます。