目次
22 章

アプリ配備の骨格

第21章で立ち上げた空の EKS クラスタの上に、サンプルサービス myshop-api を一つのまとまりのマニフェストで配備します。Namespace · ServiceAccount · ConfigMap · Secret · Deployment · Service · Ingress · HPA · PodDisruptionBudget の9つのオブジェクトを一連の流れで整理し、AWS Load Balancer Controller で ALB を自動プロビジョニングします。そのまとまりを Helm チャートで抽象化して dev / prod に異なる values で適用する方法までを一度にたどります。

第21章 EKS クラスタセットアップ で空の EKS クラスタ一台が準備できました。ノードは立ち上がっていてシステム Pod (CoreDNS, kube-proxy, VPC CNI, EBS CSI) は生きていますが、私たちのワークロードは一行もその上に載っていない状態です。本章はその空いた場所を埋めます。サンプルのバックエンドサービス myshop-api を9つのマニフェストに整理し、AWS Load Balancer Controller で ALB を自動プロビジョニングして外部の入り口を作り、そのすべてのマニフェストを Helm チャートで束ねて dev / prod の二環境に異なる values で配備されるようにします。

本章の終わりでは myshop-api が外部から HTTPS でアクセス可能な状態になります。データストアがないので /health/ready だけが 200 を返す空の殻ですが、その空いた部分は次の 第23章 で RDS · External Secrets で埋めます。

myshop-api の標準マニフェストのまとまり #

運用ワークロード一つを K8s に載せる標準のまとまりを一つの表に整理します。

オブジェクト役割本書の出所
Namespaceワークロードを束ねる隔離単位第7章
ServiceAccountPod の ID + IRSA の付着点第16章
ConfigMap環境設定値 (ログレベル、feature flag)第6章
SecretDB パスワードなど第6章
Deployment実際のワークロード (Pod replicas)第4章
Serviceクラスタ内部の仮想 IP第5章
Ingress外部の入り口 (ALB に解決される)第10章
HorizontalPodAutoscalerCPU ベースの Pod 自動調整第13章
PodDisruptionBudget自発的 disruption の可用性の下限線第13章

1 ~ 3部でオブジェクトの次元で分けて扱ったモデルが、この一つの表の中に集まります。本章はそのモデルを 一つのまとまりの運用マニフェストへ組み立てる段階 です。各オブジェクトの単独の使い方よりも、まとまりとして組み立てたときの環境変数の注入方式に集中します。

Namespace と ServiceAccount #

00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: myshop
  labels:
    name: myshop
    team: platform
    env: prod
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myshop-api
  namespace: myshop
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myshop-prod-api

Namespace のラベルは 第7章 Namespace とラベル で扱った標準です。team / env / cost-center のようなラベルを一貫して付けておくと、第25章 モニタリング · アラート の Prometheus がワークロードをグルーピングするときにそのまま活用され、第28章 コスト最適化 のコスト追跡でも同じラベルをキーに使います。

ServiceAccount の IRSA annotation は 第16章 RBAC / ServiceAccount 深掘り のその annotation です。書かれた IAM Role の権限を、myshop-api Pod がこの ServiceAccount で動作しながら自動で受け取ります。Role 自体の定義は第21章で整理した Terraform パターンで管理します。

terraform/modules/myshop-api/irsa.tf
module "myshop_api_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

  role_name = "myshop-prod-api"

  oidc_providers = {
    main = {
      provider_arn               = var.oidc_provider_arn
      namespace_service_accounts = ["myshop:myshop-api"]
    }
  }

  role_policy_arns = {
    secrets = aws_iam_policy.myshop_secrets_read.arn
  }
}

この Terraform が作った Role の ARN を ServiceAccount の annotation に書きます。以降、Pod 内の AWS SDK はこの Role の権限で Secrets Manager · S3 · CloudWatch を呼べます。namespace_service_accounts の trust 制約が、myshop ネームスペースの myshop-api ServiceAccount だけがこの Role を取得できるように束ねるセキュリティ境界です。

