8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kubernetes入門|コンテナオーケストレーションの基礎から本番運用まで

Last updated at Posted at 2025-12-25

Kubernetes入門|コンテナオーケストレーションの基礎から本番運用まで
Kubernetes,Docker,コンテナ,DevOps,インフラ

はじめに

Dockerでコンテナ化したアプリケーションを、本番環境でどう運用すればいいのか?複数のコンテナを管理し、スケーリングし、障害に対応する...。これらの課題を解決するのがKubernetes(K8s)です。

この記事では、Kubernetesの基本概念から実践的な運用方法まで、詳しく解説します。

ただし、Kubernetesは「用語を覚えた」だけだと本番で詰まります。
現場でつまずきやすいのは概ね次です。

  • Podは動くのに外から繋がらない(Service Ingress NetworkPolicy)
  • 起動はするがすぐ落ちる(Probe ResourceLimit OOM)
  • 設定や機密情報の扱いが統一されず破綻(ConfigMap Secret)
  • ロールバックやデバッグの手順がなく、障害時に手が止まる

本記事はリソースの説明に加えて
何から固めるべきか、どこを設計で固定すべきか、運用で事故らないチェックリストまで含めて整理します。

まず学ぶ順番(最短ルート)

Kubernetesは要素が多いので、順番を固定すると理解が早いです。

  • Deploymentで Pod を増減できる
  • Serviceで Pod の前に安定した入口を作る
  • ConfigMap Secretで設定を外出しする
  • Ingressで外部公開をする
  • HPAでオートスケールする
  • 監視とログ、ロールバック手順を整える

まず決める設計判断の軸

コンテナに何を入れて 何を入れないか

  • コンテナイメージは不変で再現可能にする
  • 設定値は環境ごとに外出しする
  • ローカルファイルに状態を持たない(永続化はPVなどに逃がす)

リソース要求と制限をどう置くか

リソースは置かないより「最初から置く」方が安全です。
無制限だとノード全体を巻き込んで落ちます。

  • requestsはスケジューリングの前提
  • limitsは暴走防止

監視とデバッグの入口をどこに置くか

障害時に見る場所を決めておくと、復旧が早くなります。

  • まず events
  • 次に describe
  • 次に logs
  • 最後に exec

事故防止チェックリスト(最小版)

  • readiness と liveness と startup の役割を理解して分けている
  • requests と limits を設定している
  • Secretを平文でコミットしていない
  • ロールバック手順(Deploymentの履歴)が確認できる
  • 外部公開経路を図で説明できる(Ingress Service Pod)

Kubernetesとは

コンテナオーケストレーションの必要性

Docker単体では以下のような課題があります。

課題 説明
スケーリング コンテナの数を動的に増減させたい
負荷分散 複数コンテナへのトラフィック分散
自動復旧 障害時に自動で再起動したい
ローリングアップデート ダウンタイムなしでデプロイしたい
サービスディスカバリ コンテナ間の通信を管理したい

Kubernetesはこれらすべてを解決します。

アーキテクチャ

┌─────────────────────────────────────────────────────────────┐
│                      Kubernetes Cluster                      │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                    Control Plane                        │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐  │ │
│  │  │ API      │ │ etcd     │ │Scheduler │ │Controller │  │ │
│  │  │ Server   │ │          │ │          │ │ Manager   │  │ │
│  │  └──────────┘ └──────────┘ └──────────┘ └───────────┘  │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
│  ┌──────────────────┐  ┌──────────────────┐                 │
│  │     Node 1       │  │     Node 2       │  ...            │
│  │  ┌─────────────┐ │  │  ┌─────────────┐ │                 │
│  │  │   kubelet   │ │  │  │   kubelet   │ │                 │
│  │  └─────────────┘ │  │  └─────────────┘ │                 │
│  │  ┌─────────────┐ │  │  ┌─────────────┐ │                 │
│  │  │ kube-proxy  │ │  │  │ kube-proxy  │ │                 │
│  │  └─────────────┘ │  │  └─────────────┘ │                 │
│  │  ┌─────┐ ┌─────┐ │  │  ┌─────┐ ┌─────┐ │                 │
│  │  │ Pod │ │ Pod │ │  │  │ Pod │ │ Pod │ │                 │
│  │  └─────┘ └─────┘ │  │  └─────┘ └─────┘ │                 │
│  └──────────────────┘  └──────────────────┘                 │
└─────────────────────────────────────────────────────────────┘

