Kubernetes でブルーグリーンデプロイする

# Kubernetes でブルーグリーンデプロイする

こんにちは、@studio_meowtoon です。今回は、ローカル Ubuntu の Kubernetes クラスターでブルーグリーンデプロイする方法の一例を紹介します。
![spring-boot_on_kubernetes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/148f70f1-ce76-667a-c11f-a8e3114faa7b.png)

## 目的

Windows 11 の Linux でクラウド開発します。

こちらから記事の一覧がご覧いただけます。

https://qiita.com/studio_meowtoon/items/6119ab05d8b13527efd8

## 実現すること

Windows 11 の WSL で稼働する Ubuntu の Kubernetes クラスターにて、コンテナアプリのブルーグリーンデプロイを実践してみます。

:::note alert
この記事では、**Kubernetes におけるブルーグリーンデプロイの手順**を説明していますが、これが**唯一の正しい方法ではありません**。実際には、他にも多くの手順が存在します。したがって、この記事を参考にしながら、他の情報も収集することをお勧めします。
:::

## 技術トピック

### ブルーグリーンデプロイとは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [ブルーグリーンデプロイメント](https://martinfowler.com/bliki/BlueGreenDeployment.html)

:::note info
ブルーグリーンデプロイは、アプリケーションの新しいバージョンを本番環境にデプロイする際に、従来の運用方法とは異なり、新しいバージョンを本番環境に構築し、そこでアプリケーションをテストした後、従来の本番環境と切り替える方法です。

|内容|
|:--|
通常、ブルーの本番環境とグリーンの新しい本番環境を用意します。最初はブルーがアクティブな本番環境として稼働し、グリーンはアクティブではありません。|
|新しいバージョンのアプリケーションをグリーン環境にデプロイしてテストし、問題がなければトラフィックをグリーン環境に切り替えます。|
|これにより、ダウンタイムを最小限に抑え、システムの信頼性を高めることができます。|

:::

</details>

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  
:::

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
```

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
```

Java JDK [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/4d11e94a2389758759cd)

```sh
$ java -version
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu122.04, mixed mode, sharing)
```

Maven [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/f4cdc4039158153365fb)

```sh
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.18, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
```

Docker [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe)

```sh
$ docker --version
Docker version 23.0.1, build a5ee5b1
```

Kubernetes [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/ef38ba2d5e913e343380)

```sh
$ minikube version
minikube version: v1.29.0
commit: ddac20b4b34a9c8c857fc602203b6ba2679794d3
```

kubectl [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/ef38ba2d5e913e343380)

```sh
$ kubectl version --short
Client Version: v1.26.1
Kustomize Version: v4.5.7
Server Version: v1.26.1
```

</details>

:::note warn
この記事では基本的に **Ubuntu のターミナルで操作**を行います。**Vim を使用してコピペする方法**を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
:::

https://qiita.com/studio_meowtoon/items/7771ad48676926e41463

## ブルーグリーンデプロイする仕様
※ この記事での仕様となります。

|識別するタグ|内容|エンドポイント|JSON レスポンス|
|:--|:--|:--|:--|
|v1|現用のサービス|/api/data|{"message":"Hello **Blue**!"}|
|v2|更新するサービス|/api/data|{"message":"Hello **Green**!"}|

**Kubernetes マニュフェスト**

|No|kind|ファイル|内容|
|:--|:--|:--|:--|
|1|Service|service-prod.yaml|本番環境 エンドポイント|
|2|Service|service-dev.yaml|検証環境 エンドポイント|
|3|Deployment|deployment-v1.yaml|現用のアプリケーション|
|4|Deployment|deployment-v2.yaml|更新するアプリケーション|

## ブルーグリーンデプロイする手順

### v1 アプリのコンテナイメージの作成

<details>
<summary>こちらを展開して手順をご覧いただけます。</summary>

#### Java アプリ作成

こちらの関連記事で手順がご確認いただけます。 

https://qiita.com/studio_meowtoon/items/37ac0082a3228e4de570

プロジェクトフォルダに移動します。  
※ ~/tmp/hello-spring-boot をプロジェクトフォルダとします。    

```sh
$ cd ~/tmp/hello-spring-boot
```

v1 アプリのソースコードを作成します。

```sh
$ vim src/main/java/com/example/springboot/controller/HelloController.java
```

ファイルの内容
※ コードの内容を一部省略しています。

```java:HelloController.java
@RestController
@RequestMapping("/api")
public class HelloController {
    @GetMapping("/data")
    public Map<String, String> getData() {
        Map<String, String> map = Map.of("message", "Hello Blue!");
        return map;
    }
}
```

