インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

Kubernetes応用編 第01回

公開更新
目次
  1. 1.1 はじめに
    1. 題材システム「TaskBoard」
    2. 本回のゴール
  2. 1.2 応用編の環境を準備する
    1. 1.2.1 入門編のクラスタを削除する(残っている場合)
    2. 1.2.2 マルチノードkindクラスタを作成する
    3. 1.2.3 Metrics Serverを導入する
  3. 1.3 TaskBoard APIをビルドする
    1. 1.3.1 ソースコードを配置する
    2. 1.3.2 Dockerfileを理解する — multi-stage buildとは
    3. 1.3.3 コンテナイメージをビルドしてkindに投入する
  4. 1.4 Namespaceで環境を分離する
    1. 1.4.1 VMの世界との対比 — リソースプールとフォルダ
    2. 1.4.2 TaskBoard用のNamespaceを設計する
    3. 1.4.3 Namespaceを作成する
  5. 1.5 ResourceQuotaでNamespaceにリソース上限を設ける
    1. 1.5.1 ResourceQuotaとは何か
    2. 1.5.2 各Namespaceにリソース制限を設定する
    3. 1.5.3 Quota超過を体験する
  6. 1.6 LimitRangeでPod単位のデフォルト値を設定する
    1. 1.6.1 LimitRangeとは何か
    2. 1.6.2 LimitRangeを設定する
    3. 1.6.3 ResourceQuotaとLimitRangeの関係を整理する
  7. 1.7 TaskBoardをデプロイする
    1. 1.7.1 app NamespaceにNginxとTaskBoard APIをデプロイする
    2. 1.7.2 動作確認 — ヘルスチェックとリソース消費の観察
  8. 1.8 この回のまとめ
    1. 1.8.1 TaskBoardの現在地
    2. 1.8.2 Namespace設計の判断基準 — いつ使う / いつ使わない
    3. 1.8.3 実践編への橋渡し
    4. 1.8.4 次回予告
    5. AIコラム

1.1 はじめに

入門編(全11回)を完走されたあなたは、Pod、Deployment、Service、ConfigMap、Secret、PVCといった基本リソースを一通り扱えるようになっています。kubectlでの操作も手に馴染み、トラブルシュートの初動もできるはずです。

ただし、入門編ではすべてを「シングルノードのkindクラスタ」に「default Namespace」で配置していました。VMwareの世界で例えるなら、ESXiホスト1台にVMを全部載せて、フォルダ分けもリソースプールも使わずに運用しているようなものです。検証には十分ですが、本番環境には程遠い構成です。

今回から始まる応用編では、本番環境で必要になる「武器」を一つずつ手に入れていきます。そして毎回、題材システム「TaskBoard」にその武器を装備していきます。応用編を修了する頃、あなたの手元には全武器を装備した、動くTaskBoardが出来上がっています。

題材システム「TaskBoard」

応用編と実践編を通じて構築する題材は、社内業務Webアプリケーション「TaskBoard」です。タスクの登録・参照ができるシンプルなWebアプリケーションですが、本番運用に必要な要素をすべて含んでいます。

構成要素ソフトウェアK8sリソース役割
フロントエンドNginx 1.27Deployment静的ファイル配信
APIサーバーPayara Micro 7.2026.1DeploymentREST API(タスクのCRUD)
データベースMySQL 8.0+StatefulSet業務データの永続化
DB初期化mysqlクライアントJobスキーマ作成・初期データ投入
DBバックアップmysqldumpCronJob定期バックアップ
ログ収集busyboxDaemonSet全Nodeでのログ転送

応用編では、このTaskBoardに毎回1つずつ部品を追加していきます。今回はフロントエンド(Nginx)とAPIサーバー(Payara Micro)をデプロイするところからスタートします。データベースはまだ導入しないため、APIサーバーは一時的にインメモリ(メモリ上のリスト)でデータを保持します。第3回でMySQLをStatefulSetとして導入した際に、本格的なDB接続版に切り替えます。

本回のゴール

BeforeAfter
入門編のシングルノードkindクラスタに、default Namespaceで全リソースを配置しているマルチノードクラスタ上で、自分でビルドしたアプリを含むTaskBoardが、Namespace分離された環境で稼働している

具体的には、以下を達成します。

  • マルチノード(Control Plane 1 + Worker 3)のkindクラスタを構築する
  • Metrics Serverを導入し、kubectl top でリソース消費を観察できるようにする
  • TaskBoard APIのソースコードからコンテナイメージをビルドする
  • Namespace(app / db / monitoring)で環境を分離する
  • ResourceQuotaとLimitRangeでリソース管理を設定する
  • NginxとTaskBoard APIをapp Namespaceにデプロイし、動作確認する

1.2 応用編の環境を準備する

応用編に入る前に、環境面で2つの重要な確認事項があります。

入門編のサーバーでは動きません — より大きなサーバーが必要です

入門編ではシングルノードのkindクラスタにNginxをデプロイする程度だったため、2vCPU / 3GB RAMのVPSで十分でした。しかし応用編では、4ノード構成のkindクラスタ上でNginx、Payara Micro(JVMベースのアプリケーションサーバー)、後の回ではMySQLも動かします。入門編のサーバーではリソースが足りず、Podがまともに起動しません。

項目入門編応用編・実践編
CPU2vCPU4vCPU以上推奨
RAM3GB8GB以上(必須)
ディスク30GB程度50GB以上推奨
OSAlmaLinux 10 (Minimal)AlmaLinux 10(同じ)

