基于 Kubernetes 的 MinIO 分布式集群 CI/CD 部署

CI/CD Deploy with MinIO distributed cluster on Kubernetes

欢迎阅读我们 MinIO 和 CI/CD 系列的第三篇也是最后一篇。到目前为止,我们已经讨论了CI/CD 概念的基础知识以及如何构建 MinIO 工件以及如何在开发中测试它们。在这篇博文中,我们将重点介绍持续交付和 MinIO。我们将向您展示如何使用基础设施即代码在生产环境中部署 MinIO 集群,以确保任何人都可以读取已安装的资源并对任何更改应用版本控制。

MinIO 非常通用,几乎可以在任何环境中安装。MinIO 符合开发人员在笔记本电脑上拥有与使用我们讨论的 CI/CD 概念和管道在生产中工作的相同环境的多种用例。我们之前向您展示了如何将 MinIO 作为Docker 容器甚至作为systemd 服务进行安装。今天,我们将向您展示如何使用操作符在生产 Kubernetes 集群中以分布式模式部署 MinIO。我们将首先使用 Terraform 部署基础设施,然后我们将部署所需的 MinIO 资源。

MinIO 网络

首先,我们将使用 Terraform 构建基础设施启动和运行所需的基本网络。我们将设置一个 VPC 网络,其中包含 3 种常用的基本网络类型。在该网络中,我们将启动一个 Kubernetes 集群,我们可以在其中部署 MinIO 工作负载。我们的 Terraform 模块的结构如下所示

modules
├── eks
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
└── vpc
    ├── main.tf
    ├── outputs.tf
    └── variables.tf

https://github.com/minio/blog-assets/tree/main/ci-cd-deploy/terraform/aws/modules

为了使 VPC 拥有不同的网络,每个子网都需要一个唯一的、不重叠的子网。这些子网被划分为 CIDR 块。对于少数几个子网,这很容易计算,但对于像我们这里这样拥有许多子网的情况,Terraform 提供了一个方便的函数 cidrsubnet(),可以根据我们提供的更大的子网(在本例中为 10.0.0.0/16)为我们拆分子网。

变量 "minio_aws_vpc_cidr_block" {
  描述 = "AWS VPC CIDR 块"
  类型        = 字符串
  默认值     = "10.0.0.0/16"
}

变量 "minio_aws_vpc_cidr_newbits" {
  描述 = "AWS VPC CIDR 新位"
  类型        = 数字
  默认值     = 4
}

vpc/variables.tf#L1-L11

在 Terraform 中定义 VPC 资源。创建的任何子网都将基于此 VPC。

资源 "aws_vpc" "minio_aws_vpc" {

  cidr_block           = var.minio_aws_vpc_cidr_block
  instance_tenancy     = "default"
  enable_dns_hostnames = true

}

vpc/main.tf#L1-L7

设置三个不同的网络:公共、私有和隔离。

具有互联网网关 (IGW) 的公共网络将具有入站和出站互联网访问权限,并具有公共 IP 和互联网网关。

变量 "minio_public_igw_cidr_blocks" {
  类型 = map(数字)
  描述 = "公共 IGW 子网的可用区 CIDR 映射"

  默认值 = {
    "us-east-1b" = 1
    "us-east-1d" = 2
    "us-east-1f" = 3
  }
}

vpc/variables.tf#L15-L24

aws_subnet 资源将循环 3 次,在公共 VPC 中创建 3 个子网。

resource "aws_subnet" "minio_aws_subnet_public_igw" {

  for_each = var.minio_public_igw_cidr_blocks

  vpc_id            = aws_vpc.minio_aws_vpc.id
  cidr_block        = cidrsubnet(aws_vpc.minio_aws_vpc.cidr_block, var.minio_aws_vpc_cidr_newbits, each.value)
  availability_zone = each.key

  map_public_ip_on_launch = true
}

resource "aws_route_table" "minio_aws_route_table_public_igw" {

  vpc_id = aws_vpc.minio_aws_vpc.id

}

resource "aws_route_table_association" "minio_aws_route_table_association_public_igw" {

  for_each       = aws_subnet.minio_aws_subnet_public_igw

  subnet_id      = each.value.id
  route_table_id = aws_route_table.minio_aws_route_table_public_igw.id
}