コンポーネントの役割

コンポーネント 役割
API Server クラスタへのすべてのリクエストを受け付ける
etcd クラスタの状態を保存するKVS
Scheduler Podをどのノードに配置するか決定
Controller Manager 各種コントローラーを実行
kubelet ノード上でPodを管理
kube-proxy ネットワークプロキシ

基本リソース

Pod

Kubernetesの最小デプロイ単位です。1つ以上のコンテナを含みます。

まず押さえるポイント

  • Podは「Kubernetesが管理する実行単位」で、Node上に実体として配置される
  • Podは落ちたり再作成される前提なので、IPやローカルディスクなどは永続・固定を期待しない
  • 本番では通常、Podを直接作るより Deployment / Job など上位リソースから作る(再現性と運用性が高い)
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    app: my-app
    version: v1
spec:
  containers:
    - name: app
      image: my-app:1.0.0
      ports:
        - containerPort: 8080
      env:
        - name: NODE_ENV
          value: "production"
      resources:
        requests:
          memory: "128Mi"
          cpu: "100m"
        limits:
          memory: "256Mi"
          cpu: "500m"
      livenessProbe:
        httpGet:
          path: /health
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 10
      readinessProbe:
        httpGet:
          path: /ready
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 5
# Podの作成
kubectl apply -f pod.yaml

# Podの一覧
kubectl get pods

# Podの詳細
kubectl describe pod my-app

# Podのログ
kubectl logs my-app

# Podに入る
kubectl exec -it my-app -- /bin/sh

# Podの削除
kubectl delete pod my-app

Deployment

ステートレスなPodを「望む数だけ」「落ちても戻る」ように維持しつつ、
**ダウンタイムを抑えて更新(ローリングアップデート)**するための入口がDeploymentです。

厳密には、

  • Deployment は「更新戦略」と「ReplicaSetの世代管理(ロールアウト/ロールバック)」を管理
  • ReplicaSet が「指定数のPodレプリカ維持」を管理
  • Pod は実行単位

という責務分担になっています。

何が嬉しい?

  • Podを直接触らずに「更新」「ロールバック」「レプリカ数調整」を一貫して扱える
  • ロールアウト中の不具合を rollout undo で戻しやすい(運用の手戻りが小さい)

いつ使う?

  • Web API / フロント / ワーカーなど、基本は「常に動いていて欲しい」ステートレス系

よくある事故

  • selectortemplate.labels がズレて、意図しないPod集合を掴む
  • maxUnavailable を大きくしすぎて、更新時に一気に落ちてしまう
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: my-app:1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
# Deploymentの作成
kubectl apply -f deployment.yaml

# レプリカ数の変更
kubectl scale deployment my-app --replicas=5

# イメージの更新(ローリングアップデート)
kubectl set image deployment/my-app app=my-app:2.0.0

# ロールアウト状況の確認
kubectl rollout status deployment/my-app

# ロールアウト履歴
kubectl rollout history deployment/my-app

# ロールバック
kubectl rollout undo deployment/my-app
kubectl rollout undo deployment/my-app --to-revision=2

Service

Podへのネットワークアクセスの“入口”を作り、到達先Podが入れ替わっても安定して接続できるようにします。

注意点として、PodにはPod IPが付くため同一クラスタ内からPodへ直接アクセス自体は可能です。
ただしPod IPは再作成で変わり、レプリカが増減すると到達先選択も困るため、通常はアプリへの入口をServiceに寄せます。