v1 アプリをビルドします。

```sh
$ mvn clean package
```

#### コンテナイメージ作成

こちらの関連記事で手順がご確認いただけます。 

https://qiita.com/studio_meowtoon/items/9c07e20b4124d8c5f972

Docker デーモンを起動します。

```sh
$ sudo service docker start
 * Starting Docker: docker      [ OK ]
```

v1 アプリをコンテナイメージにビルドします。

```sh
$ docker build --no-cache -t app-hello-spring-boot:v1 .
```

v1 アプリのコンテナイメージを確認します。

```sh
$ docker images | grep app-hello-spring-boot
app-hello-spring-boot   v1        c61d32929a90   58 seconds ago   390MB
```

</details>

:::note info
ここまでの作業で **v1 アプリのカスタムコンテナイメージ**が作成できました。
:::

### v1 アプリを Kubernetes クラスターにデプロイ

#### コンテナイメージの取り込み 

こちらの関連記事で手順がご確認いただけます。 

https://qiita.com/studio_meowtoon/items/ef38ba2d5e913e343380

Kubernetes クラスターを開始します。

```sh
$ minikube start
```

v1 アプリのコンテナイメージを Minikube に取り込みます。

```sh
$ minikube image load app-hello-spring-boot:v1
```

Minikube 上のコンテナイメージを確認します。

```sh
$ minikube image ls | grep app-hello-spring-boot
docker.io/library/app-hello-spring-boot:v1
```

#### Kubernetes マニュフェスト作成

本番環境 service マニュフェストファイルを作成します。

```sh
$ vim service-prod.yaml
```

ファイルの内容

```yaml:service-prod.yaml
apiVersion: v1
kind: Service
metadata:
  name: app-service-prod
spec:
  type: LoadBalancer
  selector:
    app: app
    version: v1
  ports:
  - port: 80
    targetPort: 8080
```

v1 アプリ deployment マニュフェストファイルを作成します。

```sh
$ vim deployment-v1.yaml
```

ファイルの内容

```yaml:deployment-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
        version: v1
    spec:
      containers:
      - name: app
        image: app-hello-spring-boot:v1
        ports:
        - containerPort: 8080
```

Kubernetes クラスター接続先を確認します。

```sh
$ kubectl config current-context
minikube
```

v1 アプリを Kubernetes クラスターにデプロイします。

```sh
$ kubectl apply -f deployment-v1.yaml
deployment.apps/app-v1 created
```
```sh
$ kubectl apply -f service-prod.yaml
service/app-service-prod created
```

Kubernetes クラスターの状態を確認します。

```sh
$ kubectl get pods,services,deployments
NAME                          READY   STATUS    RESTARTS   AGE
pod/app-v1-56dddcc497-s9gd6   1/1     Running   0          19m

NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/app-service-prod   LoadBalancer   10.101.34.176   <pending>     80:31015/TCP     19m
service/kubernetes         ClusterIP      10.96.0.1       <none>        443/TCP          21m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/app-v1   1/1     1            1           19m
```

:::note warn
Minikube はローカルで稼働する Kubernetes クラスターであり、LoadBalancer タイプのサービスに外部 IP を割り当てる機能が備わっていないため、**EXTERNAL-IP** が **\<pending\>** になります。しかし、**minikube service コマンド**を使用することで、Minikube の仮想マシン上で、Service リソースに割り当てられた LoadBalancer にアクセスすることができます。
:::

別ターミナルから **app-service-prod** に接続する URL を取得します。

```sh
$ minikube service app-service-prod
🎉  デフォルトブラウザーで default/app-service-prod サービスを開いています...
👉  http://127.0.0.1:41087
❗  Docker ドライバーを linux 上で使用しているため、実行するにはターミナルを開く必要があります。
```

別ターミナルから curl コマンドで **app-service-prod** サービスの URL を確認します。

```sh
$ curl http://127.0.0.1:41087/api/data
{"message":"Hello Blue!"}
```

:::note info
ここまでの作業で **v1 アプリ**の Kubernetes クラスター への**デプロイが完了**しました。
:::

### v2 アプリのコンテナイメージの作成

<details>
<summary>こちらを展開して手順をご覧いただけます。</summary>

#### Java アプリ作成

v2 アプリのソースコードを作成します。

```sh
$ vim src/main/java/com/example/springboot/controller/HelloController.java
```

ファイルの内容
※ コードの内容を一部省略しています。

```java:HelloController.java
@RestController
@RequestMapping("/api")
public class HelloController {
    @GetMapping("/data")
    public Map<String, String> getData() {
        Map<String, String> map = Map.of("message", "Hello Green!");
        return map;
    }
}
```