入門編で使っていたサーバーとは別に、新しいサーバーを用意するか、VPSのプランをアップグレードしてください。新しいサーバーを用意した場合は、Docker CE、kind、kubectlのインストールが必要です。入門編 第1回の環境構築手順を参照して、これらを先にセットアップしてください。

なお、応用編で使うPayara MicroのビルドにはMavenとJDKが必要ですが、これらはDockerのmulti-stage build内で使用するため、ホストマシンへのインストールは不要です。ホストに必要なのはDocker CE、kind、kubectlの3つだけです。

なぜVM 4台ではなくkindを使うのか

応用編ではマルチノードクラスタを使います。「マルチノードなら、VM 4台を用意してkubeadmでクラスタを組むべきでは?」と思われるかもしれません。しかし本シリーズではkindを使い続けます。理由を正直にお伝えします。

本シリーズの目的は「クラスタの組み立て方」ではなく「クラスタの上で何をどう動かすか」です。kubeadmによるクラスタ構築はそれ自体が独立した専門領域で、CNIプラグインの選定、etcdの管理、証明書の更新など、記事2〜3回分の内容があります。そこに時間を投入すると、本題のNamespace設計やワークロード管理に使える時間が減ってしまいます。

kindのNodeはDockerコンテナとして動作するため、必要なのはホスト1台だけです。4台のVMを管理する必要はありません。そして重要なのは、kindの上で実行するkubectlコマンドやマニフェストは、本番のマネージドKubernetes(EKS / AKS / GKE)でもそのまま使えるということです。

kindと本番環境の差分(LoadBalancerの挙動、StorageClass、IAM連携など)は実践編 第10回「本番への道」で体系的に整理します。まずは「クラスタの上でできること」に集中しましょう。

1.2.1 入門編のクラスタを削除する(残っている場合)

入門編で作成したkindクラスタがまだ残っている場合は、先に削除します。残っていない場合は次のセクションに進んでください。

[Execution User: developer]

kind get clusters

入門編のクラスタ名(例:k8s-intro)が表示された場合は、削除します。

[Execution User: developer]

kind delete cluster --name k8s-intro
Deleting cluster "k8s-intro" ...
Deleted nodes: ["k8s-intro-control-plane" "k8s-intro-worker"]

クラスタ名が異なる場合は、kind get clusters で表示された名前に置き換えてください。

1.2.2 マルチノードkindクラスタを作成する

応用編用のkindクラスタ定義ファイルを作成します。Control Plane 1台 + Worker 3台の4ノード構成です。

[Execution User: developer]

mkdir -p ~/k8s-applied
cat <<'EOF' > ~/k8s-applied/kind-applied.yaml
# kind-applied.yaml — 応用編・実践編共通のクラスタ定義
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
  - role: worker
EOF

入門編ではWorker 1台でしたが、応用編ではWorker 3台に増やしています。DaemonSet(第4回)が全Worker Nodeに1つずつPodを配置する様子や、HPA(第8回)がPodを複数Nodeに分散させる様子を観察するためです。

クラスタを作成します。

[Execution User: developer]

kind create cluster --name k8s-applied --config ~/k8s-applied/kind-applied.yaml

作成には数分かかります。完了すると以下のようなメッセージが表示されます。

Creating cluster "k8s-applied" ...
 ✓ Ensuring node image (kindest/node:v1.32.0) 🖼
 ✓ Preparing nodes 📦 📦 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-k8s-applied"
You can now use your cluster with:

kubectl cluster-info --context kind-k8s-applied

Have a nice day! 👋

ノードを確認します。

[Execution User: developer]

kubectl get nodes
NAME                        STATUS   ROLES           AGE   VERSION
k8s-applied-control-plane   Ready    control-plane   2m    v1.32.0
k8s-applied-worker          Ready    <none>          90s   v1.32.0
k8s-applied-worker2         Ready    <none>          90s   v1.32.0
k8s-applied-worker3         Ready    <none>          90s   v1.32.0

4ノード(Control Plane 1 + Worker 3)がReadyになっていれば成功です。入門編の2ノード構成から倍増しました。VMwareの世界で言えば、ESXiホストを1台から4台のクラスタに拡張したようなものです。

1.2.3 Metrics Serverを導入する

Metrics Serverは、NodeやPodのCPU・メモリ使用量をリアルタイムに収集するコンポーネントです。導入するとkubectl topコマンドが使えるようになります。応用編の後半で扱うHPA(Horizontal Pod Autoscaler)もMetrics Serverのデータを利用するため、ここで導入しておきます。

VMwareの世界では、vCenterがESXiホストのリソース使用率を自動的に収集してくれます。Kubernetesでは、そのリソース収集の仕組みを明示的にインストールする必要があります。

[Execution User: developer]

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

kind環境ではTLS証明書の検証に失敗するため、Metrics Serverの起動引数に--kubelet-insecure-tlsを追加します。

[Execution User: developer]

kubectl patch deployment metrics-server -n kube-system \
  --type='json' \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

Metrics Serverが起動するまで1〜2分待ちます。起動状況を確認しましょう。

[Execution User: developer]

kubectl get deployment metrics-server -n kube-system
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
metrics-server   1/1     1            1           90s

READY1/1になったら、kubectl topを試してみます。

[Execution User: developer]

kubectl top nodes
NAME                        CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
k8s-applied-control-plane   210m         5%     620Mi           7%
k8s-applied-worker          45m          1%     280Mi           3%
k8s-applied-worker2         38m          0%     260Mi           3%
k8s-applied-worker3         40m          1%     270Mi           3%

