Minikube × Kubernetes × SkaffoldでPHP＋MariaDBの開発環境を構築

# Minikubeで開発環境を作成する

## 前提条件

私の環境はWindows10です。

## ツールのダウンロード

minikubeを以下からインストールしてください：  
https://minikube.sigs.k8s.io/docs/start/

minikubeにはvirtualboxドライバーを使います。  
以下からvirtualboxをインストールしてください：  
http://virtualbox.org/wiki/Downloads

Gitも使用します。以下からGitをインストールしてください：  
https://git-scm.com/downloads

skaffoldも以下からインストールしてください：  
https://skaffold.dev/docs/install/

そしてkubectlも以下からインストールします：  
https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/

（chocoやscoopを使うと簡単にインストールできます）

## インストール確認

それぞれのツールをインストール後、PowerShellでminikubeにアクセスできるか確認しましょう。

PowerShellを開いて次のコマンドを実行：

```
minikube version
```

出力例：

```
minikube version: v1.35.0
commit: dd5d320e41b5451cdf3c01891bc4e13d189586ed-dirty
```

こんな感じになるはず。
![cap.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260163/e1f1548c-4282-4dd2-b46a-28ad3cac816e.png)

次にGitのバージョン確認：

```
git -v
```

出力例：

```
git version 2.47.1.windows.2
```

## テストプロジェクトの作成

次のようにディレクトリを作成：

```
cd Documents
mkdir projects
cd projects
mkdir test-project
cd test-project
```

このプロジェクトはminikubeのクラスターで動かします。

## Minikubeクラスターの起動

### 用語解説

- **コンテナとは？**  
  アプリケーションとその依存関係をまとめた軽量な分離環境です。様々な環境間で一貫性を保てます。

- **ポッドとは？**  
  Kubernetesにおける最小のデプロイ単位で、1つ以上のコンテナを内包します。

- **クラスターとは？**  
  複数のワーカーノードがポッドを実行する、Kubernetesの最上位構成です。

### クラスター起動

以下のコマンドで起動：

```
minikube start --driver=virtualbox
```

※Dockerドライバーを使う場合は次のようにマウントするフォルダを指定しつつ起動できます：

```
minikube start --mount --mount-string="$HOME:/src" --driver=docker
```

ただし、virtualboxではこれは使用できません（でもDocker Desktopと違って無料です）
UbuntuなどのLinux系はDockerを無料で使えるので試してみてください。仕組み上仕方ないのですが、Dockerは爆速なのに対してvirtualboxは遅いのでイライラします。
Windows/MacはDockerを使うならDocker desktopになっちゃうので、とりあえずvirtualboxでいきます。

## Kubernetesのインストール確認

minikube経由でkubernetes（kubectl）を確認します。
kubectlを直接使えることも確かめておきます。

```
minikube kubectl -- version
kubectl -- version
```

出力例：

```
Client Version: v1.32.0  
Kustomize Version: v5.5.0  
Server Version: v1.32.0
```

## ポッドの作成（PHP, Nginx, MariaDB）

まずIngressを有効にします：

```
minikube addons enable ingress
```

次に以下の内容を `php-mariadb.yaml` に保存します。  
このファイルにはポッド内のコンテナ定義が含まれています。  
**このファイルの改行コードは「CRLF」ではなく「LF」にしてください。**

以下が `php-mariadb.yaml` の内容です：