ConfigMap と Secret #

01-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myshop-api
  namespace: myshop
data:
  LOG_LEVEL: "info"
  FEATURE_NEW_CHECKOUT: "true"
  CACHE_TTL_SECONDS: "300"
  AWS_REGION: "ap-northeast-2"
---
apiVersion: v1
kind: Secret
metadata:
  name: myshop-api
  namespace: myshop
type: Opaque
stringData:
  DATABASE_URL: "postgresql://will-be-replaced-by-external-secrets"

Secret の実際の値はマニフェストに置きません。第23章 DB 連携 で External Secrets Operator が AWS Secrets Manager の秘密を K8s Secret へ自動同期する方式を扱います。本章では骨格だけを固めておき、第20章 GitOps の秘密管理モデルと併せて 第24章 CI / CD パイプライン で改めて組み合わせます。

Deployment — Pod replicas #

02-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myshop-api
  namespace: myshop
  labels:
    app.kubernetes.io/name: myshop-api
    app.kubernetes.io/component: backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: myshop-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app.kubernetes.io/name: myshop-api
        app.kubernetes.io/component: backend
    spec:
      serviceAccountName: myshop-api
      containers:
        - name: api
          image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myshop-api:1.4.2
          ports:
            - containerPort: 8000
              name: http
          envFrom:
            - configMapRef:
                name: myshop-api
            - secretRef:
                name: myshop-api
          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"
            limits:
              cpu: "1"
              memory: "512Mi"
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health/live
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /health/live
              port: http
            failureThreshold: 30
            periodSeconds: 10
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: myshop-api

運用マニフェストの標準要素が集まっています。それぞれが本書のどの章で解かれたかを押さえると次のとおりです。

  • strategy.rollingUpdate第4章 Deployment / ReplicaSet のローリングアップデート。maxUnavailable: 0 で新バージョンが十分に立ち上がってからでないと旧バージョンを下げない、無停止の基本的な保護です。
  • envFrom第6章 ConfigMap · Secret のまとまった注入。ConfigMap と Secret のすべてのキーを一度に環境変数として展開してくれます。
  • 三つの probe第12章 ヘルスチェック のその三つ。readiness は即時 (5秒後に開始)、liveness は保守的に (30秒後に開始)、startup は初期化が長い場合の猶予です。
  • resources第11章 リソース要求と上限 の requests / limits。今回のワークロードは requests < limits の Burstable QoS クラスに属します。
  • topologySpreadConstraints — Pod を複数の AZ に分散。AZ 一つが落ちても別の AZ の Pod が生き残ります。
  • app.kubernetes.io/ 標準ラベル第7章 のその標準。selector · モニタリング · ロギングがすべてこのラベルでグルーピングされます。

Service — クラスタ内部の固定の入り口 #

03-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myshop-api
  namespace: myshop
  labels:
    app.kubernetes.io/name: myshop-api
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: myshop-api
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP

type: ClusterIP が核心です。Service 自体はクラスタ内部からのみアクセス可能な仮想 IP であり、外部公開は次に作る Ingress が担当します。第5章 Service で扱ったそのモデルそのままです。アプリの外部公開は Service ではなく Ingress で決める — この分離が運用クラスタの標準パターンで、内部通信と外部公開の質感を別のレイヤーに束ねるセキュリティ · 運用の出発点です。

AWS Load Balancer Controller — Ingress の ALB マッピング #

EKS の Ingress は K8s 標準の Ingress オブジェクトですが、そのオブジェクトを実際の ALB (AWS Application Load Balancer) に解決するには AWS Load Balancer Controller というコンポーネントがクラスタにインストールされている必要があります。第10章 Ingress の §「クラウド別 Ingress Controller」で触れたそのコンポーネントが、本格的なセットアップ段階で扱われます。