各Nodeのリソース消費が表示されました。Control PlaneのCPU使用量がWorkerより高いのは、API ServerやetcdなどのK8sコンポーネントが稼働しているためです。このkubectl topコマンドは、応用編を通じてリソース消費の観察に繰り返し使います。

📝 Metrics Serverが起動しない場合
kubectl top nodeserror: Metrics API not availableと表示される場合は、Metrics ServerのPodが起動していない可能性があります。kubectl logs -n kube-system deployment/metrics-serverでログを確認してください。--kubelet-insecure-tlsのパッチが適用されていないことが原因であるケースが大半です。

1.3 TaskBoard APIをビルドする

環境が整ったので、TaskBoardのAPIサーバーをビルドします。TaskBoard APIはPayara Micro上で動作するJakarta EEアプリケーションです。読者のみなさんがJava開発者である必要はありません。ソースコードは完成品を提供しますので、作業は「ソースを配置 → ビルド → コンテナ化」の流れになります。

1.3.1 ソースコードを配置する

作業用ディレクトリを作成し、ソースコードを配置していきます。

[Execution User: developer]

mkdir -p ~/k8s-applied/taskboard-api/src/main/java/com/taskboard
mkdir -p ~/k8s-applied/taskboard-api/src/main/webapp/WEB-INF

まず、Mavenのプロジェクト定義ファイルpom.xmlを作成します。これはJavaプロジェクトの依存ライブラリやビルド設定を記述するファイルです。VMwareの世界で言えば、OVFテンプレートのデプロイ設定のようなものです。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.taskboard</groupId>
    <artifactId>taskboard-api</artifactId>
    <version>1.0.0</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>

    <dependencies>
        <!-- Jakarta EE 11 Web Profile API -->
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>11.0.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- MicroProfile 6.1 API(ヘルスチェック等) -->
        <dependency>
            <groupId>org.eclipse.microprofile</groupId>
            <artifactId>microprofile</artifactId>
            <version>6.1</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>taskboard-api</finalName>
    </build>
</project>
EOF

依存ライブラリのscopeprovidedになっている点に注目してください。これは「ビルド時にはコンパイルに使うが、WARファイルには含めない(実行時はPayara Microが提供する)」という意味です。

次に、タスクを表すエンティティクラスTask.javaを作成します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/src/main/java/com/taskboard/Task.java
package com.taskboard;

/**
 * タスクエンティティ。
 * 応用編第1〜2回ではインメモリ版として使用する。
 * 第3回でMySQL導入後、JPAエンティティに拡張する。
 */
public class Task {

    private Long id;
    private String title;
    private String status;

    public Task() {
    }

    public Task(Long id, String title, String status) {
        this.id = id;
        this.title = title;
        this.status = status;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }

    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}
EOF

次に、タスクの管理ロジックを担うTaskService.javaを作成します。現時点ではArrayListを使ったインメモリ版です。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskService.java
package com.taskboard;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

/**
 * インメモリ版タスクサービス。
 * データはArrayListに保持する。Pod再起動でデータは消える。
 * 応用編第3回でMySQL版に差し替える。
 */
@ApplicationScoped
public class TaskService {

    private final List<Task> tasks = new ArrayList<>();
    private final AtomicLong sequence = new AtomicLong(1);

    public List<Task> findAll() {
        return new ArrayList<>(tasks);
    }

    public Optional<Task> findById(Long id) {
        return tasks.stream()
                .filter(t -> t.getId().equals(id))
                .findFirst();
    }

    public Task create(Task task) {
        task.setId(sequence.getAndIncrement());
        if (task.getStatus() == null || task.getStatus().isEmpty()) {
            task.setStatus("open");
        }
        tasks.add(task);
        return task;
    }
}
EOF

最後に、REST APIのエンドポイントを定義するTaskResource.javaを作成します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskResource.java
package com.taskboard;

import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;

/**
 * TaskBoard REST API。
 *   GET  /api/tasks      → タスク一覧
 *   POST /api/tasks      → タスク追加
 *   GET  /api/tasks/{id} → 指定タスク取得
 */
@Path("/api/tasks")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class TaskResource {

    @Inject
    private TaskService taskService;

    @GET
    public List<Task> getAll() {
        return taskService.findAll();
    }

    @GET
    @Path("/{id}")
    public Response getById(@PathParam("id") Long id) {
        return taskService.findById(id)
                .map(task -> Response.ok(task).build())
                .orElse(Response.status(Response.Status.NOT_FOUND).build());
    }

    @POST
    public Response create(Task task) {
        Task created = taskService.create(task);
        return Response.status(Response.Status.CREATED).entity(created).build();
    }
}
EOF

JAX-RSのApplication クラスも必要です。アプリケーションパスを/に設定します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskBoardApplication.java
package com.taskboard;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

/**
 * JAX-RSアプリケーション定義。
 * ルートパスを "/" に設定する。
 */
@ApplicationPath("/")
public class TaskBoardApplication extends Application {
}
EOF

ファイルの配置を確認します。

[Execution User: developer]

find ~/k8s-applied/taskboard-api -type f | sort
/home/developer/k8s-applied/taskboard-api/pom.xml
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/Task.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskBoardApplication.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskResource.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskService.java

1.3.2 Dockerfileを理解する — multi-stage buildとは

次に、TaskBoard APIをコンテナイメージにするためのDockerfileを作成します。ここで使うのが「multi-stage build(マルチステージビルド)」というテクニックです。