```
# MariaDBのDeployment
apiVersion: apps/v1
kind: Deployment
metadata:
  # Deploymentの名前
  name: mariadb
  # Deploymentを区別するためのラベル
  labels:
    app: mariadb
spec:
  # MariaDBインスタンスの数
  replicas: 1
  selector:
    # Podのためのmatch label
    matchLabels:
      app: mariadb
  template:
    metadata:
      labels:
        app: mariadb
    spec:
      containers:
        - name: mariadb
          # MariaDBのためのDocker image
          image: mariadb:11.3
          # MariaDBの動くport
          ports:
            - containerPort: 3306
          # MariaDBの環境変数
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: "rootpassword"
            - name: MYSQL_DATABASE
              value: "mydatabase"
            - name: MYSQL_USER
              value: "myuser"
            - name: MYSQL_PASSWORD
              value: "mypassword"
          # MariaDB data storageのボリュームマウンティング
          volumeMounts:
            - name: mariadb-storage
              mountPath: /var/lib/mysql
      # volumeの定義
      volumes:
        - name: mariadb-storage
          emptyDir: {}
---
# MariaDB Service
apiVersion: v1
kind: Service
metadata:
  # MariaDBサービス名
  name: mariadb
spec:
  ports:
    - port: 3306
  selector:
    # MariaDB podに接続するためのセレクター
    app: mariadb
---
# PHPのWebアプリ用のDeployment
apiVersion: apps/v1
kind: Deployment
metadata:
  # Deployment名
  name: php-app
  # PHPのdeploymentを区別するためのラベル
  labels:
    app: php-app
spec:
  # PHPポッドのインスタンス数
  replicas: 1
  selector:
    matchLabels:
      app: php-app
  template:
    metadata:
      labels:
        app: php-app
    spec:
      containers:
        - name: php-app
          # PHPのウェブアプリのインスタンス数
          image: php:8.3-apache
          # PHP/Apacheの動くポート番号
          ports:
            - containerPort: 80
          # Virtualbox内のminikubeがホストマシンとなりますが、そのホストマシンへのマウント設定
          volumeMounts:
            - name: php-app-code
              mountPath: /var/www/html
            - name: apache-config
              mountPath: /etc/apache2/sites-enabled/000-default.conf
              subPath: default
      # ボリューム定義
      volumes:
        - name: php-app-code
          hostPath:
            path: "/mnt/project"
            type: Directory
        - name: apache-config
          configMap:
            name: apache-config
---
# Apacheの設定のコンフィグマップ
apiVersion: v1
kind: ConfigMap
metadata:
  name: apache-config
data:
  default: |
    <VirtualHost *:80>
        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html
        <Directory "/var/www/html">
            Options Indexes FollowSymLinks
            AllowOverride All
            Require all granted
            DirectoryIndex index.html index.php
        </Directory>
        ErrorLog /var/log/apache2/error.log
        CustomLog /var/log/apache2/access.log combined
    </VirtualHost>
---
# PHPのウェブアプリ用のService
apiVersion: v1
kind: Service
metadata:
  # Service名
  name: php-app-service
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
  selector:
    # PHPのPODに接続するためのセレクター
    app: php-app
---
# Ingressのリソース（PHPのウェブアプリに接続するためのNginxのこと。めんどくさい部分を色々初期設定済みにしてくれているのがIngress。別に自分でNginxを位置から定義してクラスターへの入口を作りたい人はそれでもいい）
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: php-app-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: localhost
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: php-app-service
                port:
                  number: 80
```
## skaffold.yamlの作成

次の内容を `skaffold.yaml` として保存します：

```
apiVersion: skaffold/v4beta12
kind: Config
metadata:
  name: php-mariadb-app
deploy:
  kubectl: {}
manifests:
  rawYaml:
    - php-mariadb.yaml
```
そうすると`test-project`のなかがこうなるはずです。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260163/954fdf92-72a6-4435-a7e4-63695d201dd6.png)

## マウント用ディレクトリ作成

まず、マウント用のディレクトリを作成します：

```
mkdir mounttest
```

以下のコマンドでホストとminikubeをマウント接続します：

```
minikube mount "$Home\\Documents\\projects\\test-project\\mounttest:/mnt/project"
```

このコマンドはマウント状態を維持するため、**このターミナルは開いたまま**にしておいてください。

以降の作業は**別ターミナルを開いて実行**します。


## skaffoldを使ったデプロイ