Helm でインストール
helm repo add eks https://aws.github.io/eks-charts
helm repo update

helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=myshop-prod \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller

ServiceAccount はあらかじめ IRSA で作っておきます (第21章 の EBS CSI IRSA マニフェストと同じパターン)。この controller がクラスタの中で Ingress オブジェクトを watch していて、Ingress が作られるとそれに合う ALB を AWS API でプロビジョニングします。Controller は K8s と AWS の二つの質感をつなぐブリッジ という位置を覚えておくとよいです — 同じパターンが 第23章 の External Secrets Operator、第25章 の CloudWatch Container Insights でも繰り返されます。

Ingress マニフェスト #

04-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myshop-api
  namespace: myshop
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
    alb.ingress.kubernetes.io/ssl-redirect: '443'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123
    alb.ingress.kubernetes.io/healthcheck-path: /health/ready
    external-dns.alpha.kubernetes.io/hostname: api.myshop.example.com
spec:
  rules:
    - host: api.myshop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myshop-api
                port:
                  number: 80

annotation 一つずつの意味を整理します。

  • scheme: internet-facing — インターネット公開の ALB です。内部用は internal にします。
  • target-type: ip — Pod IP を ALB のターゲットグループに直接登録します。ALB が Pod IP の変動を watch する流れです。
  • listen-ports / ssl-redirect — HTTPS だけを受けて HTTP は自動で redirect します。
  • certificate-arn — ACM 証明書。Route 53 と関連するドメインの証明書を ACM で発行しておきます。
  • healthcheck-path — ALB が各 Pod に送るヘルスチェックのパス。第12章 の readiness probe と同じパスに合わせるのが一般的です。
  • external-dns.alpha.kubernetes.io/hostname — external-dns コンポーネントがこの ALB の DNS を Route 53 の A レコードとして自動登録します。

このマニフェストを適用した時点から約 1 ~ 2分後に、AWS コンソールで新しい ALB が立ち上がっていて、api.myshop.example.com ドメインがその ALB へつながっている形が出来上がります。

HPA — トラフィックに応じた Pod 自動調整 #

05-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myshop-api
  namespace: myshop
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myshop-api
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300

CPU 使用率が平均 60 % を超えると Pod 数を増やし、下がれば減らします。第13章 オートスケーリング のそのモデルそのままです。behavior の stabilization ウィンドウが運用の要 — scale-up は30秒で速く、scale-down は5分で保守的に 置く非対称が核心です。トラフィックスパイクには素早く対応しながらも、一時的な dip で Pod が上下に揺れて減らないようにする質感です。

PodDisruptionBudget — 可用性の下限線 #

06-pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myshop-api
  namespace: myshop
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: myshop-api

この一枚が何を防ぐのか — ノードアップグレード / ノードドレイン / Cluster Autoscaler のノード回収のような 自発的 disruption 時に、myshop-api Pod が同時に2個未満に落ちないことを保証します。K8s 運用の定番の事故 — 「EKS アップグレード中に Pod が一度に全部下がってダウンタイムが発生」を防ぐツールです。第26章 運用チェックリスト のクラスタアップグレード手順で、PDB がなぜ必須かが本格的に解かれます。

PDB の核心は 自発的 (voluntary) という限定です。ノードの突然の障害のような 非自発的 disruption は PDB では防げません — その質感は topologySpreadConstraints と multi-AZ ノードグループが担当します。

Helm チャートで束ねる #

ここまで書いた9枚のマニフェストはそれ自体でも動作します。しかし dev / prod に同じワークロードを異なる値で配備するには、マニフェストを環境ごとに複製する負担が生じます。この地点で Helm が入ってきます。第18章 CRD と Operator の §「Helm チャートで束ねる」で触れた Helm の位置が、本格的な運用チャートとして扱われる段階です。

Chart 構造 #