VMwareの世界では、アプリケーションのデプロイは次のような流れでした。

  1. 開発サーバーでソースコードをコンパイルし、WARファイルを作成する
  2. WARファイルを本番のアプリケーションサーバー(Tomcat / WebLogicなど)にデプロイする

コンテナの世界でも考え方は同じですが、これをDockerfile 1つの中で完結させます。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/Dockerfile
# ============================================
# ステージ1: ビルド(開発サーバーに相当)
# ============================================
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app

# 依存ライブラリを先にダウンロード(キャッシュ効率化)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# ソースコードをコピーしてビルド
COPY src ./src
RUN mvn package -DskipTests

# ============================================
# ステージ2: 実行(本番APサーバーに相当)
# ============================================
FROM payara/micro:7.2026.1
# ビルドステージで作成したWARファイルだけをコピー
COPY --from=build /app/target/taskboard-api.war ${DEPLOY_DIR}
EOF

このDockerfileには2つのFROM命令があります。これがmulti-stage buildの特徴です。

ステージベースイメージ役割VMの世界での対応
ステージ1(build)maven:3.9-eclipse-temurin-21ソースコードをコンパイルしてWARファイルを生成開発サーバーでのビルド作業
ステージ2(実行)payara/micro:7.2026.1WARファイルを載せて実行本番アプリケーションサーバー

最終的なコンテナイメージにはステージ2の内容だけが含まれます。ステージ1で使ったMavenやJDKはイメージに残りません。これにより、実行に不要なツールを含まない軽量なイメージが作れます。

ホストマシンにMavenやJDKをインストールする必要がないのは、このmulti-stage buildのおかげです。ビルドに必要なツールはすべてDockerイメージの中に含まれています。

1.3.3 コンテナイメージをビルドしてkindに投入する

Dockerfileができたので、コンテナイメージをビルドします。初回のビルドではMavenの依存ライブラリのダウンロードが入るため、数分かかります。

[Execution User: developer]

cd ~/k8s-applied/taskboard-api
docker build -t taskboard-api:1.0.0 .

ビルドの最後に以下のようなメッセージが表示されれば成功です。

 => exporting to image
 => => naming to docker.io/library/taskboard-api:1.0.0

ビルドしたイメージを確認します。

[Execution User: developer]

docker images taskboard-api
REPOSITORY      TAG     IMAGE ID       CREATED          SIZE
taskboard-api   1.0.0   a1b2c3d4e5f6   30 seconds ago   210MB

このイメージはホストのDockerに保存されていますが、kindクラスタのノードからはまだ見えません。kindクラスタにイメージを投入します。

[Execution User: developer]

kind load docker-image taskboard-api:1.0.0 --name k8s-applied
Image: "taskboard-api:1.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-worker", loading...
Image: "taskboard-api:1.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-worker2", loading...
Image: "taskboard-api:1.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-worker3", loading...
Image: "taskboard-api:1.0.0" with ID "sha256:a1b2c3d4..." not yet present on node "k8s-applied-control-plane", loading...

全4ノードにイメージが配布されました。これで、Deploymentのマニフェストでimage: taskboard-api:1.0.0と指定すれば、どのWorker NodeにPodがスケジュールされてもイメージを取得できます。

📝 kind load docker-imageが必要な理由
kindのNodeはDockerコンテナとして動いており、ホストのDockerとは別のコンテナランタイムを使っています。そのため、ホストでdocker buildしたイメージをkindクラスタ内で使うには、kind loadで明示的に投入する必要があります。本番のマネージドKubernetesでは、コンテナレジストリ(Docker Hub、ECR、GCR等)にイメージをpushして使います。

1.4 Namespaceで環境を分離する

クラスタとアプリケーションイメージの準備ができました。次はリソースを配置する「場所」を整備します。入門編ではすべてのリソースをdefault Namespaceに配置していましたが、本番環境ではNamespaceを使って環境を分離するのが標準的なプラクティスです。

1.4.1 VMの世界との対比 — リソースプールとフォルダ

VMwareの世界では、ESXi上のリソースを論理的に分離するために「リソースプール」と「フォルダ」を使います。リソースプールはCPUやメモリの上限を設定して割り当てを管理し、フォルダはVMを用途やチームごとに整理するために使います。

KubernetesのNamespaceは、この両方の役割を兼ね備えています。

目的VMware vSphereKubernetes
リソースの論理的な分類・整理フォルダNamespace
リソースの使用量制限リソースプール(CPU/メモリの予約・制限)Namespace + ResourceQuota
デフォルトのリソース設定リソースプールのシェア値Namespace + LimitRange
アクセス制御の境界vCenterの権限(フォルダ単位)Namespace + RBAC(第2回で扱う)

Namespaceは「フォルダ + リソースプール + 権限境界」を1つにまとめたものだと考えると理解しやすいでしょう。

1.4.2 TaskBoard用のNamespaceを設計する

TaskBoardでは、以下の3つのNamespaceを使います。

Namespace用途配置するコンポーネント
appアプリケーション層Nginx(フロントエンド)、TaskBoard API(APIサーバー)
dbデータベース層MySQL(第3回で導入)、DB初期化Job / バックアップCronJob(第4回で導入)
monitoring監視・ログ収集ログ収集DaemonSet(第4回で導入)

