K8s 実戦 #1 EKS クラスタセットアップ — Terraform / eksctl / IRSA / アドオン

読了 11分

K8s 実戦シリーズの最初の記事です。基礎・中級・上級トラック(20 編)がマニフェスト 1 枚のモデルからポリシーエンジンとオブザーバビリティまで K8s をオブジェクト次元で追った道だったとすれば、実戦シリーズ 6 編はその上に 本物のサービスを 1 つ載せて運用する流れ です。仮想のバックエンドサービス myshop-api を EKS に載せて、RDS と接続し、CI/CD でデプロイし、モニタリング・運用段階まで 1 セットで追います。この記事はその流れの出発点 — EKS クラスタを最初から立てる段階 です。Terraform で VPC とクラスタを宣言し、ノードグループと IRSA をセットアップし、必須アドオンまで載せる流れをまとめます。

このシリーズは K8s 実戦 6 編です。

ヒント
本シリーズの実習記事は Terraform・Helm・K8s YAML マニフェストを手で書いていきます。インデント 1 つ、引用符 1 つがずれただけで kubectl apply が意図と異なるエラーを返し、原因をクラスタ側から逆に辿ることになります。マニフェストを適用する前に utilrepo の YAML 検証ツール に貼り付けておくと、構文エラーを行・列番号で示してくれます。utilrepo はブラウザで動作する軽量な Web ユーティリティ集で、秘密情報が外部に出ず --- で連結された複数文書マニフェストやタブ・スペース混在のような頻出する罠もまとめて拾ってくれます。

myshop-api — シリーズを貫く仮想サービス #

6 編を束ねるシナリオを 1 行で押さえておきます。仮想の会社 myshop が自社バックエンド API サービス myshop-api を EKS に載せると仮定します。仕様は単純です。

  • Python(FastAPI)または Go で書いた小さな REST API
  • RDS PostgreSQL をデータストアとして使用
  • HTTPS で外部公開
  • prod / dev の 2 環境
  • トラフィック変動に従って Pod 個数を自動調整

このシナリオを #1 のクラスタから #6 の運用サイクルまで一貫して追います。各記事が次の記事の入力になる構造で、最後の記事まで追ってきた時点で実際の運用クラスタの 1 サイクルが手に入ります。

セットアップツールの選択 — Terraform vs eksctl #

EKS クラスタを作る選択肢は複数あります。

ツールモデル適した用途
AWS コンソールクリック学習 / 一度見る
eksctlYAML 1 枚 + CLIPoC / 早いセットアップ / 学習
TerraformHCL で宣言運用クラスタ / マルチ環境 / IaC 標準
AWS CDK / Pulumiコード(TypeScript、Python など)で宣言コード親和的なチーム / 複雑な分岐

運用クラスタの事実上の標準は Terraform です。VPC・IAM・EKS コントロールプレーン・ノードグループ・アドオン・RDS・Route53 までを 1 つのコードベースで宣言して git に保管できるので、クラスタ自体がコードで再現可能になります。同じマニフェストが dev / prod の 2 つの環境に同一に適用され、変更は PR レビューを経て入ります。

eksctl がその位置を代替するには抽象化の肌理が EKS に限定的すぎますが、早いセットアップと学習 にはもっとも直感的です。この記事では Terraform をメインで扱い、eksctl を比較オプションとして短く押さえます。

Terraform プロジェクト構造 #

myshop-api インフラの Terraform コード構造から押さえます。

terraform/ ディレクトリ構造
terraform/
├── modules/
│   ├── network/         # VPC、サブネット、NAT、ルーティング
│   ├── eks/             # EKS クラスタ + ノードグループ
│   └── addons/          # VPC CNI、EBS CSI、IRSA roles
├── envs/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       └── ...
└── versions.tf

modules/ に再利用可能な単位を置き、envs/ で dev / prod を異なってインスタンス化する構造です。dev / prod の差はインスタンスタイプ、ノード個数、マルチ AZ 程度で、モジュール自体は共有します。

Provider と backend #

envs/prod/main.tf — provider と state backend
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket         = "myshop-tfstate"
    key            = "eks/prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "myshop-tfstate-lock"
    encrypt        = true
  }
}

provider "aws" {
  region = "ap-northeast-2"

  default_tags {
    tags = {
      Project     = "myshop"
      Environment = "prod"
      ManagedBy   = "terraform"
    }
  }
}