resource "aws_internet_gateway" "minio_aws_internet_gateway" {

  vpc_id = aws_vpc.minio_aws_vpc.id

}

resource "aws_route" "minio_aws_route_public_igw" {
  route_table_id         = aws_route_table.minio_aws_route_table_public_igw.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.minio_aws_internet_gateway.id
}

vpc/main.tf#L11-L46

具有 NAT 网关 (NGW) 的私有网络将具有出站网络访问权限,但没有入站网络访问权限,并具有私有 IP 地址和 NAT 网关。

变量 "minio_private_ngw_cidr_blocks" {
  类型 = map(数字)
  描述 = "私有 NGW 子网的可用区 CIDR 映射"

  默认值 = {
    "us-east-1b" = 4
    "us-east-1d" = 5
    "us-east-1f" = 6
  }
}

vpc/variables.tf#L26L-L35

aws_subnet 资源将循环 3 次,在私有 VPC 中创建 3 个子网。

resource "aws_subnet" "minio_aws_subnet_private_ngw" {

  for_each = var.minio_private_ngw_cidr_blocks

  vpc_id            = aws_vpc.minio_aws_vpc.id
  cidr_block        = cidrsubnet(aws_vpc.minio_aws_vpc.cidr_block, var.minio_aws_vpc_cidr_newbits, each.value)
  availability_zone = each.key
}

resource "aws_route_table" "minio_aws_route_table_private_ngw" {

  for_each = var.minio_private_ngw_cidr_blocks

  vpc_id = aws_vpc.minio_aws_vpc.id
}

resource "aws_route_table_association" "minio_aws_route_table_association_private_ngw" {

  for_each = var.minio_private_ngw_cidr_blocks

  subnet_id      = aws_subnet.minio_aws_subnet_private_ngw[each.key].id
  route_table_id = aws_route_table.minio_aws_route_table_private_ngw[each.key].id
}

resource "aws_eip" "minio_aws_eip_nat" {

  for_each = var.minio_private_ngw_cidr_blocks

  vpc = true
}

resource "aws_nat_gateway" "minio_aws_nat_gateway" {

  for_each = var.minio_private_ngw_cidr_blocks

  subnet_id     = aws_subnet.minio_aws_subnet_public_igw[each.key].id
  allocation_id = aws_eip.minio_aws_eip_nat[each.key].id

  depends_on    = [aws_internet_gateway.minio_aws_internet_gateway]
}

resource "aws_route" "minio_aws_route_private_ngw" {

  for_each = var.minio_private_ngw_cidr_blocks

  route_table_id         = aws_route_table.minio_aws_route_table_private_ngw[each.key].id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.minio_aws_nat_gateway[each.key].id
}

vpc/main.tf#L50-L98

最后,我们创建一个隔离的和与网络隔离的网络,既没有出站也没有入站互联网访问权限。此网络完全与网络隔离,只有私有 IP 地址。

变量 "minio_private_isolated_cidr_blocks" {
  类型 = map(数字)
  描述 = "私有隔离子网的可用区 CIDR 映射"

  默认值 = {
    "us-east-1b" = 7
    "us-east-1d" = 8
    "us-east-1f" = 9
  }
}

vpc/variables.tf#37-46行

aws_subnet 资源将循环三次,在隔离/气隙 VPC 中创建三个子网。

资源 "aws_subnet" "minio_aws_subnet_private_isolated" {

  for_each = var.minio_private_isolated_cidr_blocks

  vpc_id            = aws_vpc.minio_aws_vpc.id
  cidr_block        = cidrsubnet(aws_vpc.minio_aws_vpc.cidr_block, var.minio_aws_vpc_cidr_newbits, each.value)
  availability_zone = each.key
}

资源 "aws_route_table" "minio_aws_route_table_private_isolated" {

  vpc_id = aws_vpc.minio_aws_vpc.id

}

资源 "aws_route_table_association" "minio_aws_route_table_association_private_isolated" {

  for_each = aws_subnet.minio_aws_subnet_private_isolated

  subnet_id      = each.value.id
  route_table_id = aws_route_table.minio_aws_route_table_private_isolated.id
}

vpc/main.tf#L102-L123

MinIO Kubernetes 集群

