Claude 3.5と歩むGKEハンズオン

# 概要
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: プロジェクトのセットアップ
1. Google Cloud Consoleにログインし、新しいプロジェクトを作成します。
1. (追加) Google Cloud SDKからGCPアカウントにログインします。：
    `gcloud auth login`
1. ターミナルで以下のコマンドを実行し、プロジェクトを設定します：
    `gcloud config set project YOUR_PROJECT_ID`
    
    :::note info
    `YOUR_PROJECT_ID`は自分のプロジェクトIDに置き換えてください。GCPコンソールの以下の画面でプロジェクトIDを確認できます。
    ![GCP_console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/d4df5517-1a32-badd-9e5d-6eea87dfd5c7.png)

1. 必要なAPIを有効にします：
`gcloud services enable container.googleapis.com`

## ステップ2: GKEクラスタの作成
1. 以下のコマンドでGKEクラスタを作成します：
`gcloud container clusters create my-cluster --num-nodes=3 --zone=us-central1-a`
1. クラスタの認証情報を取得します：
`gcloud container clusters get-credentials my-cluster --zone=us-central1-a`

## ステップ3: アプリケーション作成
1. アプリケーションのディレクトリ構造を作成します：
    ```bash
    mkdir my-app
    cd my-app
    mkdir frontend backend database
    ```
1. 【追加】 frontendアプリの作成
    ```bash
    cd frontend
    npx create-next-app frontend
    mv frontend/* .
    mv frontend/.* .
    rmdir frontend/
    ```
1. 【一部修正】 各種ファイルの作成
    ```react:next.config.mjs
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      reactStrictMode: true,
    };
    
    export default nextConfig;
    
    ```
    ```react:/src/app/page.tsx
    import { Suspense } from 'react';
    import HelloWorld from '@/app/components/HelloWorld/page';
    
    export default function Home() {
      return (
        <div>
          <Suspense fallback={<div>Loading...</div>}>
            <HelloWorld />
          </Suspense>
        </div>
      );
    }
    
    ```
    ```react:/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>
      );
    }
    ```
    
1. 【追加】 backendアプリを作成
    ```bash
    cd ..
    cd backend
    npm init
    ```
1. パッケージをインストール
    ```bash
    npm install express mysql cors
    ```
1. server.jsを作成
    ```javascript
    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}`));
    ```
1. databaseディレクトリ配下にinit.sqlファイルを作成
    ```sql:init.sql
    USE 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環境でアプリケーションをテスト
1. 【nodeのバージョンのみ修正】 frontendディレクトリ配下にフロントエンド（React）のDockerfileを作成します：
    ```yaml:frontend/Dockerfile
    FROM node:20
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    RUN npm run build
    CMD ["npm", "start"]
    ```
1. 【nodeのバージョンのみ修正】 backendディレクトリ配下にバックエンド（Node.js）のDockerfileを作成します：
    ```yaml:backend/Dockerfile
    FROM node:20
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 5001
    CMD ["node", "server.js"]
    ```
1. 【mysqlのバージョンとMYSQL_DATABASEを修正】 databaseディレクトリ配下にデータベース（MySQL）のDockerfileを作成します：
    ```yaml:database/Dockerfile
    FROM mysql:8.0
    ENV MYSQL_ROOT_PASSWORD=rootpassword
    ENV MYSQL_DATABASE=testdb
    COPY ./init.sql /docker-entrypoint-initdb.d/
    ```
    :::note info
    `MYSQL_ROOT_PASSWORD`は`rootpassword`から予測しにくい文字列へ適宜変更してください。
1. 【一部修正】 アプリケーションのルートディレクトリにdocker-compose.yamlを作成します：
    ```yaml:docker-compose.yaml
    version: '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
    ```
    :::note info
    `MYSQL_ROOT_PASSWORD`は`rootpassword`から予測しにくい文字列へ適宜変更してください。
1. docker composeでビルドします。：
    `docker compose up -d`
1. 【追加】 単体テストを行います。：
    database：
    `docker exec -it my-app-database-1 bash`あるいはDocker Desktopのexecタブでコンテナ内に入り、以下を順に実行します。
    ```sql
    mysql -u root -p
    use testdb;
    select * from hello_world;
    ```
    `Hello World`と表示されればOKです。
    ![mysql.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/fd06403a-7ff4-6604-50a1-1475ae7a89f9.png)


    backend：
    `docker logs my-app-backend-1`またはDocker Desktopで以下のようなログが表示されればOKです。
    ![backend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/cbcd1738-986a-209d-6b25-0d22a5dc2b97.png)
    
    :::note info
    以下のようなログが出力される場合は、databaseコンテナの起動が完了する前にbackendコンテナが起動してしまい、接続が上手くいっていません。コンテナを再起動してください。
    ```bash
    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です。
    ![frontend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/b5e5cd3a-8ca7-ab5b-db2c-48cd2080096a.png)
    
1. 結合テストを行います。：
    ブラウザで`http://localhost:3000`にアクセスし、以下の画面が表示されていれば成功です。
    ![成功画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/b0ef05a1-ff1f-a204-57b5-6ab538984db4.png)

    