今回はapp Namespaceにだけリソースをデプロイしますが、dbとmonitoringも先に作成しておきます。Namespaceの分け方にはいくつかのパターンがあります。チーム別(frontend / backend)、環境別(dev / staging / prod)、機能別(app / db / monitoring)などです。TaskBoardでは機能別に分けています。この判断基準についてはまとめのセクションで詳しく触れます。

1.4.3 Namespaceを作成する

3つのNamespaceをマニフェストファイルで作成します。kubectl create namespaceコマンドでも作成できますが、マニフェストファイルにしておくと、後からResourceQuotaやLabelを追加する際に管理しやすくなります。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/namespace-app.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: app
  labels:
    team: taskboard
    layer: application
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/namespace-db.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: db
  labels:
    team: taskboard
    layer: database
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/namespace-monitoring.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: monitoring
  labels:
    team: taskboard
    layer: monitoring
EOF

3つのNamespaceを一括で作成します。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/namespace-app.yaml
kubectl apply -f ~/k8s-applied/namespace-db.yaml
kubectl apply -f ~/k8s-applied/namespace-monitoring.yaml
namespace/app created
namespace/db created
namespace/monitoring created

作成されたNamespaceを確認します。

[Execution User: developer]

kubectl get namespaces --show-labels
NAME                 STATUS   AGE   LABELS
app                  Active   10s   kubernetes.io/metadata.name=app,layer=application,team=taskboard
db                   Active   10s   kubernetes.io/metadata.name=db,layer=database,team=taskboard
default              Active   15m   kubernetes.io/metadata.name=default
kube-node-lease      Active   15m   kubernetes.io/metadata.name=kube-node-lease
kube-public          Active   15m   kubernetes.io/metadata.name=kube-public
kube-system          Active   15m   kubernetes.io/metadata.name=kube-system
local-path-storage   Active   15m   kubernetes.io/metadata.name=local-path-storage
monitoring           Active   10s   kubernetes.io/metadata.name=monitoring,layer=monitoring,team=taskboard

app、db、monitoringの3つがActiveで作成されています。kube-systemなどはKubernetesが自動的に作成するシステム用Namespaceです。これらには触れないようにしましょう。

1.5 ResourceQuotaでNamespaceにリソース上限を設ける

1.5.1 ResourceQuotaとは何か

Namespaceを作っただけでは、そのNamespace内でいくらでもリソースを消費できてしまいます。VMwareのリソースプールで言えば、CPU/メモリの「制限」を設定していない状態です。あるチームがリソースを使いすぎて、他のチームのVMが起動できなくなる、というトラブルの元になります。

ResourceQuotaは、Namespace単位でCPU、メモリ、Pod数などの上限を設定するリソースです。Quotaを超える要求は拒否されるため、1つのNamespaceがクラスタ全体のリソースを食い潰すことを防げます。

1.5.2 各Namespaceにリソース制限を設定する

TaskBoardの各Namespaceに、用途に応じたResourceQuotaを設定します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/resourcequota-app.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: app-quota
  namespace: app
spec:
  hard:
    requests.cpu: "2"          # CPU requestsの合計上限: 2コア
    requests.memory: "2Gi"     # メモリrequestsの合計上限: 2GiB
    limits.cpu: "4"            # CPU limitsの合計上限: 4コア
    limits.memory: "4Gi"       # メモリlimitsの合計上限: 4GiB
    pods: "20"                 # Pod数の上限
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/resourcequota-db.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: db-quota
  namespace: db
spec:
  hard:
    requests.cpu: "1"          # DB層はapp層より控えめ
    requests.memory: "1Gi"
    limits.cpu: "2"
    limits.memory: "2Gi"
    pods: "10"
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/resourcequota-monitoring.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: monitoring-quota
  namespace: monitoring
spec:
  hard:
    requests.cpu: "500m"       # 監視層は軽量
    requests.memory: "512Mi"
    limits.cpu: "1"
    limits.memory: "1Gi"
    pods: "10"
EOF

3つのResourceQuotaを適用します。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/resourcequota-app.yaml
kubectl apply -f ~/k8s-applied/resourcequota-db.yaml
kubectl apply -f ~/k8s-applied/resourcequota-monitoring.yaml
resourcequota/app-quota created
resourcequota/db-quota created
resourcequota/monitoring-quota created

app NamespaceのQuotaを確認してみましょう。

[Execution User: developer]

kubectl describe resourcequota app-quota -n app
Name:            app-quota
Namespace:       app
Resource         Used  Hard
--------         ----  ----
limits.cpu       0     4
limits.memory    0     4Gi
pods             0     20
requests.cpu     0     2
requests.memory  0     2Gi

Usedがすべて0なのは、まだ何もデプロイしていないからです。この後TaskBoardをデプロイすると、ここの数値が増えていきます。

1.5.3 Quota超過を体験する

ResourceQuotaが実際に効くことを確認しましょう。monitoring Namespaceはrequests.cpu: 500mに制限しています。ここにCPU requestsが1コアのPodを作ろうとするとどうなるでしょうか。

[Execution User: developer]

kubectl run quota-test --image=nginx:1.27 -n monitoring \
  --overrides='{
    "spec": {
      "containers": [{
        "name": "quota-test",
        "image": "nginx:1.27",
        "resources": {
          "requests": {"cpu": "1", "memory": "256Mi"},
          "limits": {"cpu": "2", "memory": "512Mi"}
        }
      }]
    }
  }'
Error from server (Forbidden): pods "quota-test" is forbidden: exceeded quota: monitoring-quota, requested: requests.cpu=1, used: requests.cpu=0, limited: requests.cpu=500m