创建一个 Kubernetes 集群,我们将在其上部署 MinIO 集群。minio_aws_eks_cluster_subnet_ids 将由我们创建的 VPC 提供。稍后,我们将展示如何在部署阶段将所有这些整合在一起。

variable "minio_aws_eks_cluster_subnet_ids" {
  description = "AWS EKS Cluster subnet IDs"
  type        = list(string)
}

variable "minio_aws_eks_cluster_name" {
  description = "AWS EKS Cluster name"
  type        = string
  default     = "minio_aws_eks_cluster"
}

variable "minio_aws_eks_cluster_endpoint_private_access" {
  description = "AWS EKS Cluster endpoint private access"
  type        = bool
  default     = true
}

variable "minio_aws_eks_cluster_endpoint_public_access" {
  description = "AWS EKS Cluster endpoint public access"
  type        = bool
  default     = true
}

variable "minio_aws_eks_cluster_public_access_cidrs" {
  description = "AWS EKS Cluster public access cidrs"
  type        = list(string)
  default     = ["0.0.0.0/0"]
}

eks/variables.tf#L1-L28

注意:在生产环境中,您可能不希望 Kubernetes API 端点公开访问,因为它可能成为安全问题,因为它将开放对集群的控制。

您还需要几个角色来确保 Kubernetes 集群可以通过我们创建的网络正确通信,这些角色在eks/main.tf#L1-L29中定义。Kubernetes 集群定义如下:

资源 "aws_eks_cluster" "minio_aws_eks_cluster" {
  name = var.minio_aws_eks_cluster_name
  role_arn = aws_iam_role.minio_aws_iam_role_eks_cluster.arn

  vpc_config {
    subnet_ids              = var.minio_aws_eks_cluster_subnet_ids
    endpoint_private_access = var.minio_aws_eks_cluster_endpoint_private_access
    endpoint_public_access  = var.minio_aws_eks_cluster_endpoint_public_access
    public_access_cidrs     = var.minio_aws_eks_cluster_public_access_cidrs
  }

  depends_on = [
    aws_iam_role.minio_aws_iam_role_eks_cluster,
  ]

}

eks/main.tf#L31-L46

集群接收来自 `kubectl` 等命令发出的 API 请求,但不仅仅如此 - 工作负载需要被调度到某个地方。这就是 Kubernetes 集群节点组所需的地方。下面,我们定义节点组名称、实例类型和所需的组大小。由于我们有 3 个可用区,我们将为每个可用区创建一个节点,共 3 个。

variable "minio_aws_eks_node_group_name" {
  description = "AWS EKS Node group name"
  type        = string
  default     = "minio_aws_eks_node_group"
}

variable "minio_aws_eks_node_group_instance_types" {
  description = "AWS EKS Node group instance types"
  type        = list(string)
  default     = ["t3.large"]
}

variable "minio_aws_eks_node_group_desired_size" {
  description = "AWS EKS Node group desired size"
  type        = number
  default     = 3
}

variable "minio_aws_eks_node_group_max_size" {
  description = "AWS EKS Node group max size"
  type        = number
  default     = 5
}

variable "minio_aws_eks_node_group_min_size" {
  description = "AWS EKS Node group min size"
  type        = number
  default     = 1
}

eks/variables.tf#L30-L58

您需要几个角色来确保 Kubernetes 节点组能够正常通信,这些角色在 eks/main.tf#L48-L81 中定义。Kubernetes 节点组(工作节点)的定义如下:

资源 "aws_eks_node_group" "minio_aws_eks_node_group" {
  cluster_name    = aws_eks_cluster.minio_aws_eks_cluster.name
  node_group_name = var.minio_aws_eks_node_group_name
  node_role_arn   = aws_iam_role.minio_aws_iam_role_eks_worker.arn
  subnet_ids      = var.minio_aws_eks_cluster_subnet_ids
  instance_types  = var.minio_aws_eks_node_group_instance_types

  scaling_config {
    desired_size = var.minio_aws_eks_node_group_desired_size
    max_size     = var.minio_aws_eks_node_group_max_size
    min_size     = var.minio_aws_eks_node_group_min_size
  }

  depends_on = [
    aws_iam_role.minio_aws_iam_role_eks_worker,
  ]

}