v2 アプリをビルドします。

```sh
$ mvn clean package
```

#### コンテナイメージ作成

v2 アプリをコンテナイメージにビルドします。

```sh
$ docker build --no-cache -t app-hello-spring-boot:v2 .
```

v1 アプリのコンテナイメージを確認します。

```sh
$ docker images | grep app-hello-spring-boot
app-hello-spring-boot   v2        c61d32929a90   58 seconds ago   390MB
```

</details>

:::note info
ここまでの作業で **v2 アプリのカスタムコンテナイメージ**が作成できました。
:::

### v2 アプリを Kubernetes クラスターにデプロイ

#### コンテナイメージの取り込み 

v2 アプリのコンテナイメージを Minikube に取り込みます。

```sh
$ minikube image load app-hello-spring-boot:v2
```

Minikube 上のコンテナイメージを確認します。

```sh
$ minikube image ls | grep app-hello-spring-boot
docker.io/library/app-hello-spring-boot:v2
docker.io/library/app-hello-spring-boot:v1
```

#### Kubernetes マニュフェスト作成

検証環境 service マニュフェストファイルを作成します。

```sh
$ vim service-dev.yaml
```

ファイルの内容

```yaml:service-dev.yaml
apiVersion: v1
kind: Service
metadata:
  name: app-service-dev
spec:
  type: LoadBalancer
  selector:
    app: app
    version: v2
  ports:
  - port: 80
    targetPort: 8080
```

v2 アプリ deployment マニュフェストファイルを作成します。

```sh
$ vim deployment-v2.yaml
```

ファイルの内容

```yaml:deployment-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
        version: v2
    spec:
      containers:
      - name: app
        image: app-hello-spring-boot:v2
        ports:
        - containerPort: 8080
```

v2 アプリを Kubernetes クラスターにデプロイします。

```sh
$ kubectl apply -f deployment-v2.yaml
deployment.apps/app-v2 created
```
```sh
$ kubectl apply -f service-dev.yaml
service/app-service-dev created
```

Kubernetes クラスターの状態を確認します。

```sh
$ kubectl get pods,services,deployments
NAME                          READY   STATUS    RESTARTS   AGE
pod/app-v1-56dddcc497-s9gd6   1/1     Running   0          34m
pod/app-v2-6b587b579-zpxlp    1/1     Running   0          26s

NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/app-service-dev    LoadBalancer   10.106.214.150   <pending>     80:32513/TCP     13s
service/app-service-prod   LoadBalancer   10.101.34.176    <pending>     80:31015/TCP     34m
service/kubernetes         ClusterIP      10.96.0.1        <none>        443/TCP          36m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/app-v1   1/1     1            1           34m
deployment.apps/app-v2   1/1     1            1           26s
```

別ターミナルから **app-service-dev** に接続する URL を取得します。

```sh
$ minikube service app-service-dev
🎉  デフォルトブラウザーで default/app-service-dev サービスを開いています...
👉  http://127.0.0.1:34167
❗  Docker ドライバーを linux 上で使用しているため、実行するにはターミナルを開く必要があります。
```

別ターミナルから curl コマンドで **app-service-prod** サービスの URL を確認します。

```sh
$ curl http://127.0.0.1:41087/api/data
{"message":"Hello Blue!"}
```

:::note warn
**app-service-prod** サービスの URLをリクエストすると、更新前の **v1 アプリの出力が表示**されます。
:::

別ターミナルから curl コマンドで **app-service-dev** サービスの URL を確認します。

```sh
$ curl http://127.0.0.1:34167/api/data
{"message":"Hello Green!"}
```

:::note info
Kubernetes クラスターに v2 アプリを、**app-service-dev** サービスの URL からアクセス可能なアプリとしてデプロイすることができました。また、実際にリリースする前に **app-service-dev** サービスの URL を利用して**動作確認**を行うことができます。これは、**ブルーグリーンデプロイの目的の1つ**です。
:::

### 新しいデプロイメントへの切り替え

**app-service-prod** サービスの URL の向き先を **deployment-v2** デプロイメントに切り替えます。

```sh
$ vim service-prod.yaml
```

ファイルの内容

```yaml:service-prod.yaml
apiVersion: v1
kind: Service
metadata:
  name: app-service-prod
spec:
  type: LoadBalancer
  selector:
    app: app
    version: v2
  ports:
  - port: 80
    targetPort: 8080
```

app-service-prod の更新を Kubernetes クラスターにデプロイします。

```sh
$ kubectl apply -f service-prod.yaml
service/app-service-prod configured
```

