基于 Terraform 构建企业内部 kubernetes 集群

Posted on Apr 19, 2026 · 13 min read

1. 为什么要构建企业内部 Kubernetes 集群?

随着企业业务规模的不断扩张,微服务架构逐渐成为主流,应用的数量、版本迭代的频率以及部署的复杂度都在急剧上升。在这一背景下,建立一套稳定、高效、可扩展的企业内部 Kubernetes 集群,成为支撑企业数字化转型的核心基础设施之一。

1.1 构建统一的资源调度平台

企业内部往往同时运行着多条业务线、多个团队的应用服务。Kubernetes 提供了统一的容器编排与资源调度能力,能够将计算、存储、网络资源进行统一抽象和调度,避免各团队烟囱式的资源使用模式,显著提升整体资源利用率。

1.2 支撑 CI/CD 流水线落地

Kubernetes 是现代 CI/CD 流水线的天然载体。通过与 GitLab CI、Jenkins、ArgoCD、Tekton 等工具集成,企业可以实现:

  • 持续集成(CI):代码提交后自动触发构建、测试,产出镜像并推送到私有镜像仓库。
  • 持续交付(CD):基于 GitOps 理念,通过声明式配置驱动应用在集群中的自动发布与回滚。
  • 多环境隔离:通过 Namespace、NetworkPolicy、RBAC 等机制,在同一集群内划分开发、测试、预发、生产等多套环境,降低环境管理成本。

1.3 提升业务稳定性与弹性

Kubernetes 内置的自愈能力(Pod 自动重启、节点故障迁移)、水平自动伸缩(HPA/VPA/KEDA)以及滚动更新策略,能够大幅提升业务服务的可用性和弹性,满足企业对 SLA 的严格要求。

1.4 降低运维复杂度

统一的控制平面使运维团队可以通过标准化的 kubectl 命令或声明式 YAML 配置管理所有工作负载,结合 Helm Chart 进行应用打包与版本管理,有效降低了跨团队、跨项目的运维协作成本。

2. 构建内部 K8s 集群核心流程

2.1 整体流程概览

基础资源准备 → 操作系统初始化 → 集群安装部署 → 网络插件配置 → 存储插件配置 → 集群验证 → 监控告警部署

2.2 基础资源准备

构建 K8s 集群首先需要准备充足的计算、存储和网络资源。

计算资源方案对比:

方案优势劣势适用场景
裸金属服务器性能最优、成本可控运维复杂、交付周期长大规模、对性能极致要求
云服务器(VM)弹性伸缩、快速交付存在虚拟化开销中小规模、快速上线
混合云灵活兼顾网络打通复杂已有 IDC 并需弹性扩展

主流云厂商选型:

  • AWS:EC2 + VPC + EBS,生态最完善,全球覆盖最广
  • 阿里云:ECS + VPC + 云盘,国内合规友好
  • Azure:VM + VNet,微软生态集成强
  • GCP:GCE + VPC,网络性能优秀

2.3 基础资源准备方案

企业通常有两种主流方式来完成基础资源的创建和初始化:

方案一:脚本方式(Shell/Ansible)

通过 Shell 脚本或 Ansible Playbook 调用云厂商 CLI/SDK 创建资源,再通过 SSH 批量初始化操作系统配置。这种方式上手快,但缺乏状态管理,在资源规模扩大后维护成本急剧上升。

方案二:Terraform(推荐)

通过声明式 HCL 语言描述所需的基础设施资源,Terraform 负责将期望状态与实际状态对齐。具备完整的状态管理、依赖解析和变更预览能力,是生产环境的首选方案。

3. 为什么采用 Terraform?

3.1 Terraform 核心优势

① 声明式基础设施即代码(IaC)

Terraform 采用 HashiCorp Configuration Language(HCL),用声明式语法描述基础设施的目标状态,而非一步步的操作指令。工程师只需关心"我要什么资源",而非"如何创建资源",大幅降低了认知负担。