eks/main.tf#L83-L100

此配置将在我们配置的任何 3 个 VPC 网络中启动一个包含工作节点的控制平面。集群启动后,我们将在后面展示 `kubectl get no` 的输出。

MinIO 部署

到目前为止,我们已经以代码形式拥有了所有必要的基础设施。接下来,我们将部署这些资源并在我们将在其上部署 MinIO 的集群上创建集群。

使用以下命令安装 Terraform

brew install terraform

使用以下命令安装 aws CLI

brew install awscli

创建一个具有以下策略的 AWS IAM 用户。创建用户后,请注意 `AWS_ACCESS_KEY_ID` 和 `AWS_SECRET_ACCESS_KEY`。

设置 AWS 的环境变量,因为 `terraform` 和 `awscli` 将使用它们。

$ export AWS_ACCESS_KEY_ID=<access_key>
$ export AWS_SECRET_ACCESS_KEY=<secret_key>

在与 `modules` 相同的目录中创建一个名为 `hello_world` 的文件夹,使用以下结构

.

├── hello_world

│   ├── main.tf

│   ├── outputs.tf

│   ├── terraform.tfvars

│   └── variables.tf

├── modules

│   ├── eks

│   └── vpc


https://github.com/minio/blog-assets/tree/main/ci-cd-deploy/terraform/aws/hello_world

创建一个名为 `terraform.tfvars` 的文件并设置以下变量

hello_minio_aws_region  = "us-east-1"

创建一个名为 `main.tf` 的文件并初始化 terraform AWS 提供程序和 S3 后端。请注意,S3 存储桶需要事先存在。我们使用 S3 后端来存储状态,以便在开发人员和 CI/CD 流程之间共享,而无需处理在整个组织中保持本地状态同步的问题。

terraform {
  required_version = ">= 1.0"

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

  backend "s3" {
    bucket = "aj-terraform-bucket"
    key    = "tf/aj/mo"
    region = "us-east-1"
  }

}

provider "aws" {
  region  = var.hello_minio_aws_region
}

hello_world/main.tf#L1-L21

不支持将后端存储桶和密钥设置为变量,因此这些值需要硬编码。

main.tf调用VPC模块,并将其命名为hello_minio_aws_vpc

module "hello_minio_aws_vpc" {
  source = "../modules/vpc"

  minio_aws_vpc_cidr_block   = var.hello_minio_aws_vpc_cidr_block
  minio_aws_vpc_cidr_newbits = var.hello_minio_aws_vpc_cidr_newbits

  minio_public_igw_cidr_blocks       = var.hello_minio_public_igw_cidr_blocks
  minio_private_ngw_cidr_blocks      = var.hello_minio_private_ngw_cidr_blocks
  minio_private_isolated_cidr_blocks = var.hello_minio_private_isolated_cidr_blocks

}

hello_world/main.tf#23-33 行

这些是 VPC 模块所需的变量。

hello_minio_aws_vpc_cidr_block = "10.0.0.0/16"

hello_minio_aws_vpc_cidr_newbits = 4

hello_minio_public_igw_cidr_blocks = {
  "us-east-1b" = 1
  "us-east-1d" = 2
  "us-east-1f" = 3
}

hello_minio_private_ngw_cidr_blocks = {
  "us-east-1b" = 4
  "us-east-1d" = 5
  "us-east-1f" = 6
}

hello_minio_private_isolated_cidr_blocks = {
  "us-east-1b" = 7
  "us-east-1d" = 8
  "us-east-1f" = 9
}

hello_world/terraform.tfvars#L3-L22

一旦 VPC 创建完成,下一步就是创建 Kubernetes 集群。我们从 VPC 创建中使用的唯一值是 minio_aws_eks_cluster_subnet_ids。我们将使用 VPC 创建的私有子网。

module "hello_minio_aws_eks_cluster" {
  source = "../modules/eks"