Quota超過のため、Podの作成が拒否されました。エラーメッセージには「requested: 1コア、limited: 500m(0.5コア)」と明確に理由が示されています。ResourceQuotaは、リソースを「使い始めてから制限する」のではなく、「作成時点でチェックして拒否する」仕組みです。これにより、クラスタ全体のリソースを安全に管理できます。

1.6 LimitRangeでPod単位のデフォルト値を設定する

1.6.1 LimitRangeとは何か

ResourceQuotaはNamespace全体のリソース上限を設定しますが、個々のPodに対しては何も言いません。そのため、ResourceQuotaが設定されたNamespaceに、resourcesの指定なしでPodを作ろうとすると、次のようなエラーになります。

Error from server (Forbidden): ... must specify limits.cpu, limits.memory

ResourceQuotaはNamespace内のリソース総量を管理するため、各Podがどれだけのリソースを「請求」するかを知る必要があります。resourcesが未指定だと計算できないため、拒否されるのです。

LimitRangeは、この問題を解決します。Pod(またはコンテナ)にresourcesが指定されていない場合に適用されるデフォルト値と、1つのPodが使えるリソースの最小値・最大値を設定できます。

リソース制御対象制御内容
ResourceQuotaNamespace全体Namespace内のリソース消費量の合計上限
LimitRange個々のPod / コンテナPod単位のデフォルト値と許容範囲

1.6.2 LimitRangeを設定する

各Namespaceに、コンテナ単位のデフォルトresourcesと上限・下限を設定します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/limitrange-app.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: app-limitrange
  namespace: app
spec:
  limits:
    - type: Container
      default:              # limitsのデフォルト値
        cpu: "200m"
        memory: "256Mi"
      defaultRequest:       # requestsのデフォルト値
        cpu: "100m"
        memory: "128Mi"
      max:                  # 1コンテナの上限
        cpu: "1"
        memory: "1Gi"
      min:                  # 1コンテナの下限
        cpu: "50m"
        memory: "32Mi"
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/limitrange-db.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: db-limitrange
  namespace: db
spec:
  limits:
    - type: Container
      default:
        cpu: "500m"
        memory: "512Mi"
      defaultRequest:
        cpu: "200m"
        memory: "256Mi"
      max:
        cpu: "1"
        memory: "1Gi"
      min:
        cpu: "100m"
        memory: "128Mi"
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/limitrange-monitoring.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: monitoring-limitrange
  namespace: monitoring
spec:
  limits:
    - type: Container
      default:
        cpu: "100m"
        memory: "128Mi"
      defaultRequest:
        cpu: "50m"
        memory: "64Mi"
      max:
        cpu: "500m"
        memory: "512Mi"
      min:
        cpu: "25m"
        memory: "32Mi"
EOF

3つのLimitRangeを適用します。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/limitrange-app.yaml
kubectl apply -f ~/k8s-applied/limitrange-db.yaml
kubectl apply -f ~/k8s-applied/limitrange-monitoring.yaml
limitrange/app-limitrange created
limitrange/db-limitrange created
limitrange/monitoring-limitrange created

app NamespaceのLimitRangeを確認します。

[Execution User: developer]

kubectl describe limitrange app-limitrange -n app
Name:       app-limitrange
Namespace:  app
Type        Resource  Min   Max  Default Request  Default Limit  Max Limit/Request Ratio
----        --------  ---   ---  ---------------  -------------  -----------------------
Container   cpu       50m   1    100m             200m           -
Container   memory    32Mi  1Gi  128Mi            256Mi          -

これで、app Namespaceにresourcesを指定せずにPodを作成しても、Default RequestDefault Limitが自動的に適用されます。

1.6.3 ResourceQuotaとLimitRangeの関係を整理する

ResourceQuotaとLimitRangeは補完関係にあります。VMwareのリソースプールとVMのリソース設定に対応させると、次のように整理できます。

観点ResourceQuotaLimitRange
制御の対象Namespace全体の合計値個々のPod / コンテナ
VMwareで言うとリソースプールのCPU/メモリ制限各VMのリソース割り当て設定のテンプレート
主な用途1つのNamespaceがクラスタ全体のリソースを食い潰すのを防ぐresources未指定のPodにデフォルト値を適用する。1つのPodが過大なリソースを要求するのを防ぐ
チェックのタイミングPod作成時(合計値が上限を超えないか確認)Pod作成時(デフォルト値の注入、範囲チェック)

実務では、ResourceQuotaとLimitRangeはセットで設定するのが基本です。ResourceQuotaだけ設定すると、全PodにresourcesのYAML記述が必須になりますし、LimitRangeだけ設定するとNamespace全体のリソース消費量に歯止めがかかりません。

1.7 TaskBoardをデプロイする

1.7.1 app NamespaceにNginxとTaskBoard APIをデプロイする

環境の準備がすべて整いました。いよいよTaskBoardの最初の2つのコンポーネントをデプロイします。

まず、フロントエンド(Nginx)のDeploymentとServiceを作成します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: app
  labels:
    app: taskboard
    component: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: taskboard
      component: frontend
  template:
    metadata:
      labels:
        app: taskboard
        component: frontend
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "50m"       # 静的ファイル配信のみのため軽量
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "128Mi"
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: app
  labels:
    app: taskboard
    component: frontend
spec:
  type: ClusterIP
  selector:
    app: taskboard
    component: frontend
  ports:
    - port: 80
      targetPort: 80
EOF