② 统一的多云支持

Terraform 通过 Provider 机制支持 600+ 云服务商和 SaaS 平台(AWS、阿里云、Azure、GCP、Kubernetes、Helm 等),企业无需为每个云厂商学习不同的 SDK 或 CLI,实现真正的多云统一管理。

③ 状态管理与变更预览

Terraform 通过 .tfstate 文件追踪已部署资源的实际状态。每次变更前执行 terraform plan 可以清晰预览变更内容,避免误操作。结合远端 State Backend(S3 + DynamoDB),团队可以安全地协作管理同一套基础设施。

④ 模块化与复用

Terraform Module 机制支持将常用的资源组合(如 VPC 模块、EKS 节点组模块)封装为可复用的模块,在不同项目、不同环境间复用,保持一致性。

⑤ 幂等性与自愈能力

多次执行 terraform apply 不会产生副作用,Terraform 会自动识别已存在的资源并跳过创建,确保基础设施始终与代码描述一致。

3.2 与其他方案的对比

维度TerraformShell 脚本AnsibleCloudFormation(AWS 专属)
多云支持✅ 原生支持⚠️ 需自行适配⚠️ 部分支持❌ 仅 AWS
状态管理✅ 内置❌ 无❌ 无✅ 内置
变更预览✅ plan❌ 无⚠️ 有限✅ ChangeSet
学习曲线中等中等
模块复用✅ 完善❌ 弱✅ Role⚠️ 一般
社区生态✅ 非常活跃✅ 活跃⚠️ 局限 AWS
幂等性✅ 强❌ 弱✅ 较强✅ 较强

3.3 Terraform 的局限性

  • 状态文件风险.tfstate 包含敏感信息,需妥善保管,并启用加密和访问控制。
  • 学习成本:HCL 语法和 Provider 文档有一定学习曲线。
  • 漂移检测:若有人手动修改了云资源,Terraform 需要通过 terraform refreshterraform plan 才能感知漂移。

尽管存在上述局限,Terraform 在企业级基础设施管理场景中的综合能力仍然是目前最优的选择。

4. 基于 Terraform 构建 Kubernetes 集群方案

4.1 主流方案概览

基于 Terraform 构建 K8s 集群主要有以下几种思路:

① 托管 K8s 服务(EKS / AKS / GKE)

直接通过 Terraform 的云厂商 Provider 创建托管集群,控制平面由云厂商负责运维。优点是运维简单、升级便捷;缺点是定制化空间有限,且存在厂商锁定风险。适合对运维能力要求不高的中小团队。

② Kubespray(Ansible + Terraform)

Kubespray 是 Kubernetes 官方维护的基于 Ansible 的部署工具。结合 Terraform 完成基础设施准备后,通过 Ansible Playbook 部署集群。支持高度定制化,但部署流程较复杂,调试成本高,升级路径也较为繁琐。

③ RKE2(Rancher Kubernetes Engine 2)

RKE2 是 Rancher 推出的生产级 Kubernetes 发行版,专注于安全性与合规性(通过 CIS Benchmark),内置 containerd、CoreDNS、Ingress 等核心组件。结合 Terraform 可以通过 remote-execcloud-init 方式在节点上自动化完成 RKE2 的安装和集群初始化,整体流程简洁、可靠。

4.2 方案对比

维度托管 K8s(EKS 等)KubesprayRKE2
控制平面自管
部署复杂度
定制化能力
安全合规(CIS)需额外配置需额外配置✅ 内置
升级便捷性
厂商锁定
社区活跃度
生产可靠性

4.3 生产级别推荐:RKE2