  minio_aws_eks_cluster_name                    = var.hello_minio_aws_eks_cluster_name
  minio_aws_eks_cluster_endpoint_private_access = var.hello_minio_aws_eks_cluster_endpoint_private_access
  minio_aws_eks_cluster_endpoint_public_access  = var.hello_minio_aws_eks_cluster_endpoint_public_access
  minio_aws_eks_cluster_public_access_cidrs     = var.hello_minio_aws_eks_cluster_public_access_cidrs
  minio_aws_eks_cluster_subnet_ids              = values(module.hello_minio_aws_vpc.minio_aws_subnet_private_ngw_map)
  minio_aws_eks_node_group_name                 = var.hello_minio_aws_eks_node_group_name
  minio_aws_eks_node_group_instance_types       = var.hello_minio_aws_eks_node_group_instance_types
  minio_aws_eks_node_group_desired_size         = var.hello_minio_aws_eks_node_group_desired_size
  minio_aws_eks_node_group_max_size             = var.hello_minio_aws_eks_node_group_max_size
  minio_aws_eks_node_group_min_size             = var.hello_minio_aws_eks_node_group_min_size

}

hello_world/main.tf#L37-L51

这些是 EKS 模块所需的变量。

hello_minio_aws_eks_cluster_name = "hello_minio_aws_eks_cluster"
hello_minio_aws_eks_cluster_endpoint_private_access = true
hello_minio_aws_eks_cluster_endpoint_public_access = true
hello_minio_aws_eks_cluster_public_access_cidrs = ["0.0.0.0/0"]
hello_minio_aws_eks_node_group_name = "hello_minio_aws_eks_node_group"
hello_minio_aws_eks_node_group_instance_types = ["t3.large"]
hello_minio_aws_eks_node_group_desired_size = 3
hello_minio_aws_eks_node_group_max_size = 5
hello_minio_aws_eks_node_group_min_size = 1

hello_world/terraform.tfvars#L24-L32

最后,我们将应用配置。在仍然位于 `hello_world` 目录中时,运行以下 `terraform` 命令。这将花费大约 15-20 分钟才能使整个基础设施启动并运行。接近结束时,您应该会看到如下所示的输出

$ terraform init


…截断…


$ terraform apply


…截断…


hello_minio_aws_eks_cluster_name = "hello_minio_aws_eks_cluster"
hello_minio_aws_eks_cluster_region = "us-east-1"

…截断…

完成: 成功

使用 aws eks 命令更新您的 --kubeconfig 默认配置以使用我们刚刚创建的集群。--region--name 可从之前的输出中获取。

$ aws eks --region us-east-1 update-kubeconfig \
    --name hello_minio_aws_eks_cluster

检查以验证您是否可以获取节点列表

$ kubectl get no
名称                           状态   角色    年龄    版本
ip-10-0-105-186.ec2.internal   就绪    <无>   3d8h   v1.23.9-eks-ba74326
ip-10-0-75-92.ec2.internal     就绪    <无>   3d8h   v1.23.9-eks-ba74326
ip-10-0-94-57.ec2.internal     就绪    <无>   3d8h   v1.23.9-eks-ba74326

接下来,安装 EBS 驱动程序,以便 gp2 PVC 可以挂载。我们使用 gp2,因为这是 AWS 支持的默认存储类。

使用与 awscli 相同的凭据设置 AWS 密钥的凭据。

kubectl create secret generic aws-secret \
    --namespace kube-system \
    --from-literal "key_id=${AWS_ACCESS_KEY_ID}" \
    --from-literal "access_key=${AWS_SECRET_ACCESS_KEY}"

应用 EBS 驱动程序资源

$ kubectl apply -k "github.com/kubernetes-sigs/aws-ebs-csi-driver/deploy/kubernetes/overlays/stable/?ref=release-1.12"

您的 Kubernetes 集群现在应该已准备就绪。

现在我们准备部署 MinIO。首先,克隆 MinIO 存储库

$ git clone https://github.com/minio/operator.git

由于这是 AWS,我们需要将 storageClassName 更新为 gp2。打开以下文件,并将所有 storageClassName: standard 的引用更新为 storageClassName: gp2。每个 MinIO 租户都有自己的 tenant.yaml 文件,其中包含 storageClassName 配置。根据您使用的租户,确保相应地更新 storageClassName。

$ vim ./operator/examples/kustomization/base/tenant.yaml

将资源应用于 Kubernetes 以安装 MinIO

$ kubectl apply -k operator/resources

$ kubectl apply -k operator/examples/kustomization/tenant-lite