state ファイルを S3 に、ロックを DynamoDB に置く標準パターンです。default_tags がこの provider で作ったすべてのリソースに自動でタグを付けてくれるのでコスト追跡が単純になります。

VPC — EKS の土台 #

EKS は自分の VPC を作りません。ユーザーが作った VPC の上にコントロールプレーンの ENI を挿す構造です。なのでクラスタを作る前に VPC とサブネットから決めなければなりません。

VPC モジュール — terraform-aws-modules/vpc/aws #

VPC を最初から直接定義せずにコミュニティモジュールを使うのが標準です。

modules/network/main.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${var.project}-${var.env}"
  cidr = "10.10.0.0/16"

  azs              = ["ap-northeast-2a", "ap-northeast-2c"]
  private_subnets  = ["10.10.1.0/24",  "10.10.2.0/24"]
  public_subnets   = ["10.10.101.0/24", "10.10.102.0/24"]
  database_subnets = ["10.10.201.0/24", "10.10.202.0/24"]

  enable_nat_gateway     = true
  single_nat_gateway     = var.env == "dev"
  one_nat_gateway_per_az = var.env == "prod"

  enable_dns_hostnames = true
  enable_dns_support   = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = "1"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = "1"
  }
}

EKS と組み合わさるときの核心は最後の 2 つのタグです。

  • kubernetes.io/role/elb = 1 — public サブネットに付けて、AWS Load Balancer Controller が外部用 LB をこのサブネットに作ります。
  • kubernetes.io/role/internal-elb = 1 — private サブネットに付けて、内部用 LB(クラスタ内部 + VPC 内)をこちらに作ります。

このタグがないと LB がどのサブネットに入るかわからなくて #2 で作る Ingress が動作しません。

single NAT vs NAT per AZ #

single_nat_gateway は dev でだけオンにします。NAT Gateway 1 つのコストが少なくないので(時間あたり + データ転送量あたり)dev 環境では 1 つに束ね、prod は AZ 別に 1 つずつ置いて 1 つの AZ が死んでも別の AZ のワークロードが NAT を失わないようにします。

EKS モジュール — コントロールプレーン + ノードグループ #

VPC が準備できれば EKS クラスタ自体を定義します。

modules/eks/main.tf
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "${var.project}-${var.env}"
  cluster_version = "1.30"

  vpc_id     = var.vpc_id
  subnet_ids = var.private_subnet_ids

  cluster_endpoint_public_access  = true
  cluster_endpoint_private_access = true

  enable_irsa = true

  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent = true
    }
    aws-ebs-csi-driver = {
      most_recent              = true
      service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
    }
  }

  eks_managed_node_groups = {
    general = {
      desired_size = var.env == "prod" ? 3 : 2
      min_size     = 2
      max_size     = 10

      instance_types = ["t3.medium"]
      capacity_type  = var.env == "prod" ? "ON_DEMAND" : "SPOT"

      labels = {
        role = "general"
      }
    }
  }
}

この 1 つのモジュールが次のすべてを作り出します。

  • EKS コントロールプレーン (管理型 K8s 1.30)
  • クラスタの IAM Role
  • OIDC provider (IRSA の基盤 — enable_irsa = true)
  • Managed Node Group (EC2 インスタンスでワーカーノード生成)
  • 標準アドオン 4 種 (VPC CNI、CoreDNS、kube-proxy、EBS CSI Driver)

上級 #2 IRSA 節 で扱った OIDC provider がここで自動的に有効化され、後の #3 で ServiceAccount を IAM Role と組み合わせるときにその OIDC が信頼の基盤になります。

Managed Node Group という標準 #

EKS のワーカーノードは 3 つの方法で構成できます。

種類モデル
Managed Node GroupEKS が EC2 ノードのライフサイクルを管理。もっとも標準的な道。
Self-managed Node Groupユーザーが直接 EC2 グループ運用。高度なカスタマイズ時。
Fargateサーバレス。ノード自体を気にしません。コスト・制約あり。

Managed Node Group が最初の導入の標準です。インスタンスタイプ・サイズ・capacity_type(ON_DEMAND / SPOT)をマニフェストで定義すれば EKS がノードの参加・除去・アップグレードを自動で管理します。ノード自体の OS は Amazon Linux 2 または Bottlerocket で、kubelet・containerd・CNI エージェントが事前にインストールされた AMI を受け取ります。