对于需要完整自主掌控控制平面、追求安全合规、同时希望保持部署流程相对简洁的企业,RKE2 是生产环境的首选方案,原因如下:

  • 开箱即用的安全加固:默认通过 CIS Kubernetes Benchmark Level 1/2 审计,满足企业合规要求。
  • 内置关键组件:containerd、CNI 占位符、CoreDNS、Ingress-nginx、metrics-server 等均已内置,无需额外安装。
  • 简洁的安装方式:通过单一安装脚本 + 配置文件即可完成 Server/Agent 节点的初始化,易于与 Terraform remote-execcloud-init 集成。
  • 活跃的商业支持:背靠 SUSE/Rancher,有完善的商业支持和长期维护承诺。

5. Kubernetes 集群网络选型

CNI(Container Network Interface)插件是 K8s 集群网络的核心组件,负责 Pod 间通信、网络策略执行等关键功能。目前企业级最主流的两款 CNI 插件是 CalicoCilium

5.1 Calico 与 Cilium 的对比分析

Calico

Calico 是目前生产环境中使用最广泛的 CNI 插件之一,由 Tigera 公司主导开发。

优势:

  • 成熟稳定,社区案例丰富,生产验证充分
  • 支持 BGP 原生路由模式,网络性能优秀,无需 Overlay 封装
  • NetworkPolicy 实现完善,支持 Kubernetes 标准及扩展的 GlobalNetworkPolicy
  • 与 AWS VPC、阿里云 VPC 等主流云环境集成良好
  • 运维调试工具链完善(calicoctl、Felix 日志等)
  • 资源消耗相对较低,对节点内核版本要求宽松

劣势:

  • 高级可观测性能力(流量追踪、L7 策略)依赖商业版 Calico Enterprise
  • eBPF 支持为可选模式,默认仍基于 iptables,在超大规模集群中 iptables 规则数量庞大

Cilium

Cilium 是基于 eBPF 技术构建的新一代 CNI 插件,由 Isovalent 主导开发。

优势:

  • 原生基于 eBPF,绕过 iptables,在高并发场景下网络性能更优
  • 内置 Hubble 可观测性平台,提供丰富的 L3/L4/L7 流量可视化
  • 原生支持 Service Mesh 能力(Cilium Service Mesh),可替代部分 Sidecar 代理
  • 对 NetworkPolicy 的支持更加丰富(DNS-based、FQDN policy 等)

劣势:

  • 对 Linux 内核版本要求较高(推荐 5.10+),在一些老旧 OS 环境中兼容性存在风险
  • 生产落地案例相对 Calico 少,运维人员学习曲线较陡
  • 架构相对复杂,故障排查难度更高

5.2 选用 Calico 的原因

在本次架构中,选择 Calico 作为集群 CNI 插件,主要基于以下考量:

  1. 生产成熟度高:Calico 在业界有大量超过 1000 节点规模集群的生产验证案例,稳定性有充分保证。
  2. AWS VPC 集成友好:Calico 的 VPC Native Routing 模式可以直接利用 AWS VPC 路由表进行 Pod 路由,无需 Overlay 封装,网络性能接近原生,且便于与 AWS 安全组联动。
  3. 运维友好:团队现有运维人员对 Calico 的 iptables 模式更为熟悉,故障定位路径清晰,降低了生产运维风险。
  4. 50 节点规模适配:在 50 节点规模下,iptables 规则数量尚在可控范围内,Calico 的性能表现完全满足需求,无需引入 Cilium eBPF 的额外复杂度。
  5. 开源免费:Calico 开源版本功能已能满足企业 NetworkPolicy 的核心需求,无需额外采购商业授权。

6. 基于 Terraform + RKE2 + Calico 构建 K8s 集群

6.1 架构设计概览

关键技术选型:

  • 云平台:AWS
  • 节点规模:50 个工作节点 + 3 个控制平面节点(高可用)
  • 网络方案:AWS VPC(自定义)
  • 集群发行版:RKE2 v1.30+
  • CNI 插件:Calico(替换 RKE2 内置 Canal)
  • 操作系统:Ubuntu 24.04 LTS

