Kubernetes応用編 第01回
環境構築とNamespace設計 — マルチノードクラスタで環境を分離する
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.27 | Deployment | 静的ファイル配信 |
| APIサーバー | Payara Micro 7.2026.1 | Deployment | REST API(タスクのCRUD) |
| データベース | MySQL 8.0+ | StatefulSet | 業務データの永続化 |
| DB初期化 | mysqlクライアント | Job | スキーマ作成・初期データ投入 |
| DBバックアップ | mysqldump | CronJob | 定期バックアップ |
| ログ収集 | busybox | DaemonSet | 全Nodeでのログ転送 |
応用編では、このTaskBoardに毎回1つずつ部品を追加していきます。今回はフロントエンド(Nginx)とAPIサーバー(Payara Micro)をデプロイするところからスタートします。データベースはまだ導入しないため、APIサーバーは一時的にインメモリ(メモリ上のリスト)でデータを保持します。第3回でMySQLをStatefulSetとして導入した際に、本格的なDB接続版に切り替えます。
本回のゴール
| Before | After |
|---|---|
| 入門編のシングルノード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がまともに起動しません。
| 項目 | 入門編 | 応用編・実践編 |
|---|---|---|
| CPU | 2vCPU | 4vCPU以上推奨 |
| RAM | 3GB | 8GB以上(必須) |
| ディスク | 30GB程度 | 50GB以上推奨 |
| OS | AlmaLinux 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
READYが1/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 nodesでerror: 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
依存ライブラリのscopeがprovidedになっている点に注目してください。これは「ビルド時にはコンパイルに使うが、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の世界では、アプリケーションのデプロイは次のような流れでした。
- 開発サーバーでソースコードをコンパイルし、WARファイルを作成する
- 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.1 | WARファイルを載せて実行 | 本番アプリケーションサーバー |
最終的なコンテナイメージにはステージ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 vSphere | Kubernetes |
|---|---|---|
| リソースの論理的な分類・整理 | フォルダ | 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が使えるリソースの最小値・最大値を設定できます。
| リソース | 制御対象 | 制御内容 |
|---|---|---|
| ResourceQuota | Namespace全体 | 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 RequestとDefault Limitが自動的に適用されます。
1.6.3 ResourceQuotaとLimitRangeの関係を整理する
ResourceQuotaとLimitRangeは補完関係にあります。VMwareのリソースプールとVMのリソース設定に対応させると、次のように整理できます。
| 観点 | ResourceQuota | LimitRange |
|---|---|---|
| 制御の対象 | 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/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/api/tasks
[{"id":1,"title":"応用編の環境構築を完了する","status":"open"}]
正常にタスクの登録・取得ができています。ただし、現時点ではインメモリ版なので、Podが再起動するとデータは消えます。第3回でMySQLを導入し、データを永続化します。
最後に、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は複数の選択肢を比較して提示してくれるので、自分のケースに合ったパターンを選ぶ参考になります。ただし、最終的な判断はあなたの環境の制約(チーム構成、セキュリティ要件、既存の運用ルール)を踏まえて行ってください。