charts/myshop-api/ ディレクトリ
charts/myshop-api/
├── Chart.yaml
├── values.yaml             # デフォルト値
├── values-dev.yaml         # dev override
├── values-prod.yaml        # prod override
└── templates/
    ├── _helpers.tpl
    ├── namespace.yaml
    ├── serviceaccount.yaml
    ├── configmap.yaml
    ├── secret.yaml
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── hpa.yaml
    └── pdb.yaml

Chart.yaml #

Chart.yaml
apiVersion: v2
name: myshop-api
description: myshop API service
type: application
version: 0.1.0
appVersion: "1.4.2"
maintainers:
  - name: platform-team
    email: platform@myshop.example.com

version がチャート自体のバージョン、appVersion がそのチャートが配備するアプリケーションのバージョンです。二つが分離される理由 — チャートマニフェストはそのままでイメージタグだけを上げる流れが多いためです。

values.yaml — デフォルト値 #

values.yaml
image:
  repository: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myshop-api
  tag: "1.4.2"
  pullPolicy: IfNotPresent

replicaCount: 3

resources:
  requests:
    cpu: 200m
    memory: 256Mi
  limits:
    cpu: 1
    memory: 512Mi

serviceAccount:
  create: true
  name: myshop-api
  irsaRoleArn: ""

config:
  LOG_LEVEL: info
  FEATURE_NEW_CHECKOUT: "true"
  CACHE_TTL_SECONDS: "300"

ingress:
  enabled: true
  host: api.myshop.example.com
  certificateArn: ""

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
  targetCPUUtilizationPercentage: 60

pdb:
  enabled: true
  minAvailable: 2

values-prod.yaml — 環境別 override #

values-prod.yaml
replicaCount: 5

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 2
    memory: 1Gi

serviceAccount:
  irsaRoleArn: arn:aws:iam::123456789012:role/myshop-prod-api

config:
  LOG_LEVEL: warn

ingress:
  host: api.myshop.example.com
  certificateArn: arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123

autoscaling:
  minReplicas: 5
  maxReplicas: 50

Template — Deployment 一枚の例 #