Serviceが提供するもの(代表)

  • 安定した仮想IP(ClusterIP)
  • DNS名(例: my-app-service.<namespace>.svc.cluster.local
  • ラベルセレクタでPod集合を束ねる(Service Discovery)
  • (typeによって)ノード外/クラウドLB経由の公開

いつ使う?

  • 「このアプリ(Pod群)にリクエストを届けたい」と思ったら基本はService
  • PodのIPが変わってもクライアント側を変えたくないとき

よくある事故

  • selector が間違っていて「ServiceはあるのにEndpointsが空」になる
  • targetPort の設定ミスで疎通できない
  • readinessが落ちてEndpointsから外れているのに、ネットワークのせいだと勘違いする
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP  # ClusterIP, NodePort, LoadBalancer

Serviceの種類

まず結論(迷ったときの指針)

  • ClusterIP: クラスタ内向けの基本形。まずはこれ。
  • NodePort: Ingress ControllerやLBがない環境での暫定公開・検証用。常用は慎重に。
  • LoadBalancer: クラウド/MetalLBなどがあるなら、外部公開の標準ルート。

外部公開の全体像(よくある構成)

  • L7(HTTP/HTTPS)で公開したい: Ingress →(Ingress Controller)→ Service(ClusterIP)→ Pod
  • L4(TCP)で単体公開したい: Service(LoadBalancer)→ Pod
# ClusterIP(デフォルト): クラスタ内部のみ
apiVersion: v1
kind: Service
metadata:
  name: internal-service
spec:
  type: ClusterIP
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

# いつ使う?
# - ほぼ常に。Pod群への安定した入口(DNS/ClusterIP)が欲しいとき
# 注意
# - クラスタ外から到達できない(別途IngressやLBが必要)
# NodePort: 各ノードのポートを開放
apiVersion: v1
kind: Service
metadata:
  name: nodeport-service
spec:
  type: NodePort
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080  # 30000-32767

# いつ使う?
# - 手元検証・簡易デモ・Ingress Controllerの入口(前段にLBを置けない場合)など
# 避けたい/注意
# - ノードの全台でポートが開くため攻撃面が増える
# - スケール/ノード入れ替えで到達先や許可設定がブレやすい(本番常用は慎重に)
# LoadBalancer: クラウドのロードバランサーを使用
apiVersion: v1
kind: Service
metadata:
  name: lb-service
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

# 補足
# - 多くのクラウド環境では Service type=LoadBalancer が「外部IP(External IP)を払い出して公開する」標準ルート
# - ただし外部IPの取り扱いは環境/実装(クラウドコントローラ、MetalLB等)に依存します
# - ExternalIPs を手動で入れる運用も可能ですが、ネットワーク設計・セキュリティの影響が大きいので原則は管理された方式を推奨

# いつ使う?
# - 外部へ“単体で”公開したい(L4) / Ingress Controllerの前段にLBを置きたい
# 注意
# - LBの払い出しはコストやクォータに効く(作りすぎ注意)

Ingress

主にHTTP/HTTPSの公開・ルーティング・TLS終端など、**L7(アプリ層)**のトラフィック管理を担当します。

Ingress Controllerが“実体”(ここを知らないとハマる)

Ingressは「ルールを表現するリソース」で、実際に通信をさばくのは Ingress Controller(nginx-ingress、Traefik など)です。
つまり

  • Ingressリソースだけ作っても、Controllerがいなければ何も起きない
  • ingressClassName がControllerの対象と一致しないと、拾われずに無視される

という前提があります。

補足

  • “Ingress” というリソース自体はHTTPルールを表現するのが中心です
  • 一方で Ingress Controllerの実装次第では TCP/UDP(例: gRPC以外の生TCP)を扱える拡張機能を持つものもあります
  • そのため「HTTPだけ」と断言せず「基本はHTTP/HTTPS、拡張で別プロトコルもあり得る」と理解すると事故りにくいです

何が嬉しい?

  • ホスト名/パスベースのルーティング(api.example.comexample.com を分ける等)
  • TLS終端や証明書(cert-manager等)との連携がしやすい

いつ使う?

  • 外部公開がHTTP/HTTPSで、複数サービスを1つの入口でさばきたいとき

よくある事故

  • Ingress Controllerが未導入/未設定で、Ingressリソースを作っても何も起きない
  • ingressClassName の指定ミスでコントローラに拾われない
  • path とアプリ側ルーティングの認識ずれ(rewriteの有無)で404になる
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - example.com
        - api.example.com
      secretName: tls-secret
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80

ConfigMap と Secret

ここは運用で差が出ます。
「イメージは不変、設定は外出し」を徹底すると、環境差分(dev/stg/prod)を安全に扱えます。

ざっくり

  • ConfigMap: 機密ではない設定(フラグ、接続先ホスト名、ログレベルなど)
  • Secret: パスワードやAPIキーなどの機密(ただし“暗号化されて安全”と誤解しない。保護は運用設計も必要)

ConfigMap

何が嬉しい?

  • 同じコンテナイメージを使い回しつつ、設定だけ差し替えられる
  • キー/値を環境変数にもファイルにもできる(アプリ側事情に合わせられる)

注意

  • ConfigMap更新を即時反映したいか(Pod再起動が必要なケース)を最初に決める
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "db.example.com"
  DATABASE_PORT: "5432"
  LOG_LEVEL: "info"
  config.json: |
    {
      "debug": false,
      "maxConnections": 100
    }
# Podでの使用
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:1.0.0
      # 環境変数として使用
      env:
        - name: DATABASE_HOST
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: DATABASE_HOST
      # すべてのキーを環境変数として使用
      envFrom:
        - configMapRef:
            name: app-config
      # ファイルとしてマウント
      volumeMounts:
        - name: config-volume
          mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: app-config

Secret

注意(入門で勘違いしがち)

  • data: はbase64であって暗号化ではありません
  • etcd暗号化、RBAC、Secretの配布方式(External Secrets等)まで含めると「漏れにくさ」が上がります
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  # base64エンコードされた値
  DATABASE_PASSWORD: cGFzc3dvcmQxMjM=
  API_KEY: c2VjcmV0LWFwaS1rZXk=
stringData:
  # プレーンテキストで記述可能(適用時にエンコードされる)
  ANOTHER_SECRET: "plain-text-secret"
# base64エンコード
echo -n 'password123' | base64

# Secretの作成(コマンドライン)
kubectl create secret generic app-secrets \
  --from-literal=DATABASE_PASSWORD=password123 \
  --from-literal=API_KEY=secret-api-key

PersistentVolume と PersistentVolumeClaim

永続化が必要なワークロード(DB、ファイルアップロード、キャッシュの永続化など)で使います。
逆に、ステートレスなWeb APIはまず「永続化しない設計」を優先した方が運用が楽です。

ざっくり

  • PV(PersistentVolume): クラスタ側が提供するストレージの“実体”
  • PVC(PersistentVolumeClaim): Pod側が要求するストレージの“リクエスト”
# persistent-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: standard
  hostPath:
    path: /data/my-pv
# persistent-volume-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard
# Podでの使用
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:1.0.0
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: my-pvc

実践的な構成例

Webアプリケーション + データベース

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-app
# database-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  namespace: my-app
type: Opaque
stringData:
  POSTGRES_USER: myuser
  POSTGRES_PASSWORD: mypassword
  POSTGRES_DB: mydb
# database-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: my-app
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
# database-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:15
          ports:
            - containerPort: 5432
          envFrom:
            - secretRef:
                name: postgres-secret
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
      volumes:
        - name: postgres-data
          persistentVolumeClaim:
            claimName: postgres-pvc
# database-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: my-app
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432
# app-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: my-app
data:
  DATABASE_HOST: postgres
  DATABASE_PORT: "5432"
  NODE_ENV: production
# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: app
          image: my-web-app:1.0.0
          ports:
            - containerPort: 3000
          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: postgres-secret
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
# app-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-app
  namespace: my-app
spec:
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 3000
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-ingress
  namespace: my-app
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-app
                port:
                  number: 80

水平スケーリング(HPA)

HPA(HorizontalPodAutoscaler)は、負荷に応じてDeployment等のレプリカ数を自動調整します。

注意

  • requests が未設定だとCPU使用率ベースのスケールが破綻しがちです(計算の前提が崩れる)
  • 急なスケールは外部依存(DB、外部API)を先に詰まらせるため、伸び方/縮み方(behavior)も設計します
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
  namespace: my-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15
# HPAの確認
kubectl get hpa -n my-app

# 詳細表示
kubectl describe hpa web-app-hpa -n my-app

Job と CronJob

アプリ本体とは別に「バッチ」や「定期処理」をKubernetes上で回したいときに使います。

  • Job: 1回きりの処理(マイグレーション、ワンショット集計など)
  • CronJob: スケジュール実行(毎日バックアップなど)

注意

  • 冪等性(同じ処理が2回走っても壊れない)を前提に設計する
  • 実行時間が長い場合、concurrencyPolicyや履歴保持を調整する

Job(一回限りのタスク)

# job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: database-migration
spec:
  ttlSecondsAfterFinished: 3600
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migration
          image: my-app:1.0.0
          command: ["npm", "run", "migrate"]
          envFrom:
            - secretRef:
                name: postgres-secret

CronJob(定期実行)

# cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-backup
spec:
  schedule: "0 2 * * *"  # 毎日午前2時
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: backup-tool:1.0.0
              command: ["/bin/sh", "-c", "./backup.sh"]
              envFrom:
                - secretRef:
                    name: backup-credentials

Helmによるパッケージ管理

HelmはKubernetesマニフェストの「テンプレート + パッケージ管理」です。
複数リソース(Deployment/Service/Ingress/ConfigMap…)を一塊として配布・更新できます。

注意

  • 「values.yamlに何を寄せるか」を決めないと、環境差分がカオスになります
  • アプリのバージョン(image tag)とChartのバージョン(version/appVersion)を混同しない

Helmの基本

# Helmのインストール
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# リポジトリの追加
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# チャートの検索
helm search repo nginx

# チャートのインストール
helm install my-nginx bitnami/nginx

# リリースの一覧
helm list

# リリースの削除
helm uninstall my-nginx

Chartの作成

# 新しいチャートを作成
helm create my-app
my-app/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── hpa.yaml
│   └── _helpers.tpl
└── charts/
# Chart.yaml
apiVersion: v2
name: my-app
description: My application Helm chart
version: 1.0.0
appVersion: "1.0.0"
# values.yaml
replicaCount: 3

image:
  repository: my-app
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  host: myapp.example.com

resources:
  requests:
    memory: "128Mi"
    cpu: "100m"
  limits:
    memory: "256Mi"
    cpu: "500m"

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

env:
  NODE_ENV: production
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
          env:
            {{- range $key, $value := .Values.env }}
            - name: {{ $key }}
              value: {{ $value | quote }}
            {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
# テンプレートのレンダリング確認
helm template my-app ./my-app

# インストール(dry-run)
helm install my-app ./my-app --dry-run

# インストール
helm install my-app ./my-app

# 値を上書きしてインストール
helm install my-app ./my-app --set replicaCount=5

# アップグレード
helm upgrade my-app ./my-app --set image.tag=2.0.0

運用のベストプラクティス

リソース制限

requests/limitsは「後で入れる」より、最初から置いた方が事故りません。
理由は、無制限だとノード全体を巻き込んで落ちたり、逆に厳しすぎるとスケジュールできず Pending で止まるからです。

最小テンプレ(まずはここから)

resources:
  requests:
    memory: "128Mi"    # 保証されるリソース
    cpu: "100m"
  limits:
    memory: "256Mi"    # 上限
    cpu: "500m"

置き方の指針

  • requests: 「平常時これくらいは必要」を置く(スケジューリングの前提)
  • limits.memory: 暴走対策として置く(超えると OOMKilled
  • limits.cpu: 置くなら“絞りすぎ注意”(超えるとスロットリングで遅くなる)

よくある事故

  • requests が高すぎて Pending(空きノードがなく配置できない)
  • limits.memory が低すぎて OOMKilled → 再起動ループ
  • limits.cpu が低くてスロットリング → レイテンシ悪化 → readiness/livenessがタイムアウトして落ちる

補足:Troubleshootingとの対応

  • Pending / OOMKilled / Evicted が出たら、まずこの節の設計を疑うと早いです

ヘルスチェック

livenessProbe:         # 失敗するとコンテナ再起動
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10
  failureThreshold: 3

readinessProbe:        # 失敗するとサービスから除外
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

startupProbe:          # 起動完了の確認
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 0
  periodSeconds: 10
  failureThreshold: 30

readiness / liveness / startup の違い(ここで詰まりがち)

  • readinessProbe(準備できた?)
    • 目的: 「今このPodにトラフィックを流していいか」を判定
    • 失敗したら: Serviceのエンドポイントから外れる(再起動はしない)
    • 例: 起動直後のウォームアップ中、DB接続不可、依存サービス未準備
  • livenessProbe(生きてる?)
    • 目的: 「プロセスが詰んでいて、自力では戻らない」状態を検知
    • 失敗したら: コンテナ再起動
    • 例: デッドロック、メモリ破壊で応答不能、イベントループ停止
  • startupProbe(起動完了した?)
    • 目的: 起動が遅いアプリで、起動中にlivenessが誤判定して再起動ループになるのを防ぐ
    • ポイント: startupが成功するまで、liveness/readinessの判定を遅らせられる(扱いは設定次第)

よくある事故

  • readinessを「/health」1本にしてしまい、依存DBが落ちた瞬間に全Podがreadyから外れて全滅する
  • livenessに重い依存チェック(DB疎通など)を入れてしまい、外部要因で再起動ループする

Pod Disruption Budget

PDBは「計画停止(ノード入れ替えやアップグレード)でも、落ちるPod数を制限する」ための安全装置です。

使いどころ

  • 本番運用でノードメンテやオートスケールがある
  • 1台落ちたら即死するサービスを避けたい

注意

  • minAvailable を強くしすぎると、メンテが進まず詰まることがあります
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
spec:
  minAvailable: 2      # 最低2つは稼働を保証
  # または maxUnavailable: 1
  selector:
    matchLabels:
      app: my-app

Network Policy

NetworkPolicyは「どのPodからどのPodへ通信を許可するか」を宣言します。
ゼロトラスト寄りに寄せたい場合や、マルチテナント環境で重要です。

注意

  • NetworkPolicyはCNI依存です(対応していない環境では効きません)
  • まずはNamespace単位の方針→重要通信だけ絞る、の順で段階導入すると安全です
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-network-policy
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432

トラブルシューティング

障害対応は「見る順番」を固定すると速いです。

  1. events(何が起きたか)
  2. describe(状態と理由)
  3. logs(アプリの出力)
  4. exec(最後の手段)

まずは「Podが落ちているのか / readyじゃないのか / そもそもルーティングされてないのか」を切り分けます。

外から繋がらない時の最短チェック

  • DNS/証明書: そもそも名前解決とTLSが合っているか
  • Ingress: Ingress Controllerがいるか、ingressClassName が合っているか
  • Service: Endpointsが空ではないか(readinessが落ちていないか)
  • NetworkPolicy: 追加した途端に遮断していないか(CNI対応も含めて)

外部公開の疎通チェック手順(これだけで7割解ける)

前提

  • <ns> は対象Namespace(例: default
  • <app> は対象アプリ名(Deployment/Service/Ingressの名前など)
  1. IngressがControllerに拾われているか
# Ingress一覧とアドレス(ADDRESS)が付いているか
kubectl get ingress -n <ns>

# どのclassで扱う想定か / イベントにエラーが出てないか
kubectl describe ingress <ingress-name> -n <ns>

見るポイント

  • ingressClassName(またはannotation)が想定Controllerと一致
  • Events に "no IngressClass" / "service not found" 的なものが出ていない
  1. Serviceが正しいPod集合を掴んでいるか(Endpoints)
# Serviceのselectorとportの確認
kubectl describe svc <service-name> -n <ns>

# Endpointsが空じゃないか(空なら大抵selectorかreadiness)
kubectl get endpoints <service-name> -n <ns>
kubectl describe endpoints <service-name> -n <ns>

見るポイント

  • Endpoints: が空なら、まず selector とPod側 labels を疑う
  • Endpoints: が空でないのに繋がらないなら、次は targetPort とアプリの待受ポートを疑う
  1. PodがReadyか、落ちてないか(readiness/liveness)
kubectl get pods -n <ns> -o wide
kubectl describe pod <pod-name> -n <ns>

# readinessの失敗理由をログと合わせて見る
kubectl logs <pod-name> -n <ns>

見るポイント

  • READY0/1 なら、ServiceのEndpointsに入りません(=繋がらない)
  • EventsReadiness probe failed / Liveness probe failed が出ていないか
  1. NetworkPolicyで遮断していないか
kubectl get networkpolicy -n <ns>
kubectl describe networkpolicy <np-name> -n <ns>

見るポイント

  • まず「PolicyがあるNamespaceか」を確認(あるだけでデフォルト拒否になる実装もある)
  • ingress/egress の podSelector / ports の指定が意図通りか
  1. それでもダメなら「アプリ自体は応答できるか」を最短で確認

IngressやServiceの外側から到達できない原因が、実は

  • アプリが想定ポートでlistenしていない
  • ルーティング/パスが違っていて外からは404
  • アプリが内部的に依存(DB等)で詰まっていてタイムアウト

のように “中身” 側にあることも多いです。

# Service越しにローカルへ転送して叩く(Ingressを介さず切り分け)
kubectl port-forward -n <ns> svc/<service-name> 18080:80

# 例: /health を確認(PowerShellでもcurlは使えます)
curl http://127.0.0.1:18080/health

さらに一歩(Pod内から疎通確認)

# コンテナに入って、待受ポートやHTTP応答を確認
kubectl exec -n <ns> -it <pod-name> -- /bin/sh

# (コンテナ内)
# 例: ポート確認やHTTP疎通(busybox/alpine等で利用可な範囲で)
# wget -qO- http://127.0.0.1:8080/health

見るポイント

  • port-forwardで健康チェックが通るなら「Ingress/LB/DNS側」を疑う
  • port-forwardでもダメなら「アプリ設定/待受ポート/Probe/依存先」を疑う

Probe起因で詰まるとき(再起動ループ / readyにならない)

症状と原因の当たり

  • Podが再起動を繰り返すRESTARTS が増える / CrashLoopBackOff
    • まず疑う: livenessProbe が厳しすぎる(起動が遅い、タイムアウトが短い、失敗許容が少ない)
  • Podは落ちないが、ずっとReadyにならないREADY 0/1 のまま)
    • まず疑う: readinessProbe が厳しすぎる / 依存先(DB等)が未準備

最初に見る場所

kubectl describe pod <pod-name> -n <ns>
kubectl logs <pod-name> -n <ns>
kubectl logs <pod-name> -n <ns> --previous

調整の基本(“どれをいじるか”)

  • initialDelaySeconds: 起動直後は見ない(まず時間を稼ぐ)
  • timeoutSeconds: たまに遅いだけで落ちるのを防ぐ
  • failureThreshold × periodSeconds: 何秒連続で失敗したらNGにするか

startupProbe を足す判断

  • 起動が重い/ウォームアップが必要なアプリは、startupProbeで「起動完了までlivenessを待たせる」
  • これがないと、起動途中をlivenessが殺して 再起動ループになりがち

読み違え注意

  • readiness失敗は「トラフィックを止める」だけ(再起動しない)
  • liveness失敗は「再起動」なので、外部依存(DB等)の不調チェックを入れると自爆しやすい

Podの典型ステータス別:原因の目星と最短対処

まずはこれだけ

kubectl get pods -n <ns> -o wide
kubectl describe pod <pod-name> -n <ns>
kubectl logs <pod-name> -n <ns>
kubectl get events -n <ns> --sort-by='.lastTimestamp'
症状/ステータス ありがちな原因 最初に見る場所 まずやる対処
Pending のまま リソース不足(CPU/mem)/ nodeSelector・tolerations・affinity・PVC未バインド kubectl describe podEvents / kubectl get nodes / kubectl describe node requests/制約を見直す、ノード増強、PVC/StorageClass確認
ErrImagePull / ImagePullBackOff イメージ名/タグ誤り、レジストリ認証(imagePullSecret)、Pull制限 kubectl describe podEvents イメージ名修正、imagePullSecrets 追加、レジストリ疎通確認
CrashLoopBackOff 起動直後にアプリが落ちる、設定不足、probeが厳しすぎ、依存先未準備 kubectl logs --previous / describe pod まずログで原因特定、設定(env/secret)確認、必要ならstartupProbe導入
OOMKilled(再起動する) limits.memory が低い / メモリリーク / JVM等の設定不一致 kubectl describe podLast State / kubectl top pod memory limit引き上げ、アプリのメモリ設定見直し、HPAや垂直スケール検討
Evicted ノードが逼迫(メモリ/ディスク)で追い出された kubectl describe podStatus/Reason / describe node ノード資源を増やす、requests/limits適正化、ログ/一時領域の削減
READY 0/1(落ちない) readinessが失敗、依存先未準備、path/port違い kubectl describe podReadiness probe failed / アプリログ readinessの条件を見直す、依存を切り離す、timeoutや閾値調整
# Podの状態確認
kubectl get pods -o wide

# Podの詳細(イベント確認)
kubectl describe pod <pod-name>

# ログの確認
kubectl logs <pod-name>
kubectl logs <pod-name> -f              # フォロー
kubectl logs <pod-name> --previous      # 前のコンテナ
kubectl logs <pod-name> -c <container>  # 特定コンテナ

# Podに入る
kubectl exec -it <pod-name> -- /bin/sh

# リソース使用量
kubectl top pods
kubectl top nodes

# イベントの確認
kubectl get events --sort-by='.lastTimestamp'

# すべてのリソースを確認
kubectl get all -n <namespace>

まとめ

リソース 役割
Pod コンテナの実行単位
Deployment ロールアウト/ロールバック(ReplicaSetの世代管理)
Service 安定した入口(ClusterIP/DNS/負荷分散・Service Discovery)
Ingress 主にHTTP/HTTPSの外部公開・L7ルーティング・TLS
ConfigMap 設定情報
Secret 機密情報
PVC 永続ストレージ
HPA 自動スケーリング

Kubernetesを活用して、スケーラブルで信頼性の高いコンテナ運用を実現しましょう!

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?