次に、TaskBoard APIのDeploymentとServiceを作成します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: taskboard-api
  namespace: app
  labels:
    app: taskboard
    component: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: taskboard
      component: api
  template:
    metadata:
      labels:
        app: taskboard
        component: api
    spec:
      containers:
        - name: taskboard-api
          image: taskboard-api:1.0.0
          imagePullPolicy: Never        # kindに直接投入済みのため
          ports:
            - containerPort: 8080       # Payara MicroのデフォルトHTTPポート
          resources:
            requests:
              cpu: "200m"              # JVMベースのため起動時にCPU消費が高い
              memory: "384Mi"          # JVMヒープ + Metaspace
            limits:
              cpu: "500m"
              memory: "512Mi"
EOF

NginxのresourcesとTaskBoard APIのresourcesに差がある点に注目してください。Nginxは静的ファイルの配信のみを行うため、CPU 50m / メモリ 64Mi程度で十分です。一方、TaskBoard API(Payara Micro)はJVMベースのアプリケーションサーバーです。JVMはヒープメモリとMetaspaceを確保するため、メモリのrequestsを384MiB、limitsを512MiBに設定しています。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: taskboard-api
  namespace: app
  labels:
    app: taskboard
    component: api
spec:
  type: NodePort
  selector:
    app: taskboard
    component: api
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 30080
EOF

TaskBoard APIのServiceはNodePort(30080)にしています。第5回でGateway APIを導入するまでの一時的な外部アクセス手段です。NginxのServiceはClusterIP(クラスタ内部のみ)にしています。こちらも第5回でGateway API経由の外部アクセスに切り替えます。

すべてのマニフェストをまとめてデプロイします。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/nginx-deployment.yaml
kubectl apply -f ~/k8s-applied/nginx-service.yaml
kubectl apply -f ~/k8s-applied/taskboard-api-deployment.yaml
kubectl apply -f ~/k8s-applied/taskboard-api-service.yaml
deployment.apps/nginx created
service/nginx created
deployment.apps/taskboard-api created
service/taskboard-api created

Podの起動状況を確認します。TaskBoard API(Payara Micro)は起動に15〜20秒かかるため、すべてのPodがRunningになるまで少し待ちます。

[Execution User: developer]

kubectl get pods -n app -o wide
NAME                             READY   STATUS    RESTARTS   AGE   IP           NODE
nginx-7d8b4f6c9-k2m5x           1/1     Running   0          45s   10.244.1.3   k8s-applied-worker
nginx-7d8b4f6c9-p8n3q           1/1     Running   0          45s   10.244.2.4   k8s-applied-worker2
taskboard-api-5f9d7b8c4-h6j2r   1/1     Running   0          45s   10.244.3.5   k8s-applied-worker3
taskboard-api-5f9d7b8c4-w4t9s   1/1     Running   0          45s   10.244.1.6   k8s-applied-worker

4つのPod(Nginx 2 + TaskBoard API 2)がすべてRunningになり、3つのWorker Nodeに分散配置されています。

1.7.2 動作確認 — ヘルスチェックとリソース消費の観察

まず、TaskBoard APIのヘルスチェックエンドポイントにアクセスします。Payara MicroはMicroProfile Health仕様に準拠しており、/healthエンドポイントが標準で提供されます。

[Execution User: developer]

kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -n app \
  -- curl -s http://taskboard-api:8080/health
{"status":"UP","checks":[]}

"status":"UP"が返ってきました。TaskBoard APIが正常に稼働しています。現時点ではchecksが空ですが、第8回でカスタムヘルスチェック(DB接続確認など)を追加します。

次に、TaskBoard APIの機能を確認します。タスクを1つ登録してみましょう。

[Execution User: developer]

kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -n app \
  -- curl -s -X POST http://taskboard-api:8080/taskboard-api/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"応用編の環境構築を完了する"}'
{"id":1,"title":"応用編の環境構築を完了する","status":"open"}

タスクが登録されました。一覧を取得してみます。

[Execution User: developer]

kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -n app \
  -- curl -s http://taskboard-api:8080/taskboard-api/api/tasks
[{"id":1,"title":"応用編の環境構築を完了する","status":"open"}]

上記の結果が返ってこず、 []pod “curl-test” deleted from app namespace と表示された場合は以下の症状になっています。

現在、API の Pod は 2つ(h6j2r と w4t9s) 動いています。そして、Kubernetes の Service はリクエストが来るたびに、どちらかの Pod にランダムに振り分けます。

何が起きたのか?

  1. POST 時: たまたま Pod A にリクエストが飛び、Pod A のメモリにタスクが保存されました。
  2. GET 時: たまたま Pod B にリクエストが飛びました。しかし、Pod B のメモリは空なので、[] が返ってきました。

現在の構成(Service + Deployment)では、リクエストがどの Pod に飛ぶかは制御されません。これを「ステートレス(状態を持たない)」な構成と呼びます。

動作を確認する方法

何度か curl(GET)を繰り返してみてください。

kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -n app \
  -- curl -s http://taskboard-api:8080/taskboard-api/api/tasks

運良く「保存された方の Pod」に当たればデータが表示され、「空の方の Pod」に当たれば [] が返ってきます。

解決策(今後の手順)

この「Pod ごとにデータがバラバラ」という問題は、まさにこの後の連載で解決していく課題そのものです。

  • 第3回 MySQL を導入し、すべての Pod が同じデータベースを参照するようにします(データの集約)。
  • 第8回: 冗長化(Replica 数を増やす)や、Pod が増減してもデータが失われない仕組み(永続化)を完成させます。