Kubernetes クラスターの状態を確認します。

```sh
$ kubectl get pods,services,deployments
NAME                          READY   STATUS    RESTARTS   AGE
pod/app-v1-56dddcc497-s9gd6   1/1     Running   0          44m
pod/app-v2-6b587b579-zpxlp    1/1     Running   0          10m

NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/app-service-dev    LoadBalancer   10.106.214.150   <pending>     80:32513/TCP     10m
service/app-service-prod   LoadBalancer   10.101.34.176    <pending>     80:31015/TCP     44m
service/kubernetes         ClusterIP      10.96.0.1        <none>        443/TCP          46m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/app-v1   1/1     1            1           44m
deployment.apps/app-v2   1/1     1            1           10m
```

別ターミナルから curl コマンドで **app-service-prod** サービスの URL を確認します。

```sh
$ curl http://127.0.0.1:41087/api/data
{"message":"Hello Green!"}
```

:::note info
**app-service-prod** サービスの URLをリクエストして、更新後の **v2 アプリの出力**が表示されました。**本番環境**への**デプロイ作業が完了**しました。
:::

### トラブル発生時の切り戻し

:::note alert
サービスを最新バージョンにデプロイした後に**トラブルが発生**した場合、以前に**正常に動作**していた**デプロイに戻す**必要があります。これは、**ブルーグリーンデプロイの目的の1つ**です。
:::

**app-service-prod** サービスの URL の向き先を **deployment-v1** デプロイメントに切り戻します。

```sh
$ vim service-prod.yaml
```

ファイルの内容

```yaml:service-prod.yaml
apiVersion: v1
kind: Service
metadata:
  name: app-service-prod
spec:
  type: LoadBalancer
  selector:
    app: app
    version: v1
  ports:
  - port: 80
    targetPort: 8080
```

app-service-prod の更新を Kubernetes クラスターにデプロイします。

```sh
$ kubectl apply -f service-prod.yaml
service/app-service-prod configured
```

Kubernetes クラスターの状態を確認します。

```sh
$ kubectl get pods,services,deployments
NAME                          READY   STATUS    RESTARTS   AGE
pod/app-v1-56dddcc497-s9gd6   1/1     Running   0          49m
pod/app-v2-6b587b579-zpxlp    1/1     Running   0          15m

NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/app-service-dev    LoadBalancer   10.106.214.150   <pending>     80:32513/TCP     15m
service/app-service-prod   LoadBalancer   10.101.34.176    <pending>     80:31015/TCP     49m
service/kubernetes         ClusterIP      10.96.0.1        <none>        443/TCP          51m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/app-v1   1/1     1            1           49m
deployment.apps/app-v2   1/1     1            1           15m
```

別ターミナルから curl コマンドで **app-service-prod** サービスの URL を確認します。

```sh
$ curl http://127.0.0.1:41087/api/data
{"message":"Hello Blue!"}
```

:::note info
**app-service-prod** サービスの URLをリクエストして、更新前の **v1 アプリの出力**が表示されました。デプロイの**切り戻し**が完了しました。
:::

### デプロイ後の注意点

v2 アプリのデプロイに成功したので v1 アプリの**デプロイメント**を Kubernetes クラスターから削除します。

```sh
$ kubectl delete deployment app-v1
deployment.apps "app-v1" deleted
```

Kubernetes クラスターの状態を確認します。

```sh
$ kubectl get pods,services,deployments
NAME                         READY   STATUS    RESTARTS   AGE
pod/app-v2-6b587b579-zpxlp   1/1     Running   0          27m

NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/app-service-dev    LoadBalancer   10.106.214.150   <pending>     80:32513/TCP     26m
service/app-service-prod   LoadBalancer   10.101.34.176    <pending>     80:31015/TCP     60m
service/kubernetes         ClusterIP      10.96.0.1        <none>        443/TCP          63m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/app-v2   1/1     1            1           27m
```

v1 アプリのデプロイメントが削除されたことが確認できました。

## まとめ

WSL Ubuntu の Kubernetes クラスターでブルーグリーンデプロイを実践することができました。

:::note warn
クラウド開発においては、**Kubernetes の基本的な理解**は重要です。また、Ubuntu を使用することで Linux の知識も身につけることができます。最初は難しく感じるかもしれませんが、少しずつ進めていくことで自信を持って取り組むことができるようになります。
:::

どうでしたか？ WSL Ubuntu で、Kubernetes クラスターでのブルーグリーンデプロイを手軽に体験することができます。ぜひお試しください。今後もクラウド開発手法などを紹介していきますので、ぜひお楽しみにしてください。

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/5c7f288078c314462568