以下のコマンドを実行して、開発環境を開始します（`test-project` ディレクトリで実行）：

```
cd {test-projectのディレクトリ}
skaffold delete
skaffold run --force
```

これも実行中にログを出力し続けるため、別の作業をする場合は**さらに別のターミナル**を使いましょう。

## 環境の動作確認

数分待つと、Kubernetes環境がminikubeクラスター上に構築されます。

次のコマンドでポッドの状態を確認します：

```
kubectl get pods
```

出力例：

```
NAME                      READY   STATUS    RESTARTS   AGE
mariadb-998f96ddb-84kqs   1/1     Running   0          64s
php-app-bf6f77579-454jl   1/1     Running   0          64s
```

`READY` がすべて `1/1` であれば、環境は正常に動作しています。  
これらは「ポッド（Pod）」と呼ばれます。

例：

- mariadb-998f96ddb-84kqs（ポッド）
- php-app-bf6f77579-454jl（ポッド）

それぞれのポッドの中に、`.yaml` で定義したコンテナが動作しています。  
1つのポッドに複数コンテナを含めることも可能です。たとえば、ログ用の「サイドカーコンテナ」を一緒に動かすこともできます。

## ポッドのトラブルシューティング

もし `READY` が `1/1` にならない場合は、以下でポッドの詳細を確認します：

```
kubectl describe pod php-app-bf6f77579-454jl
```

標準出力のログを確認するには：

```
kubectl logs php-app-bf6f77579-454jl
```

クラスタ全体のイベントを確認するには：

```
kubectl get events --sort-by='.metadata.creationTimestamp'
```

## ホストPCからminikubeへアクセス

## Ingressでファイルを確認

まずはminikube tunnelをつかっておきます。
```
minikube tunnel
```
minikubeはVirtual machine内でk8s環境をつくります。通常はHost PCに直接k8s環境を作りますが、今回は開発用にVirtual machine内にk8sを作りました。このk8s環境にHostからアクセスしようとするとPort forwardなどの対応が必要になっちゃいます。

minikube  tunnelをするとアクセスできるようになります。（環境によってはアクセスできないかもなので、その場合はあきらめてkubectlでport forwardingしましょう…）

minikube tunnelは一度実行すると動き続けるので、次の作業は別ターミナルを開いて行います。

IngressのIPを確認します。
```
kubectl get ingress
NAME              CLASS   HOSTS       ADDRESS          PORTS   AGE
php-app-ingress   nginx   localhost   192.168.59.100   80      93
```
IPは192.168.59.100とわかりました。

ブラウザで `http://192.168.59.100` にアクセスすると、 `404` エラーが表示されます。  
これが表示されれば、Ingress経由で接続できている証拠です。

![キャプチャ (1).PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260163/800da3a8-4859-4261-ad21-c4706d9c6c49.png)


今度はこのIPを使って次のように `.yaml` を修正します：

```
# Ingress resource for PHP Application
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: php-app-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: 192.168.59.100.nip.io #!!! ここをかえる !!!!
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: php-app-service
                port:
                  number: 80
```
## PHPファイルを作成して動作確認

`mounttest` に `index.php` を作成します：

```
<?php
echo "Hello World!";
```

ブラウザで次にアクセスします：

```
http://192.168.59.100.nip.io/
```

「Hello World!」が表示されれば成功！

![キャプチャ (2).PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260163/04a9be00-d21c-464c-9f04-7ab252da1916.png)

なんで最初は404だったのにIPを`localhost`から`192.168.59.100.nip.io`に書き換えるとアクセスできるようになったのでしょうか？
.yaml の中のIngress定義では、最初こうなっていました：
```
rules:
  - host: localhost
```
つまりこのIngressルールは「ホスト名が localhost のリクエストだけを処理する」という設定です。

でも、minikube が作った仮想ネットワーク（例：192.168.59.100）は、ホストOSの localhost とは別世界のIPアドレスです。なのでブラウザから 192.168.59.100 にアクセスすると、