今は「Kubernetes の Service はリクエストを分散させているんだな」ということが実体験として確認できたので、環境構築としては大成功です。
最後に、kubectl topでPodのリソース消費量を確認します。

[Execution User: developer]

kubectl top pods -n app
NAME                             CPU(cores)   MEMORY(bytes)
nginx-7d8b4f6c9-k2m5x           1m           3Mi
nginx-7d8b4f6c9-p8n3q           1m           3Mi
taskboard-api-5f9d7b8c4-h6j2r   15m          280Mi
taskboard-api-5f9d7b8c4-w4t9s   12m          275Mi

NginxとTaskBoard API(Payara Micro)のリソース消費量に大きな差があることがわかります。Nginxはアイドル時にCPU 1m / メモリ 3Mi程度ですが、Payara MicroはCPU 12〜15m / メモリ 275〜280Mi程度を消費しています。これはJVMがヒープメモリを事前に確保する特性によるものです。

この差が、先ほどマニフェストでNginxのrequestsを50m / 64Mi、Payara Microのrequestsを200m / 384Miと設定した根拠です。resourcesの値は「なんとなく」ではなく、実際の消費量を基に設計します。

ResourceQuotaの消費状況も確認しておきましょう。

[Execution User: developer]

kubectl describe resourcequota app-quota -n app
Name:            app-quota
Namespace:       app
Resource         Used    Hard
--------         ----    ----
limits.cpu       1400m   4
limits.memory    1280Mi  4Gi
pods             4       20
requests.cpu     500m    2
requests.memory  896Mi   2Gi

Nginx 2台(requests.cpu: 50m × 2 = 100m)とTaskBoard API 2台(requests.cpu: 200m × 2 = 400m)の合計で、CPU requestsが500m使われていることが確認できます。Quotaの上限2コアに対してまだ余裕があり、追加のPodをデプロイする余地があります。

1.8 この回のまとめ

1.8.1 TaskBoardの現在地

今回の作業で、TaskBoardは以下の状態になりました。

第1回終了時の構成:

[app Namespace]
  ├── Nginx (Deployment, 2レプリカ) + Service (ClusterIP)
  └── TaskBoard API (Deployment, 2レプリカ, インメモリ版) + Service (NodePort:30080)

[db Namespace]
  └── (空 — 第3回でMySQL StatefulSetを追加)

[monitoring Namespace]
  └── (空 — 第4回でログ収集DaemonSetを追加)

基盤:
  ├── kindクラスタ (CP 1 + Worker 3)
  ├── Metrics Server 稼働中
  ├── 各NamespaceにResourceQuota適用済み
  └── 各NamespaceにLimitRange適用済み

1.8.2 Namespace設計の判断基準 — いつ使う / いつ使わない

Namespaceの分け方には絶対的な正解はありませんが、判断の指針はあります。

分割パターン適しているケース注意点
機能別(app / db / monitoring)1つのシステム内で役割が明確に分かれている場合TaskBoardはこのパターン。NetworkPolicy(第6回)との相性が良い
チーム別(frontend-team / backend-team)チームごとにリソース管理やアクセス制御を分けたい場合チームをまたぐ通信が増えると管理が複雑になる
環境別(dev / staging / prod)同一クラスタ上で複数環境を動かす場合本番はクラスタごと分ける方が安全なケースが多い

Namespaceを増やしすぎると管理コストが上がります。「Namespace内のリソースが10〜50個程度」が管理しやすい粒度の目安です。1つのNamespaceにリソースが100を超えるようなら分割を検討し、逆にリソースが2〜3個しかないNamespaceは統合を検討してください。

ResourceQuotaとLimitRangeの使い分けは次のとおりです。

状況推奨
複数チーム・複数アプリがクラスタを共有ResourceQuota + LimitRange をセットで設定
1チーム専用のクラスタLimitRangeだけでも十分な場合がある
テスト用の一時的な環境設定しなくても問題ないが、習慣としてLimitRangeは入れておくと安全

1.8.3 実践編への橋渡し

今回学んだNamespace設計、ResourceQuota、LimitRangeは、実践編 第2回「基本設計」と第4回「環境構築」で再び登場します。応用編では「こう使う」を体験しましたが、実践編では「なぜこの値にするのか」を設計書として言語化します。例えば、app NamespaceのResourceQuotaをCPU 2コアに設定した根拠を、コンポーネントのリソース消費量から逆算して設計書に記載する、というプロセスを踏みます。

1.8.4 次回予告

次回は第2回「『誰が何をできるか』の制御 — RBAC」です。今のTaskBoardは、kubeconfigを持っている人なら誰でもどのNamespaceのリソースでも操作できる状態です。「開発者はapp Namespaceだけ」「運用者はDeploymentの更新まで許可」といった権限の分離を実装します。

AIコラム

💡 AIの活用ヒント

「Namespaceをどう分けるべきか」のような設計判断は、AIに要件を伝えて壁打ちすると効率的です。
例:「社内Webアプリをk8sにデプロイする。フロントエンド、APIサーバー、DBの3層構成。チームは開発チーム3名と運用チーム2名。Namespaceの分け方を提案してほしい。根拠も添えて」

AIは複数の選択肢を比較して提示してくれるので、自分のケースに合ったパターンを選ぶ参考になります。ただし、最終的な判断はあなたの環境の制約(チーム構成、セキュリティ要件、既存の運用ルール)を踏まえて行ってください。

前の記事
次の記事