集群节点规划:

节点类型数量实例规格用途
Control Plane3m5.xlarge(4C 16G)API Server、etcd、Scheduler、Controller Manager
Worker Node50m5.2xlarge(8C 32G)业务工作负载
Bastion Host1t3.micro运维跳板机

6.2 目录结构

terraform-rke2-aws/
├── main.tf                  # 根模块入口
├── variables.tf             # 变量定义
├── outputs.tf               # 输出定义
├── terraform.tfvars         # 变量赋值
├── versions.tf              # Provider 版本锁定
├── modules/
│   ├── vpc/                 # VPC 及网络资源
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── security_groups/     # 安全组规则
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── iam/                 # IAM 角色与实例 Profile
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── control_plane/       # 控制平面节点
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── worker_nodes/        # 工作节点(ASG)
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── scripts/
│   ├── rke2-server-init.sh  # 首个 Server 节点初始化脚本
│   ├── rke2-server-join.sh  # 追加 Server 节点加入脚本
│   └── rke2-agent.sh        # Worker 节点加入脚本
└── calico/
    └── custom-resources.yaml # Calico 安装配置

6.3 Step 1:配置 Terraform Provider 与 Backend

versions.tf

terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
  }

  backend "s3" {
    bucket         = "your-company-terraform-state"
    key            = "rke2-cluster/terraform.tfstate"
    region         = "ap-southeast-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = "rke2-k8s-cluster"
      Environment = var.environment
      ManagedBy   = "Terraform"
    }
  }
}

6.4 Step 2:构建 VPC 网络基础设施

modules/vpc/main.tf

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr          # 10.0.0.0/16
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.cluster_name}-vpc"
  }
}

# 互联网网关
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.cluster_name}-igw"
  }
}

# 公有子网(Bastion / NLB)
resource "aws_subnet" "public" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "${var.cluster_name}-public-${count.index + 1}"
    "kubernetes.io/role/elb" = "1"
  }
}

# 私有子网(Control Plane / Worker Nodes)
resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.cluster_name}-private-${count.index + 1}"
    "kubernetes.io/role/internal-elb" = "1"
  }
}

# NAT 网关(每个 AZ 一个,生产高可用要求)
resource "aws_eip" "nat" {
  count  = length(var.availability_zones)
  domain = "vpc"
}

resource "aws_nat_gateway" "main" {
  count         = length(var.availability_zones)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "${var.cluster_name}-nat-${count.index + 1}"
  }
}

# 私有子网路由表
resource "aws_route_table" "private" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = {
    Name = "${var.cluster_name}-private-rt-${count.index + 1}"
  }
}

resource "aws_route_table_association" "private" {
  count          = length(var.availability_zones)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

6.5 Step 3:配置安全组

modules/security_groups/main.tf

# Control Plane 安全组
resource "aws_security_group" "control_plane" {
  name        = "${var.cluster_name}-control-plane-sg"
  description = "RKE2 Control Plane Security Group"
  vpc_id      = var.vpc_id

  # RKE2 API Server
  ingress {
    from_port   = 6443
    to_port     = 6443
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
    description = "Kubernetes API Server"
  }

  # RKE2 Supervisor API
  ingress {
    from_port   = 9345
    to_port     = 9345
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
    description = "RKE2 Supervisor API"
  }

  # etcd
  ingress {
    from_port       = 2379
    to_port         = 2380
    protocol        = "tcp"
    self            = true
    description     = "etcd client and peer"
  }

  # Calico BGP
  ingress {
    from_port   = 179
    to_port     = 179
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
    description = "Calico BGP"
  }

  # Calico VXLAN
  ingress {
    from_port   = 4789
    to_port     = 4789
    protocol    = "udp"
    cidr_blocks = [var.vpc_cidr]
    description = "Calico VXLAN"
  }

  # Kubelet
  ingress {
    from_port   = 10250
    to_port     = 10250
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
    description = "Kubelet"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.cluster_name}-control-plane-sg"
  }
}