等待至少 5 分钟以使资源启动,然后验证 MinIO 是否已启动并正在运行。

$ kubectl -n tenant-lite get po -o wide
NAME                                           READY   STATUS    RESTARTS      AGE   IP            NODE                           NOMINATED NODE   READINESS GATES
storage-lite-log-0                             1/1     Running   0             17m   10.0.94.169   ip-10-0-94-57.ec2.internal     <none>           <none>
storage-lite-log-search-api-66f7db97f5-j268m   1/1     Running   3 (17m ago)   17m   10.0.93.40    ip-10-0-94-57.ec2.internal     <none>           <none>
storage-lite-pool-0-0                          1/1     Running   0             17m   10.0.88.36    ip-10-0-94-57.ec2.internal     <none>           <none>
storage-lite-pool-0-1                          1/1     Running   0             17m   10.0.104.48   ip-10-0-105-186.ec2.internal   <none>           <none>
storage-lite-pool-0-2                          1/1     Running   0             17m   10.0.71.81    ip-10-0-75-92.ec2.internal     <none>           <none>
storage-lite-pool-0-3                          1/1     Running   0             17m   10.0.94.183   ip-10-0-94-57.ec2.internal     <none>           <none>
storage-lite-prometheus-0                      2/2     Running   0             15m   10.0.85.181   ip-10-0-94-57.ec2.internal     <none>           <none>

如果您注意到以上输出,每个 storage-lite-pool- 都位于不同的工作节点上。其中两个共享同一个节点,因为我们有 3 个节点,但这没关系,因为我们只有 3 个可用区 (AZ)。基本上,3 个 AZ 中有 3 个节点,4 个 MinIO Pod,每个 Pod 有 2 个 PVC,这反映在下面的状态“8 在线”中。

$ kubectl -n tenant-lite logs storage-lite-pool-0-0

…TRUNCATED…

状态:        8 在线, 0 离线。
API: https://minio.tenant-lite.svc.cluster.local
控制台: https://10.0.88.36:9443 https://127.0.0.1:9443

文档: https://min-io.cn/docs/minio/linux/index.html

您需要 MinIO 控制台的 TCP 端口;在本例中为 9443

$ kubectl -n tenant-lite get svc | grep -i console
storage-lite-console   ClusterIP   172.20.26.209   <none>        9443/TCP   6s

有了这些信息,我们可以设置 Kubernetes 端口转发。我们为主机选择了端口 39443,但这可以是任何端口,只需确保在通过 Web 浏览器访问控制台时使用相同的端口。

$ kubectl -n tenant-lite port-forward svc/storage-lite-console 39443:9443

转发自 127.0.0.1:39443 -> 9443

转发自 [::1]:39443 -> 9443

使用以下凭据通过 Web 浏览器访问 MinIO 运算符控制台

URL:https://localhost:39443

用户:minio

密码:minio123

您现在已完全设置了分布式 MinIO 集群的生产环境。以下是如何使用 Jenkins 自动化它

以下是文本格式的执行 shell 命令

export PATH=$PATH:/usr/local/bin

cd ci-cd-deploy/terraform/aws/hello_world/
terraform init
terraform plan
terraform apply -auto-approve

最终想法

在 CI/CD 系列的过去几个博文中,我们向您展示了 MinIO 的灵活性和灵活性。您可以使用 Packer 将其构建到任何您想要的内容中,并将其部署在所需的虚拟机或 Kubernetes 集群中。这使您的开发人员能够在他们的开发环境中拥有尽可能接近生产基础设施的环境,同时利用强大的安全功能,例如服务器端对象加密管理 IAM 策略来限制对存储桶的访问。

在生产环境中,您可能希望将 IAM 用户限制为特定的策略,但这实际上取决于您的用例。出于演示目的,我们使用广泛的策略使事情保持简单,但在生产环境中,您需要将其缩小到特定的资源和用户组。在以后的博文中,我们将展示一些有关如何为不同的 AZ 和区域设计基础设施的最佳实践。

您是否想尝试使用 Jenkins 自动化 kubectl 部分以及手动应用?请告诉我们您使用我们的教程构建了哪种管道来计划、部署、扩展和保护跨多云的 MinIO,并在我们的Slack上与我们联系并分享您的管道!