クラスタ endpoint の 2 つのモード #

cluster_endpoint_public_accesscluster_endpoint_private_access の 2 つのフラグの組み合わせがクラスタ API サーバアクセスを制御します。

publicprivate意味
truefalseインターネットから誰でもアクセス (RBAC だけがセキュリティ境界)
truetrueインターネット + VPC 内部の両方 (もっとも一般的な設定)
falsetrueVPC 内部のみ。もっとも厳格。bastion や VPN 必要。

prod 環境のセキュリティガイドは通常最後(private only)ですが、GitHub Actions から直接 kubectl を呼ぶには public がオンでなければなりません。一般的に public + IP 制限(cluster_endpoint_public_access_cidrs)を一緒に掛ける折衷がよく使われます。

IRSA — アドオンの IAM Role 付与 #

EBS CSI Driver、AWS Load Balancer Controller、External Secrets のようなアドオンはクラスタ内で AWS API を呼び出します。この呼び出しに IRSA で権限を付与するパターンを押さえます。

modules/eks/irsa-ebs-csi.tf
module "ebs_csi_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

  role_name = "${var.project}-${var.env}-ebs-csi"

  attach_ebs_csi_policy = true

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"]
    }
  }
}

このモジュールが次を自動で作ります。

  • IAM Role (名前が myshop-prod-ebs-csi)
  • ポリシー (EBS ディスク生成・削除・attach 権限)
  • Trust policy (上級 #2 IRSA のその trust policy — kube-system namespace の ebs-csi-controller-sa ServiceAccount だけがこの Role を取れるように制限)

この IAM Role の ARN が上の cluster_addons.aws-ebs-csi-driver.service_account_role_arn に渡されて、EKS が EBS CSI アドオンの ServiceAccount に自動で annotation を付けてくれます。結果として PVC を作ると EBS ボリュームが自動でプロビジョニングされる流れが動作します。

同じパターンが #3 の External Secrets にも、#5 で扱う CloudWatch 側にも適用されます。ワークロード別に ServiceAccount + IAM Role 1:1 マッピング が K8s 実戦の標準セキュリティ構造です。

kubeconfig — クラスタにアクセス #

Terraform でクラスタを作った後にローカルで kubectl がそのクラスタを呼べるように kubeconfig を取ってこなければなりません。

kubeconfig 更新
aws eks update-kubeconfig \
  --region ap-northeast-2 \
  --name myshop-prod
確認
kubectl get nodes
kubectl get pods -A
期待出力
NAME                                            STATUS   ROLES    AGE   VERSION
ip-10-10-1-145.ap-northeast-2.compute.internal  Ready    <none>   3m    v1.30.0
ip-10-10-2-201.ap-northeast-2.compute.internal  Ready    <none>   3m    v1.30.0
ip-10-10-2-83.ap-northeast-2.compute.internal   Ready    <none>   3m    v1.30.0

3 つのノードが立っていて kube-system namespace のシステム Pod(coredns、kube-proxy、aws-node、ebs-csi-controller)がすべて Running 状態であればクラスタが正常です。

アクセス権限 — aws-auth ConfigMap の役割 #

EKS クラスタを作った IAM ユーザー/Role が自動で system:masters 権限を持ちます。他の IAM ユーザーにアクセス権限を与えるには aws-auth ConfigMap を修正するか、1.23+ の新モデルである EKS Access Entries を使用します。Access Entries がより標準で Terraform モジュールでも推奨されます。

EKS Access Entries — Terraform
access_entries = {
  developers = {
    principal_arn = "arn:aws:iam::123456789012:role/Developer"

    policy_associations = {
      view = {
        policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy"
        access_scope = {
          type = "cluster"
        }
      }
    }
  }
}

IAM Role 1 個をクラスタ全体の view 権限と組み合わせるマニフェストです。K8s RBAC の view ClusterRole が自動でマッピングされて、この Role を持つユーザーはクラスタのすべてのオブジェクトを読めるようになります。

eksctl — 早いセットアップの道 #

学習や PoC で早いクラスタが必要なときに eksctl が 1 行で終わります。

cluster.yaml — eksctl マニフェスト
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: myshop-dev
  region: ap-northeast-2
  version: "1.30"

vpc:
  nat:
    gateway: Single

managedNodeGroups:
  - name: general
    instanceType: t3.medium
    desiredCapacity: 2
    minSize: 2
    maxSize: 5
    spot: true

addons:
  - name: vpc-cni
  - name: coredns
  - name: kube-proxy
  - name: aws-ebs-csi-driver

iam:
  withOIDC: true
クラスタ生成
eksctl create cluster -f cluster.yaml

この 1 つのコマンドで VPC・EKS・ノードグループ・OIDC provider・基本アドオンまで自動で作られます。15~20 分くらいかかり、終わると kubeconfig も自動で更新されます。

eksctl の肌理を整理すると次のとおりです。

  • 長所 — 学習曲線がもっとも低い、1 つのマニフェストでクラスタ 1 台まるごと
  • 短所 — マルチ環境 / RDS / Route53 のような周辺リソースと 1 セットで管理しにくい。CloudFormation を内部的に使うので Terraform と state が分離される。

運用クラスタの到着点はほぼ常に Terraform ですが、最初の学習や 1 度限りの PoC には eksctl がもっとも早いです。

Karpenter — ノードオートスケーリングの新しい道 #

Managed Node Group は自体オートスケーリング(Cluster Autoscaler)が可能ですが、インスタンスタイプが事前に決まっているいくつかに制限されます。トラフィック変動が大きくワークロードのリソース要求が多様な環境では Karpenter が新しい標準として位置づけられつつあります。

Karpenter は 上級 #5 で扱ったメトリクスベースのオートスケーリングと違う肌理です。Pending 状態の Pod を見て、その Pod のリソース要求に合うインスタンスタイプをリアルタイムで選んで新しいノードを立てます。 決まったインスタンスプールではなく AWS のすべての EC2 タイプの中からもっとも適合するものを選択します。

Karpenter NodePool — ノード自動プロビジョニングポリシー
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: node.kubernetes.io/instance-type
          operator: In
          values: ["t3.medium", "t3.large", "m5.large", "m5.xlarge"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: 100
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized

Karpenter 導入は #1 の出発点では負担なので、Managed Node Group で始めてトラフィックパターンが定着すれば Karpenter に転換する流れが自然です。シリーズでは #6 運用チェックリストで再び押さえます。

クラスタセットアップ後の最初の点検 #

クラスタが立った直後に 1 度ずつ回しておくと良い点検コマンドたちです。

バージョンとノードヘルス
kubectl version --short
kubectl get nodes -o wide
システム Pod
kubectl get pods -n kube-system
OIDC provider 確認 (IRSA が有効化されたか)
aws eks describe-cluster \
  --name myshop-prod \
  --region ap-northeast-2 \
  --query "cluster.identity.oidc.issuer" \
  --output text
EKS アドオン状態
aws eks list-addons --cluster-name myshop-prod --region ap-northeast-2
aws eks describe-addon --cluster-name myshop-prod \
  --addon-name vpc-cni --region ap-northeast-2

この 4 つのコマンドでクラスタ・ノード・システム Pod・OIDC・アドオンがすべて正常か確認されます。異常が見えれば次の記事に行く前に押さえておくのが安全です。

コストの最初の印象 #

EKS クラスタのコストは大きく 3 つです。

項目コスト (ap-northeast-2 基準)
EKS コントロールプレーン時間あたり $0.10 (≈ 月 $73)
EC2 ノード (t3.medium 3 台)月約 $80 (ON_DEMAND) / $25 (SPOT)
NAT Gateway月約 $35 + データ転送
EBS / Load Balancer / データ転送使用量に応じて

もっとも小さい prod クラスタの開始コストが月 $200~$300 程度です。dev 環境で SPOT インスタンス + Single NAT を使うとその半分以下に下がります。コストは #6 で再び扱いますが、クラスタを立てる時点から認識して始めるのが良いです。

締めくくり #

K8s 実戦シリーズの最初の記事を締めくくります。Terraform で VPC・EKS コントロールプレーン・ノードグループ・IRSA・標準アドオンまでを 1 つのコードベースで宣言する流れを追い、eksctl で早く始める道と Karpenter でノードオートスケーリングを広げる肌理まで押さえました。この時点でクラスタは空の状態です — ノードは立っていてシステム Pod は生きていますが、私たちが載せようとする myshop-api はまだ 1 行もマニフェストに入っていません。次の記事ではその空白を埋めます — Deployment / Service / Ingress / ConfigMap / Secret を 1 セットで整理し、Helm chart で束ねて環境別デプロイまで接続する流れです。

X