Ingressコントローラーが
> ホスト名が一致しない (localhostじゃない) から

適用するルールがなくて 404 を返すという状態になります。

なぜ 192.168.59.100.nip.io に変えるとアクセスできるのかというと
```
rules:
  - host: 192.168.59.100.nip.io
```
に変更することで、Ingressがこのホスト名を認識して、

「あ、これは自分が担当するホスト名だ！」

と判断して、ルーティング処理してくれるようになるためです。

:::note warn
ここでのキモ
nip.io は無料のDNSサービスで、xxx.xxx.xxx.xxx.nip.io にアクセスすると自動でそのIPに名前解決される。
192.168.59.100.nip.io → DNSが 192.168.59.100 に解決してくれる。
つまり「DNSで名前をつけつつ、Ingressの host 条件にも合う」ということになる。
:::

## PHPコードの変更が即時反映されない場合

`php.ini` に次の設定を追加・変更して、`opcache` を無効にしてください：

```
opcache.enable=0
opcache.validate_timestamps=1
opcache.revalidate_freq=0
```

これで、`mounttest` フォルダのPHPを変更すれば、kubernetes内のpodに即時反映されるようになります。

## MariaDBをホストPCから使う（NodePort）

MariaDBのService定義をNodePortに変更する必要があります。以下は修正済みの `php-mariadb.yaml` の該当部分です：

```
# MariaDB Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mariadb
  labels:
    app: mariadb
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mariadb
  template:
    metadata:
      labels:
        app: mariadb
    spec:
      containers:
        - name: mariadb
          image: mariadb:11.3
          ports:
            - containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: "rootpassword"
            - name: MYSQL_DATABASE
              value: "mydatabase"
            - name: MYSQL_USER
              value: "myuser"
            - name: MYSQL_PASSWORD
              value: "mypassword"
          volumeMounts:
            - name: mariadb-storage
              mountPath: /var/lib/mysql
      volumes:
        - name: mariadb-storage
          emptyDir: {}
---
# NodePort Service
kind: Service
apiVersion: v1
metadata:
  name: mariadb
spec:
  type: NodePort
  selector:
    app: mariadb
  ports:
    - port: 3306
      targetPort: 3306
      protocol: TCP
      nodePort: 30036  # 任意のポート番号（例：30000〜32767）
```

## 再デプロイ

`skaffold run` を止めて（Ctrl+C）、以下のコマンドで再デプロイします：

```
skaffold delete
skaffold run --force
```

minikube tunnelもやり直したほうがいいかもしれません。

## MinikubeのIP確認

```
minikube ip
```

出力例：

```
192.168.59.100
```

このIPとNodePort（例：30036）を組み合わせてMariaDBにアクセスできます。

例：

```
192.168.59.100:30036
```

## MariaDBへの接続情報（環境変数）

MariaDBの接続には以下の設定が使われています：

```
ユーザー名: myuser  
パスワード: mypassword  
データベース: mydatabase  
ルートパスワード: rootpassword  
```

この情報を使って、ホスト側のMySQLクライアントやDBツール（A5SQLmk2やDBeaverなど）から接続できます。

![キャプチャ (3).PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260163/efbfbf78-fd9f-4567-b7b5-a54b0e255054.png)
(A5SQLmk2から接続した際のキャプチャです)

## まとめ

これで、

- PHPファイルを編集するとリアルタイム反映されるKubernetes環境  
- Ingress経由でアクセス可能なWebサーバ  
- NodePort経由でMariaDBに外部から接続可能

な**本格的なローカルKubernetes開発環境**が完成しました！

## おまけリンク集

- Skaffold公式サンプル:  
  https://github.com/GoogleContainerTools/skaffold/tree/main/examples

- Skaffoldのパラメータ一覧:  
  https://skaffold.dev/docs/references/yaml/