templates/deployment.yaml — 一部
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myshop-api.fullname" . }}
  labels:
    {{- include "myshop-api.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myshop-api.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myshop-api.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ .Values.serviceAccount.name }}
      containers:
        - name: api
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          envFrom:
            - configMapRef:
                name: {{ include "myshop-api.fullname" . }}
            - secretRef:
                name: {{ include "myshop-api.fullname" . }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

{{ include "myshop-api.fullname" . }} のような Helper は _helpers.tpl に定義しておきます。Helm チャートの標準パターンで、helm create で骨格を作ると自動で入ってきます。

配備 — 同じチャート、異なる環境 #

dev 配備
helm upgrade --install myshop-api charts/myshop-api \
  -n myshop --create-namespace \
  -f charts/myshop-api/values.yaml \
  -f charts/myshop-api/values-dev.yaml
prod 配備
helm upgrade --install myshop-api charts/myshop-api \
  -n myshop --create-namespace \
  -f charts/myshop-api/values.yaml \
  -f charts/myshop-api/values-prod.yaml

同じチャートが環境別に異なって適用されます。この一つのコマンドは 第24章 CI / CD パイプライン の ArgoCD Application sync に置き換えられますが、開発段階の手動配備にはそのまま有効です。第20章 GitOps の git 単一ソースモデルが、この Helm チャートのディレクトリへ自然につながります。

cert-manager と external-dns — 付加コンポーネント #

ACM 証明書を直接発行 · 更新する代わりに、クラスタの中で Let’s Encrypt 証明書を自動で受け取りたいなら cert-manager、Route 53 の DNS レコードをマニフェストで自動管理したいなら external-dns を併せて導入します。

cert-manager インストール
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  -n cert-manager --create-namespace \
  --set installCRDs=true
external-dns インストール
helm install external-dns external-dns/external-dns \
  -n external-dns --create-namespace \
  --set provider=aws \
  --set aws.region=ap-northeast-2 \
  --set "domainFilters[0]=myshop.example.com"

運用クラスタの標準セットアップです。ドメインが追加されるたびに Route 53 コンソールを開かず、マニフェストの annotation 一行で終わる流れが出来上がります。ACM 証明書を使うときは cert-manager は必要ありませんが、マルチクラスタ · マルチクラウド環境では cert-manager の方が一貫します。二つのコンポーネントの ServiceAccount も 第21章 の EBS CSI IRSA と同じパターンで IAM Role を受け取らないと、Route 53 のレコードを変更できません。

最初の配備後の点検 #

配備直後に点検する項目です。

基本のヘルスチェック
kubectl get all -n myshop
kubectl get hpa -n myshop
kubectl get pdb -n myshop
kubectl get ingress -n myshop
ALB プロビジョニング確認 (1 ~ 2分後)
kubectl describe ingress myshop-api -n myshop | grep "Address"
外部からアクセス
curl -i https://api.myshop.example.com/health/ready

この四段階で、Pod · HPA · Ingress · 外部からの入り口までが動作する状態かが確認されます。200 応答が返れば、クラスタの最初のワークロードが運用に入った時点です。もし ALB の Address が空だったり 503 が返ったりしたら、第27章 kubectl デバッグパターン の Ingress デバッグ節を先に見ておくと役に立ちます。

練習問題 #

  1. 本章の9つのマニフェストを一つのディレクトリに展開しておき、第21章 で立ち上げた dev EKS クラスタに順に適用します。kubectl get all -n myshop の出力で、Deployment が 3 / 3、ReplicaSet が同一、Pod がすべて Running、Service の ClusterIP が割り当てられたところまで確認します。Ingress の ALB Address が立ち上がるまで約 1 ~ 2分かかりますが、この時間のあいだに AWS コンソールで ALB · TargetGroup · Route 53 の A レコードが順に作られる様子を併せて観察します。
  2. 本章の9つのマニフェストを Helm チャート一つのまとまりに変換します。helm create myshop-api で骨格を作ったあと、values.yaml · values-dev.yaml · values-prod.yaml の三つのファイルがどう分かれるかを整理し、dev の replicaCount と prod の replicaCount が異なって適用されるかを helm template で事前検証します。この事前検証が 第24章 CI / CD の ArgoCD diff モデルとどうつながるかを一段落でメモします。
  3. topologySpreadConstraintswhenUnsatisfiableScheduleAnywayDoNotSchedule の二つに変えながら、Pod の分布がどう変わるかを観察します。AZ が二つだけなのに replicaCount が3の場合、どのオプションがどんなトレードオフを作るかを、ご自身の運用シナリオに照らして一段落で比較します。第21章 の VPC モジュールでの単一 AZ vs マルチ AZ の決定が、このオプションの結果をどう揺らすかも併せて押さえます。

一行まとめ: 運用ワークロード一つの標準は Namespace · ServiceAccount · ConfigMap · Secret · Deployment · Service · Ingress · HPA · PodDisruptionBudget の9つのオブジェクトであり、このまとまりを Helm チャートで抽象化すれば、同じチャートが dev / prod に異なる values で展開される。EKS の Ingress は AWS Load Balancer Controller が ALB として実際に解決し、ACM 証明書 + Route 53 + external-dns がドメイン · TLS · DNS の三つの質感をマニフェストの annotation 一行で束ねる。

次の章 #

この時点で myshop-api は外部から HTTPS でアクセス可能な状態ですが、データストアがなく /health/ready だけが 200 を返す空の殻です。次の章ではその空いた部分を埋めます。

第23章 DB 連携 では、RDS PostgreSQL との接続、Secrets Manager で DB パスワードを安全に注入する道、External Secrets Operator で K8s Secret とクラウドの秘密ストアを同期する流れ、そしてコネクションプールの運用原則までを一つのサイクルで扱います。

X