## ステップ5: アプリケーションのコンテナ化
1. 【追加】 docker buildxを設定します（開発環境がMacbookのため）：
    ```bash
    docker buildx create --name mybuilder
    docker buildx use mybuilder
    docker buildx inspect --bootstrap
    ```
    参考：[Dockerの「マルチCPUアーキテクチャ」に対応したイメージをビルドする](https://qiita.com/SuyamaDaichi/items/cadc31df50fd42689661)

    :::note info
    筆者の開発環境はMacBook Air (M1)であり、プロセッサがARM64です。そのため、ローカル環境でdocker buildを実行すると、イメージのアーキテクチャがARM64になります。一方、GKEのデフォルトのCPUはx86_64であるため、ARM64イメージを利用すると動作しない場合があります。そこで、docker buildxを用いてマルチCPUアーキテクチャに対応したイメージをビルドします。
1. 【修正】 各コンポーネントのDockerイメージをビルドします：
    ```bash
    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
    ```
    :::note info
    `YOUR_PROJECT_ID`は自分のプロジェクトIDに置き換えてください

## ステップ6: Kubernetes マニフェストの作成
1. フロントエンドのデプロイメントとサービス：
    ```yaml: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
    ```
    バックエンドのデプロイメントとサービス：
    ```yaml:backend-deployment.yaml
    apiVersion: 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
    ```
    データベースのデプロイメントとサービス：
    ```yaml:database-deployment.yaml
    apiVersion: 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
    ```
    :::note info
    `YOUR_PROJECT_ID`は自分のプロジェクトIDに置き換えてください

## ステップ7: アプリケーションのデプロイ
1. 作成したマニフェストファイルを適用します：
    ```bash
    kubectl apply -f frontend-deployment.yaml
    kubectl apply -f backend-deployment.yaml
    kubectl apply -f database-deployment.yaml
    ```
1. デプロイメントの状態を確認します：
    ```bash
    kubectl get deployments
    kubectl get pods
    kubectl get services
    ```
    :::note info
    以下の説明では、`kubectl get pods`の出力結果の`NAME`列に表示されている文字列を「pod名」としています。
    ![kubectl_get_pods.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/520374d6-6a15-614f-3af2-8f8562c7eb3c.png)

1. 【追加】 単体テストをします。：
    database：
    `kubectl exec -it <databaseのpod名> -- /bin/bash`でコンテナ内に入り、以下を順に実行します。
    ```sql
    mysql -u root -p
    use testdb;
    select * from hello_world;
    ```
    `Hello World`と表示されればOKです。
    ![kubectl_database.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/0b434ced-8cbd-d07c-087e-c0eaba45a1c1.png)

    backend：
    `kubectl logs <backendのpod名>`で以下のようなログが表示されればOKです。
    ![kubectl_backend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/41db4e5e-d236-47cd-477c-3c236b4fe709.png)

    
    :::note info
    以下のようなログが出力される場合は、database podのコンテナの起動が完了する前にbackend podのコンテナが起動してしまい、接続が上手くいっていません。podを`kubectl rollout restart deployment backend`で再起動してください。
    ```bash
    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 <frontendのpod名>`で以下のようなログが表示されればOKです。
    ![kubectl_frontend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/50790811-3717-7ba3-082e-bac1d901c860.png)

    
1. 結合テストをします。：
    フロントエンドサービスの外部IPアドレスを取得します。
    ```bash
    kubectl get service frontend
    ```
    ![kubectl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/14905f09-986f-b8e3-b754-0074ae1bd6ec.png)
    EXTERNAL-IPに表示されているIPアドレスにアクセスして、以下のような画面が表示されれば成功です。
    ![成功画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/026eab56-2e1d-271e-8919-bffa103866dd.png)


## ステップ8: アプリケーションへの後始末
1. クリーンアップ
    ハンズオンが終了したら、リソースを削除してコストを節約します：
    クラスタを削除します：
    ```bash
    gcloud container clusters delete my-cluster --zone=us-central1-a
    ```
    
    Container Registryの画像を削除します：
    ```bash
    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
    ```
    :::note info
    `YOUR_PROJECT_ID`は自分のプロジェクトIDに置き換えてください

# 作業用フォルダ内ディレクトリ構造
![ディレクトリ構造.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118714/55844512-599d-8071-1305-950c627a5c69.png)