概要
Google Cloud Platform (GCP)のGoogle Kubernetes Engine (GKE)を勉強するのに、ハンズオンが欲しかったので、Claude 3.5 Sonnetに作ってもらいました。
環境
- マシン:Macbook Air (M1チップ、2020)
- OS:Sonoma 14.6
- Docker:v4.33.0
- Node.js v20.5.1
- Google Cloud SDK 486.0.0
- MySQL Ver 8.0.36 for macos14.2 on arm64 (Homebrew)
アーキテクチャ
- フロントエンド:Next.js+React+Typescript
- バックエンド:Node.js+Express
- データベース:MySQL
ハンズオン
以下、基本Claude 3.5 Sonnetの出力をベースに書いていきます。自分で追加・修正した部分は【】内にその旨記述します。
(念のため記述しますが)以下のコマンドはCursolのコンソールウィンドウより実施しています。
前提条件
- Google Cloud Platformのアカウントを持っていること
- gcloud CLIツールがインストールされていること
- kubectl コマンドラインツールがインストールされていること
- Dockerがインストールされていること
ステップ1: プロジェクトのセットアップ
-
Google Cloud Consoleにログインし、新しいプロジェクトを作成します。
-
(追加) Google Cloud SDKからGCPアカウントにログインします。:
gcloud auth login
-
ターミナルで以下のコマンドを実行し、プロジェクトを設定します:
gcloud config set project YOUR_PROJECT_ID
-
必要なAPIを有効にします:
gcloud services enable container.googleapis.com
ステップ2: GKEクラスタの作成
- 以下のコマンドでGKEクラスタを作成します:
gcloud container clusters create my-cluster --num-nodes=3 --zone=us-central1-a
- クラスタの認証情報を取得します:
gcloud container clusters get-credentials my-cluster --zone=us-central1-a
ステップ3: アプリケーション作成
-
アプリケーションのディレクトリ構造を作成します:
mkdir my-app cd my-app mkdir frontend backend database
-
【追加】 frontendアプリの作成
cd frontend npx create-next-app frontend mv frontend/* . mv frontend/.* . rmdir frontend/
-
【一部修正】 各種ファイルの作成
next.config.mjs/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, }; export default nextConfig;
/src/app/page.tsximport { Suspense } from 'react'; import HelloWorld from '@/app/components/HelloWorld/page'; export default function Home() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <HelloWorld /> </Suspense> </div> ); }
/src/app/components/HelloWorld/page.tsx'use server' import React from 'react'; async function getMessage() { const backendUrl = `http://${process.env.BACKEND_SERVICE_HOST}:${process.env.BACKEND_SERVICE_PORT}`; const res = await fetch(`${backendUrl}/api/hello`, { cache: 'no-store' }); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); } export default async function HelloWorld() { const data = await getMessage(); return ( <div> <h1>The message from backend is:</h1> <h2>{data[0].message}</h2> </div> ); }
-
【追加】 backendアプリを作成
cd .. cd backend npm init
-
パッケージをインストール
npm install express mysql cors
-
server.jsを作成
const express = require('express'); const mysql = require('mysql2'); const cors = require('cors'); const app = express(); app.use(cors()); const db = mysql.createConnection({ host: 'database', user: 'root', password: 'mysql', database: 'testdb' }); db.connect((err) => { if (err) { console.error('Error connecting to database:', err); return; } console.log('Connected to database'); }); app.get('/api/hello', (req, res) => { db.query('SELECT * FROM hello_world', (err, results) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(results); }); }); const PORT = 5001; app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
-
databaseディレクトリ配下にinit.sqlファイルを作成
init.sqlUSE testdb; CREATE TABLE IF NOT EXISTS hello_world (id INT AUTO_INCREMENT PRIMARY KEY, message VARCHAR(255)); INSERT INTO hello_world (message) VALUES ('Hello World');
ステップ4: ローカル環境のDocker環境でアプリケーションをテスト
-
【nodeのバージョンのみ修正】 frontendディレクトリ配下にフロントエンド(React)のDockerfileを作成します:
frontend/DockerfileFROM node:20 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build CMD ["npm", "start"]
-
【nodeのバージョンのみ修正】 backendディレクトリ配下にバックエンド(Node.js)のDockerfileを作成します:
backend/DockerfileFROM node:20 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 5001 CMD ["node", "server.js"]
-
【mysqlのバージョンとMYSQL_DATABASEを修正】 databaseディレクトリ配下にデータベース(MySQL)のDockerfileを作成します:
database/DockerfileFROM mysql:8.0 ENV MYSQL_ROOT_PASSWORD=rootpassword ENV MYSQL_DATABASE=testdb COPY ./init.sql /docker-entrypoint-initdb.d/
MYSQL_ROOT_PASSWORD
はrootpassword
から予測しにくい文字列へ適宜変更してください。 -
【一部修正】 アプリケーションのルートディレクトリにdocker-compose.yamlを作成します:
docker-compose.yamlversion: '3' services: frontend: build: context: ./frontend/ dockerfile: Dockerfile ports: - "3000:3000" depends_on: - backend backend: build: context: ./backend/ dockerfile: Dockerfile ports: - "5001:5001" depends_on: - database environment: - DB_HOST=database - DB_USER=root - DB_PASSWORD=rootpassword - DB_NAME=testdb database: build: context: ./database/ dockerfile: Dockerfile ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql
MYSQL_ROOT_PASSWORD
はrootpassword
から予測しにくい文字列へ適宜変更してください。 -
docker composeでビルドします。:
docker compose up -d
-
【追加】 単体テストを行います。:
database:
docker exec -it my-app-database-1 bash
あるいはDocker Desktopのexecタブでコンテナ内に入り、以下を順に実行します。mysql -u root -p use testdb; select * from hello_world;
backend:
docker logs my-app-backend-1
またはDocker Desktopで以下のようなログが表示されればOKです。
以下のようなログが出力される場合は、databaseコンテナの起動が完了する前にbackendコンテナが起動してしまい、接続が上手くいっていません。コンテナを再起動してください。
Server running on port 5001 Error connecting to database: Error: connect ECONNREFUSED 172.18.0.2:3306 at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1159:16) { errno: -111, code: 'ECONNREFUSED', syscall: 'connect', address: '172.18.0.2', port: 3306, fatal: true }
:
frontend:
docker logs my-app-frontend-1
またはDocker Desktopで以下のようなログが表示されればOKです。
-
結合テストを行います。:
ブラウザでhttp://localhost:3000
にアクセスし、以下の画面が表示されていれば成功です。
ステップ5: アプリケーションのコンテナ化
-
【追加】 docker buildxを設定します(開発環境がMacbookのため):
docker buildx create --name mybuilder docker buildx use mybuilder docker buildx inspect --bootstrap
参考:Dockerの「マルチCPUアーキテクチャ」に対応したイメージをビルドする
筆者の開発環境はMacBook Air (M1)であり、プロセッサがARM64です。そのため、ローカル環境でdocker buildを実行すると、イメージのアーキテクチャがARM64になります。一方、GKEのデフォルトのCPUはx86_64であるため、ARM64イメージを利用すると動作しない場合があります。そこで、docker buildxを用いてマルチCPUアーキテクチャに対応したイメージをビルドします。
-
【修正】 各コンポーネントのDockerイメージをビルドします:
docker buildx build --platform linux/amd64,linux/arm64 -t gcr.io/YOUR_PROJECT_ID/frontend:v1 ./frontend --push docker buildx build --platform linux/amd64,linux/arm64 -t gcr.io/YOUR_PROJECT_ID/backend:v1 ./backend --push docker buildx build --platform linux/amd64,linux/arm64 -t gcr.io/YOUR_PROJECT_ID/database:v1 ./database --push
YOUR_PROJECT_ID
は自分のプロジェクトIDに置き換えてください
ステップ6: Kubernetes マニフェストの作成
- フロントエンドのデプロイメントとサービス:
frontend-deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: frontend spec: replicas: 3 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: containers: - name: frontend image: gcr.io/YOUR_PROJECT_ID/frontend:v1 ports: - containerPort: 3000 --- apiVersion: v1 kind: Service metadata: name: frontend spec: type: LoadBalancer ports: - port: 80 targetPort: 3000 selector: app: frontend
backend-deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: backend spec: replicas: 3 selector: matchLabels: app: backend template: metadata: labels: app: backend spec: containers: - name: backend image: gcr.io/YOUR_PROJECT_ID/backend:v1 ports: - containerPort: 5000 --- apiVersion: v1 kind: Service metadata: name: backend spec: ports: - port: 5000 selector: app: backend
database-deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: database spec: replicas: 1 selector: matchLabels: app: database template: metadata: labels: app: database spec: containers: - name: database image: gcr.io/YOUR_PROJECT_ID/database:v1 ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD value: rootpassword --- apiVersion: v1 kind: Service metadata: name: database spec: ports: - port: 3306 selector: app: database
YOUR_PROJECT_ID
は自分のプロジェクトIDに置き換えてください
ステップ7: アプリケーションのデプロイ
-
作成したマニフェストファイルを適用します:
kubectl apply -f frontend-deployment.yaml kubectl apply -f backend-deployment.yaml kubectl apply -f database-deployment.yaml
-
デプロイメントの状態を確認します:
kubectl get deployments kubectl get pods kubectl get services
-
【追加】 単体テストをします。:
database:
kubectl exec -it <databaseのpod名> -- /bin/bash
でコンテナ内に入り、以下を順に実行します。mysql -u root -p use testdb; select * from hello_world;
backend:
kubectl logs <backendのpod名>
で以下のようなログが表示されればOKです。
以下のようなログが出力される場合は、database podのコンテナの起動が完了する前にbackend podのコンテナが起動してしまい、接続が上手くいっていません。podを
kubectl rollout restart deployment backend
で再起動してください。Server running on port 5001 Error connecting to database: Error: connect ECONNREFUSED 172.18.0.2:3306 at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1159:16) { errno: -111, code: 'ECONNREFUSED', syscall: 'connect', address: '172.18.0.2', port: 3306, fatal: true }
:
-
結合テストをします。:
フロントエンドサービスの外部IPアドレスを取得します。kubectl get service frontend
ステップ8: アプリケーションへの後始末
-
クリーンアップ
ハンズオンが終了したら、リソースを削除してコストを節約します:
クラスタを削除します:gcloud container clusters delete my-cluster --zone=us-central1-a
Container Registryの画像を削除します:
gcloud container images delete gcr.io/YOUR_PROJECT_ID/frontend:v1 --force-delete-tags gcloud container images delete gcr.io/YOUR_PROJECT_ID/backend:v1 --force-delete-tags gcloud container images delete gcr.io/YOUR_PROJECT_ID/database:v1 --force-delete-tags
YOUR_PROJECT_ID
は自分のプロジェクトIDに置き換えてください