# Worker Node 安全组
resource "aws_security_group" "worker" {
  name        = "${var.cluster_name}-worker-sg"
  description = "RKE2 Worker Node Security Group"
  vpc_id      = var.vpc_id

  # NodePort 范围
  ingress {
    from_port   = 30000
    to_port     = 32767
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
    description = "NodePort Services"
  }

  # Kubelet
  ingress {
    from_port   = 10250
    to_port     = 10250
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
    description = "Kubelet"
  }

  # Calico BGP & VXLAN
  ingress {
    from_port   = 179
    to_port     = 179
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  ingress {
    from_port   = 4789
    to_port     = 4789
    protocol    = "udp"
    cidr_blocks = [var.vpc_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.cluster_name}-worker-sg"
  }
}

6.6 Step 4:IAM 角色配置

modules/iam/main.tf

# Control Plane IAM Role
resource "aws_iam_role" "control_plane" {
  name = "${var.cluster_name}-control-plane-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy" "control_plane" {
  name = "${var.cluster_name}-control-plane-policy"
  role = aws_iam_role.control_plane.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:DescribeInstances",
          "ec2:DescribeRegions",
          "ec2:DescribeRouteTables",
          "ec2:DescribeSecurityGroups",
          "ec2:DescribeSubnets",
          "ec2:DescribeVolumes",
          "ec2:DescribeVpcs",
          "ec2:CreateSecurityGroup",
          "ec2:CreateTags",
          "ec2:CreateVolume",
          "ec2:ModifyInstanceAttribute",
          "ec2:AttachVolume",
          "ec2:DetachVolume",
          "elasticloadbalancing:*",
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:GetRepositoryPolicy",
          "ecr:DescribeRepositories",
          "ecr:ListImages",
          "ecr:BatchGetImage",
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_instance_profile" "control_plane" {
  name = "${var.cluster_name}-control-plane-profile"
  role = aws_iam_role.control_plane.name
}

# Worker Node IAM Role(权限收窄)
resource "aws_iam_role" "worker" {
  name = "${var.cluster_name}-worker-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy" "worker" {
  name = "${var.cluster_name}-worker-policy"
  role = aws_iam_role.worker.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:DescribeInstances",
          "ec2:DescribeRegions",
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:GetRepositoryPolicy",
          "ecr:DescribeRepositories",
          "ecr:ListImages",
          "ecr:BatchGetImage"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_instance_profile" "worker" {
  name = "${var.cluster_name}-worker-profile"
  role = aws_iam_role.worker.name
}

6.7 Step 5:部署控制平面节点

modules/control_plane/main.tf

# 生成集群 Token
resource "random_password" "rke2_token" {
  length  = 64
  special = false
}

# 将 Token 存入 SSM Parameter Store
resource "aws_ssm_parameter" "rke2_token" {
  name  = "/${var.cluster_name}/rke2-token"
  type  = "SecureString"
  value = random_password.rke2_token.result
}

# 首个 Server 节点(Init 节点)
resource "aws_instance" "control_plane_init" {
  ami                    = var.ami_id
  instance_type          = "m5.xlarge"
  subnet_id              = var.private_subnet_ids[0]
  vpc_security_group_ids = [var.control_plane_sg_id]
  iam_instance_profile   = var.control_plane_instance_profile
  key_name               = var.key_pair_name

  root_block_device {
    volume_size = 100
    volume_type = "gp3"
    encrypted   = true
  }

  user_data = base64encode(templatefile("${path.module}/../../scripts/rke2-server-init.sh", {
    rke2_version     = var.rke2_version
    cluster_name     = var.cluster_name
    rke2_token       = random_password.rke2_token.result
    aws_region       = var.aws_region
    pod_cidr         = var.pod_cidr
    service_cidr     = var.service_cidr
  }))

  tags = {
    Name = "${var.cluster_name}-control-plane-init"
    "kubernetes.io/cluster/${var.cluster_name}" = "owned"
    "rke2-node-role" = "server"
  }
}

# NLB(API Server 高可用入口)
resource "aws_lb" "api_server" {
  name               = "${var.cluster_name}-api-nlb"
  internal           = true
  load_balancer_type = "network"
  subnets            = var.private_subnet_ids
}

resource "aws_lb_target_group" "api_server" {
  name     = "${var.cluster_name}-api-tg"
  port     = 6443
  protocol = "TCP"
  vpc_id   = var.vpc_id

  health_check {
    protocol            = "HTTPS"
    path                = "/healthz"
    port                = "6443"
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }
}

resource "aws_lb_listener" "api_server" {
  load_balancer_arn = aws_lb.api_server.arn
  port              = 6443
  protocol          = "TCP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api_server.arn
  }
}

# 追加 2 个 Server 节点
resource "aws_instance" "control_plane_join" {
  count                  = 2
  ami                    = var.ami_id
  instance_type          = "m5.xlarge"
  subnet_id              = var.private_subnet_ids[count.index + 1]
  vpc_security_group_ids = [var.control_plane_sg_id]
  iam_instance_profile   = var.control_plane_instance_profile
  key_name               = var.key_pair_name

  root_block_device {
    volume_size = 100
    volume_type = "gp3"
    encrypted   = true
  }

  user_data = base64encode(templatefile("${path.module}/../../scripts/rke2-server-join.sh", {
    rke2_version  = var.rke2_version
    rke2_token    = random_password.rke2_token.result
    server_url    = "https://${aws_lb.api_server.dns_name}:9345"
  }))

  depends_on = [aws_instance.control_plane_init]

  tags = {
    Name = "${var.cluster_name}-control-plane-${count.index + 2}"
    "kubernetes.io/cluster/${var.cluster_name}" = "owned"
    "rke2-node-role" = "server"
  }
}

scripts/rke2-server-init.sh

#!/bin/bash
set -eux

# 系统初始化
swapoff -a
sed -i '/swap/d' /etc/fstab

# 内核参数
cat >> /etc/sysctl.d/99-k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sysctl --system

# 安装 RKE2
curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION="${rke2_version}" sh -

# 配置 RKE2 Server
mkdir -p /etc/rancher/rke2

cat > /etc/rancher/rke2/config.yaml <<EOF
token: "${rke2_token}"
tls-san:
  - "${server_lb_dns}"
cluster-cidr: "${pod_cidr}"
service-cidr: "${service_cidr}"
cni: none
disable:
  - rke2-canal
cloud-provider-name: aws
node-label:
  - "node.kubernetes.io/role=control-plane"
EOF

# 启动 RKE2 Server
systemctl enable rke2-server.service
systemctl start rke2-server.service

# 等待 kubeconfig 生成
until [ -f /etc/rancher/rke2/rke2.yaml ]; do sleep 5; done

# 配置 kubectl
mkdir -p /root/.kube
cp /etc/rancher/rke2/rke2.yaml /root/.kube/config
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
ln -sf /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl

echo "RKE2 Server Init Completed"

6.8 Step 6:部署工作节点(Auto Scaling Group)

modules/worker_nodes/main.tf

# 启动模板
resource "aws_launch_template" "worker" {
  name_prefix   = "${var.cluster_name}-worker-"
  image_id      = var.ami_id
  instance_type = "m5.2xlarge"
  key_name      = var.key_pair_name

  vpc_security_group_ids = [var.worker_sg_id]

  iam_instance_profile {
    name = var.worker_instance_profile
  }

  block_device_mappings {
    device_name = "/dev/sda1"
    ebs {
      volume_size           = 200
      volume_type           = "gp3"
      encrypted             = true
      delete_on_termination = true
    }
  }

  user_data = base64encode(templatefile("${path.module}/../../scripts/rke2-agent.sh", {
    rke2_version = var.rke2_version
    rke2_token   = var.rke2_token
    server_url   = var.server_url
  }))

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name = "${var.cluster_name}-worker"
      "kubernetes.io/cluster/${var.cluster_name}" = "owned"
      "rke2-node-role" = "agent"
    }
  }

  lifecycle {
    create_before_destroy = true
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "worker" {
  name                = "${var.cluster_name}-worker-asg"
  desired_capacity    = 50
  min_size            = 10
  max_size            = 60
  vpc_zone_identifier = var.private_subnet_ids

  launch_template {
    id      = aws_launch_template.worker.id
    version = "$Latest"
  }

  health_check_type         = "EC2"
  health_check_grace_period = 300

  tag {
    key                 = "Name"
    value               = "${var.cluster_name}-worker"
    propagate_at_launch = true
  }

  tag {
    key                 = "kubernetes.io/cluster/${var.cluster_name}"
    value               = "owned"
    propagate_at_launch = true
  }
}

scripts/rke2-agent.sh

#!/bin/bash
set -eux

swapoff -a
sed -i '/swap/d' /etc/fstab

cat >> /etc/sysctl.d/99-k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables  = 1
net.ipv4.ip_forward                 = 1
EOF
sysctl --system

curl -sfL https://get.rke2.io | INSTALL_RKE2_TYPE="agent" INSTALL_RKE2_VERSION="${rke2_version}" sh -

mkdir -p /etc/rancher/rke2

cat > /etc/rancher/rke2/config.yaml <<EOF
server: "${server_url}"
token: "${rke2_token}"
cloud-provider-name: aws
node-label:
  - "node.kubernetes.io/role=worker"
EOF

systemctl enable rke2-agent.service
systemctl start rke2-agent.service

echo "RKE2 Agent Join Completed"

6.9 Step 7:安装 Calico CNI

calico/custom-resources.yaml

apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    ipPools:
    - blockSize: 26
      cidr: 192.168.0.0/16
      encapsulation: VXLAN
      natOutgoing: Enabled
      nodeSelector: all()
  nodeAddressAutodetectionV4:
    interface: eth0
---
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
  name: default
spec: {}

安装步骤(在 Control Plane 节点执行):

# 1. 安装 Tigera Operator
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml

# 2. 应用自定义配置
kubectl create -f calico/custom-resources.yaml

# 3. 等待 Calico 组件就绪
kubectl wait --for=condition=Ready pods -n calico-system --all --timeout=300s

# 4. 验证节点状态
kubectl get nodes

6.10 Step 8:执行 Terraform 部署

# 初始化
terraform init

# 预览变更
terraform plan -var-file="terraform.tfvars"

# 执行部署
terraform apply -var-file="terraform.tfvars" -auto-approve

# 查看输出
terraform output

6.11 集群验证

# 检查节点状态
kubectl get nodes -o wide

# 检查核心组件
kubectl get pods -n kube-system
kubectl get pods -n calico-system

# 检查集群信息
kubectl cluster-info

# 部署测试 Deployment
kubectl create deployment nginx-test --image=nginx --replicas=3
kubectl get pods -o wide
kubectl delete deployment nginx-test

7. 如何做 K8s 集群的监控

7.1 监控体系概览

完整的 K8s 集群监控体系应覆盖以下四个层次:

基础设施层(节点 CPU/内存/磁盘/网络)
    ↓
K8s 组件层(API Server、etcd、Scheduler、Controller Manager)
    ↓
工作负载层(Pod、Deployment、DaemonSet 指标)
    ↓
应用业务层(服务 SLI/SLO、错误率、延迟)

7.2 核心监控组件栈

推荐方案:kube-prometheus-stack(Prometheus + Grafana + Alertmanager)

kube-prometheus-stack 是目前最主流的 K8s 监控解决方案,通过 Helm 一键部署,集成了:

  • Prometheus Operator:以 CRD 方式管理 Prometheus 实例、告警规则、抓取配置
  • Prometheus:时序指标采集与存储
  • Grafana:可视化大盘
  • Alertmanager:告警路由与通知(支持钉钉/Slack/PagerDuty 等)
  • kube-state-metrics:K8s 对象状态指标(Pod 状态、Deployment 副本数等)
  • node-exporter:宿主机层面指标(CPU、内存、磁盘、网络)
# 通过 Helm 安装 kube-prometheus-stack
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set prometheus.prometheusSpec.retention=15d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=gp3 \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=100Gi \
  --set grafana.adminPassword="your-secure-password" \
  --set alertmanager.enabled=true

7.3 关键监控指标

节点层指标

指标名说明告警阈值建议
node_cpu_seconds_total节点 CPU 使用率> 85% 持续 5min
node_memory_MemAvailable_bytes节点可用内存< 10% 持续 5min
node_filesystem_avail_bytes磁盘剩余空间< 15%
node_disk_io_time_seconds_total磁盘 IO 压力ioutil > 90%

K8s 组件层指标

指标名说明告警阈值建议
apiserver_request_duration_secondsAPI Server 延迟P99 > 1s
apiserver_request_totalAPI Server 请求错误率5xx 错误率 > 1%
etcd_disk_wal_fsync_duration_secondsetcd WAL 写入延迟P99 > 10ms
etcd_server_has_leaderetcd Leader 是否存在= 0 立即告警

工作负载层指标

指标名说明告警阈值建议
kube_pod_status_phasePod 非 Running 状态Pending > 10min
kube_deployment_status_replicas_unavailableDeployment 不可用副本数> 0 持续 5min
container_memory_working_set_bytes容器内存使用量接近 limit 的 90%
container_oom_events_total容器 OOM 次数> 0 立即告警

7.4 日志监控方案

指标监控解决"是什么问题",日志则帮助解决"为什么出问题"。推荐采用 Loki + Promtail 方案与 Grafana 集成,实现指标与日志的统一视图:

helm upgrade --install loki grafana/loki-stack \
  --namespace monitoring \
  --set promtail.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.size=50Gi

7.5 链路追踪(可选)

对于微服务架构,建议引入分布式追踪能力,推荐 OpenTelemetry + Jaeger / Tempo 方案:

  • OpenTelemetry Collector:统一采集应用 Trace、Metric、Log
  • Jaeger / Grafana Tempo:Trace 存储与查询
  • Grafana:通过 Exemplar 实现 Metric → Trace 的联动跳转

7.6 告警通知配置示例

Alertmanager 钉钉通知配置(alertmanager-config.yaml)

apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
  name: cluster-alerts
  namespace: monitoring
spec:
  route:
    receiver: dingtalk-webhook
    groupBy: ['alertname', 'namespace']
    groupWait: 30s
    groupInterval: 5m
    repeatInterval: 4h
  receivers:
    - name: dingtalk-webhook
      webhookConfigs:
        - url: 'http://dingtalk-webhook-adapter:8060/dingtalk/webhook/send'
          sendResolved: true

7.7 推荐 Grafana Dashboard

Dashboard ID用途
15757Kubernetes / Views / Global(集群全局视图)
13332Kubernetes Cluster Monitoring
1860Node Exporter Full(节点详细监控)
7249Kubernetes Cluster(工作负载视图)
3070Kubernetes Persistent Volumes

通过 Grafana 的 Import 功能直接导入以上 Dashboard,可以快速获得开箱即用的监控大盘,无需从零配置。

我之前所在的企业 PaaS 平台为了满足客户多样性需求自研了一套多云管理平台,提供了源码托管、CI/CD、资源管理等一整套体系,管控端部署在中心集群,一个客户一套边缘集群。