Рубрики
Uncategorized

Развертывание кластера Kubernetes (AWS EKS) и шлюза API, обеспеченного MTLS, с Terraform, Overse -DNS & Traefik — Часть 1

В этой серии статьи мы увидим способ развертывания кластера Kubernetes (AWS EKS) и API-шлюза, обеспеченного MTLS, с Terraform, Overse-DNS и Traefik. Помечено Kubernetes, Devops, Terraform, Traefik.

Однажды команда, которая хочет иметь выделенный кластер Kubernetes для своих услуг. Услуги, которые будут развернуты, должны быть выявлены через шлюз API и защищены через MTL.

Цель этой истории является основной, есть несколько способов ее достижения, и мы увидим один способ сделать это. Не идеальное, а решение, способ развернуть кластер, управляемый кластер, с доступными и защищенными контейнерами/услугами.

Вы готовы?

Как и другие реальные проекты, наш проект имеют ограничения.

Нам нужно развернуть кластер Kubernetes, это факт. Но где? Можем ли мы справиться с нашим правильным кластером Kubernetes? У нас есть специальная команда? Нет … поэтому нам нужно выбрать управляемый кластер Kubernetes. Можем ли мы выбрать нашего облачного провайдера? Нет … ограничение — установить его в AWS … Итак, давайте начнем с EKS, Kubernetes управлял кластером от AWS!:-)

Чтобы развернуть кластер AWS EKS, существует несколько решений, вы можете загрузить и использовать командную строку EKSCTL или использовать Terraform.

Наша команда всегда управляет своей инфаструктурой, управляемыми ресурсами с Terraform, инфраструктурой в качестве инструмента кода (IAC).

Существующий модуль Terraform существует, чтобы развернуть AWS EKS, но:

  • Это старый модуль, а не подъем и не поддерживается
  • это не совместимо с TF 0,12

Таким образом, мы определим наши надлежащие ресурсы.

Чтобы получить доступ к услугам, развернутым в контейнерах, существует несколько решений. Нам нужен обратный проток, развертываемый в качестве входа в Kubernetes, легко развертываем и совместимый с MTL, мы выберем Traefik.

Мы не хотим создавать новый DN вручную, поэтому мы будем использовать внешние DNS, связанные с нашими услугами, которые создадут записи AWS Route53.

Для MTLS мы создадим наш собственный PKI и генерируем наши собственные сертификаты CA Root, сервера и клиента.

А для тестов мы создадим коллекцию тестов и используем почтальон.

Мы знаем, что хотим развернуть с Terraform A AWS EKS Cluster. Так конкретно нам нужно развернуть много ресурсов для этого кластера EKS, много ресурсов … 😅

Здесь список ресурсов AWS для определения:

  • Эк
  • IAM Роли
  • IAM Роли Политики
  • IAM POLICIES привязанность
  • IAM OpenID Connect Provider
  • Асг
  • IG
  • Подготовительный
  • Подсети
  • Маршрут 53

Прежде всего, нам нужно инициализировать нашу кодовую организацию.

Предварительные результаты: установите Terraform CLI на вашем машине.

Создает папку Terraform:

$ cd my_git_repository
$ mkdir terraform
$ cd terraform/

Теперь мы, как обычно, создаем Backend.tf :

В этом файле мы определяем хранилище S3. Хорошей практикой является хранение вашего государства Terraform удаленно. Таким образом, мы будем хранить государство в ведре AWS S3:

# Backend configuration is loaded early so we can't use variables
terraform {
  required_version = ">= 0.12"

  backend "s3" {
    region  = "eu-central-1"
    bucket  = "my-tf-state-bucket"
    key     = "eks-cluster.tfstate"
    encrypt = true
  }
}

Далее нам нужно определить Provider.tf Файл, в котором мы определим поставщика AWS, который мы используем, и в конечном итоге роль IAM, которую мы должны взять на себя:

############### PROVIDERS DEFINITION ###############

provider "aws" {
  region = var.aws_region
}

Итак, теперь мы можем определить наш кластер EKS и группу узлов кластера EKS в eks.tf файл:

resource "aws_eks_cluster" "eks-scraly-cluster" {
  name     = local.cluster_name
  role_arn = aws_iam_role.eks-scraly-cluster-ServiceRole.arn

  vpc_config {
    security_group_ids = [ aws_security_group.eks-scraly-cluster-sg.id ]
    subnet_ids = concat(aws_subnet.eks-subnet-private[*].id, aws_subnet.eks-subnet-public[*].id)
    endpoint_private_access = true
    public_access_cidrs = var.eks_public_access_cidrs
  }

  # Ensure that IAM Role permissions are created before and deleted after EKS Cluster handling.
  # Otherwise, EKS will not be able to properly delete EKS managed EC2 infrastructure such as Security Groups.
  depends_on = [
    aws_iam_role_policy_attachment.eks-scraly-cluster-AmazonEKSClusterPolicy,
    aws_iam_role_policy_attachment.eks-scraly-cluster-AmazonEKSServicePolicy,
  ]

  version = var.eks_version

  tags = local.tags
}

resource "aws_eks_node_group" "eks-scraly-cluster-node-group" {
  cluster_name    = aws_eks_cluster.eks-scraly-cluster.name
  node_group_name = "eks-scraly-cluster-node-group"
  node_role_arn   = aws_iam_role.eks-scraly-cluster-worker-role.arn
  subnet_ids      = aws_subnet.eks-subnet-private[*].id

  scaling_config {
    desired_size = 1
    max_size     = 1
    min_size     = 1
  }

  # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling.
  # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces.
  depends_on = [
    aws_iam_role_policy_attachment.eks-scraly-cluster-AmazonEKSWorkerNodePolicy,
    aws_iam_role_policy_attachment.eks-scraly-cluster-AmazonEKS_CNI_Policy,
    aws_iam_role_policy_attachment.eks-scraly-cluster-AmazonEC2ContainerRegistryReadOnly,
  ]}

EKS нуждается в большом количестве ролей, политики и политики, давайте создадим их в iam.tf файл:

resource "aws_iam_role" "eks-scraly-cluster-ServiceRole" {
    name               = "${var.eks_resource_prefix}-ServiceRole"
    path               = "/"
    assume_role_policy = jsonencode({
        Version = "2012-10-17"
        Statement = [{
            Action = "sts:AssumeRole"
            Effect ="Allow"
            Principal = {
                Service = [
                    "eks.amazonaws.com",
                    "eks-fargate-pods.amazonaws.com"
                ]
            }
        }]        
    })
    tags = local.tags
}


resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-AmazonEKSClusterPolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.eks-scraly-cluster-ServiceRole.name
}

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-AmazonEKSServicePolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy"
  role       = aws_iam_role.eks-scraly-cluster-ServiceRole.name
}

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-NLBPolicy" {
  policy_arn  = aws_iam_policy.nlb_iam_policy.arn
  role        = aws_iam_role.eks-scraly-cluster-ServiceRole.name
}

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-ExternalDnsRoute53" {
  policy_arn  = aws_iam_policy.externalDNS_iam_policy.arn
  role        = aws_iam_role.eks-scraly-cluster-ServiceRole.name
}

resource "aws_iam_role" "eks-scraly-cluster-worker-role" {
  name = "${var.eks_resource_prefix}-worker-role"

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

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-AmazonEKSWorkerNodePolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.eks-scraly-cluster-worker-role.name
}

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-AmazonEKS_CNI_Policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.eks-scraly-cluster-worker-role.name
}

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-AmazonEC2ContainerRegistryReadOnly" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.eks-scraly-cluster-worker-role.name
}

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-CertManagerRoute53" {
  policy_arn = aws_iam_policy.certmanager_route53_iam_policy.arn
  role       = aws_iam_role.eks-scraly-cluster-worker-role.name
}

### External cli kubergrunt
data "external" "thumb" {
  program = [ "get_thumbprint.sh", var.aws_region ]
}

# Enabling IAM Roles for Service Accounts
resource "aws_iam_openid_connect_provider" "eks-scraly-cluster-oidc" {
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = [data.external.thumb.result.thumbprint]
  url             = aws_eks_cluster.eks-scraly-cluster.identity.0.oidc.0.issuer
}

data "aws_iam_policy_document" "eks-scraly-cluster-assume-role-policy" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    effect  = "Allow"

    condition {
      test     = "StringEquals"
      variable = "${replace(aws_iam_openid_connect_provider.eks-scraly-cluster-oidc.url, "https://", "")}:aud"
      values   = ["sts.amazonaws.com"]
    }

    principals {
      identifiers = ["${aws_iam_openid_connect_provider.eks-scraly-cluster-oidc.arn}"]
      type        = "Federated"
    }
  }
}

resource "aws_iam_role" "eks-scraly-cluster-externalDns-role" {
  description = "Role used by External-DNS to manage route 53 inside the cluster"
  assume_role_policy = data.aws_iam_policy_document.eks-scraly-cluster-assume-role-policy.json
  name               = "${var.eks_resource_prefix}-ServiceRole-ExternalDns"
  tags = local.tags
}

resource "aws_iam_role_policy_attachment" "eks-scraly-cluster-externalDNS-attachment" {  
  policy_arn  = aws_iam_policy.externalDNS_iam_policy.arn
  role        = aws_iam_role.eks-scraly-cluster-externalDns-role.name
}

resource "aws_iam_policy" "certmanager_route53_iam_policy" {
  name        = "${var.eks_resource_prefix}-CertManagerRoute53Policy"
  path        = "/"
  description = "Route53 policy IAM Policy for eks"

  policy = jsonencode(
    {
      Version: "2012-10-17"
      Statement: [
        {
            Effect: "Allow"
            Action: "route53:GetChange"
            Resource: "arn:aws:route53:::change/*"
        },
        {
            Effect: "Allow"
            Action: "route53:ChangeResourceRecordSets"
            Resource: "arn:aws:route53:::hostedzone/*"
        },
        {
            Effect: "Allow"
            Action: "route53:ListHostedZonesByName"
            Resource: "*"
        }
      ]
    }
  )
}

resource "aws_iam_policy" "externalDNS_iam_policy" {
  name        = "${var.eks_resource_prefix}-externalDNSPolicy"
  path        = "/"
  description = "ExternalDNS IAM Policy for eks"

  policy = jsonencode(
    {
      Version: "2012-10-17"
      Statement: [
        {
          Effect: "Allow"
          Action: [
            "route53:ChangeResourceRecordSets"
          ]
          Resource: [
            "arn:aws:route53:::hostedzone/*"
          ]
        },
        {
          Effect: "Allow",
          Action: [
            "route53:ListHostedZones",
            "route53:ListResourceRecordSets"
          ],
          Resource: [
            "*"
          ]
        }
      ]
   }
  )
}

resource "aws_iam_policy" "nlb_iam_policy" {
  name        = "${var.eks_resource_prefix}-NLBPolicy"
  path        = "/"
  description = "NLB IAM Policy for eks"

  policy = jsonencode(
    {
      Version: "2012-10-17"
      Statement: [
          {
              Action: [
                  "elasticloadbalancing:*",
                  "ec2:CreateSecurityGroup",
                  "ec2:Describe*"
              ],
              Resource: "*",
              Effect: "Allow"
          }
      ]
    }
  )
}

Нам нужно развернуть кластер EKS в сетевых ресурсах, поэтому давайте создадим их в Network.tf файл:

data "aws_vpc" "eks-vpc" {
  filter {
    name = "tag:Name"
    values = [ var.eks_vpc_name ]
  }
}

data "aws_nat_gateway" "nat-gateway" {
  count = length(var.nat_gateway_ids)
  id = var.nat_gateway_ids[count.index]
}

data "aws_route_table" "eks-route-table-public" {
  # There is a single route table for all public subnet
  filter {
    name = "tag:Name"
    values = [ var.route_table_name_public ]
  }
}

data "aws_route_table" "eks-route-table-private" {
  count = length(var.route_table_name_private)
  filter {
    name = "tag:Name"
    values = [ var.route_table_name_private[count.index] ]
  }
}

data "aws_internet_gateway" "internet-gateway" {  
  filter {
    name = "tag:Name"
    values = [ var.internet_gateway_name ]
  }
}


resource "aws_route_table_association" "rt-association-private" {
  count           = length(aws_subnet.eks-subnet-private)
  subnet_id       = aws_subnet.eks-subnet-private[count.index].id
  route_table_id  = data.aws_route_table.eks-route-table-private[count.index].id
}

resource "aws_route_table_association" "rt-association-public" {
  count           = length(aws_subnet.eks-subnet-public)
  subnet_id       = aws_subnet.eks-subnet-public[count.index].id
  route_table_id  = data.aws_route_table.eks-route-table-public.id
}

resource "aws_security_group" "eks-scraly-cluster-sg" {
  name        = "eks-scraly-cluster-sg"
  description = "Cluster communication with worker nodes"
  vpc_id      = data.aws_vpc.eks-vpc.id

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

  tags = local.tags  
}

################
# Public subnet
################
resource "aws_subnet" "eks-subnet-public" {  
  count = length(var.subnet_public_cidr)
  vpc_id                          = data.aws_vpc.eks-vpc.id
  cidr_block                      = var.subnet_public_cidr[count.index]
  availability_zone               = length(regexall("^[a-z]{2}-", element(local.azs, count.index))) > 0 ? element(local.azs, count.index) : null
  availability_zone_id            = length(regexall("^[a-z]{2}-", element(local.azs, count.index))) == 0 ? element(local.azs, count.index) : null
  map_public_ip_on_launch         = true

  tags = merge(
    {
      "Name" = format(
        "%s-public-%s",
        var.eks_subnet_prefix,
        element(local.azs, count.index),
      ),
      "kubernetes.io/cluster/${local.cluster_name}" = "shared"
    },
    local.tags,
    local.public_subnet_tags
  )
}

#################
# Private subnet
#################
resource "aws_subnet" "eks-subnet-private" {  
  count = length(var.subnet_public_cidr)
  vpc_id                          = data.aws_vpc.eks-vpc.id
  cidr_block                      = var.subnet_private_cidr[count.index]

  availability_zone               = length(regexall("^[a-z]{2}-", element(local.azs, count.index))) > 0 ? element(local.azs, count.index) : null
  availability_zone_id            = length(regexall("^[a-z]{2}-", element(local.azs, count.index))) == 0 ? element(local.azs, count.index) : null

  map_public_ip_on_launch         = false

  tags = merge(
    {
      "Name" = format(
        "%s-private-%s",
        var.eks_subnet_prefix,
        element(local.azs, count.index),
      ),
      "kubernetes.io/cluster/${local.cluster_name}" = "shared"
    },
    local.tags,
    local.private_subnet_tags
  )
}

Теперь мы создадим Местные жители.tf Файл, в котором мы создадим уловки для создания будущего файла KubeConfig. Да, файл, который нам нужен, чтобы получить доступ к нашему будущему кластеру Kubernetes:-).

# Get the Availability Zones to automatically set default subnets number (1 in each AZ)
data "aws_availability_zones" "az_list" {}

locals {
    azs = data.aws_availability_zones.az_list.names

    tags = {
        Application    = "${var.tag_application}"
        Contact        = "${var.tag_contact}"
        Tool         = « Terrafor »m
    }

    cluster_name = var.eks_cluster_name  

    public_subnet_tags = {
        Description = "Public subnet for ${var.tag_application} environment"
        Usage       = "Public"
        Type        = "Public"
        "kubernetes.io/role/elb" = "1"
    }

    private_subnet_tags = {
        Description = "Private subnet for ${var.tag_application} environment"
        Usage       = "Private"
        Type        = "Private"        
    }

    kubeconfig = <

Теперь нам нужно вывести некоторую полезную информацию (например, содержимое файла KubeConfig), поэтому давайте определим их в outputs.tf файл:

output "endpoint" {
    value = aws_eks_cluster.eks-scraly-cluster.endpoint
}

output "kubeconfig" {
  value = "${local.kubeconfig}"
}

output "arn_external_dns_role" {
  value = aws_iam_role.eks-scraly-cluster-externalDns-role.arn
}

Если мы хотим создать новый маршрут Route53, вы можете создать Route53.tf Файл, но лично я буду использовать существующий. Кластер будет доступен через *.scraly.com Маршрут 53.

Мы используем некоторые входные переменные в наших ресурсах, поэтому давайте cretae переменные.

variable "region" {
  default = "eu-central-1"
}

variable "vpc_cidr" {
  description = "CIDR of the VPC to create. Example : 10.1.0.0/22"
  default = "10.xx.x.x/16"
}

variable "subnet_public_cidr" {
  description = "For public ips"
  type        = list(string)
  default = ["10.xx.x.x/24","10.xx.x.x/24","10.xx.x.x/24"]
}

variable "subnet_private_cidr" {
  description = "For private ips"
  type        = list(string)
  default = ["10.xx.x.x/24","10.xx.x.x/24","10.xx.x.x/24"]
}

variable "eks_vpc_id" {
  default = "vpc-123456"
}

variable "eks_vpc_name" {
  default = "our_existing_vpc"
}

variable "eks_subnet_prefix" {
  default = "eks-scraly"
}

variable "nat_gateway_ids" {
  description = "The NAT gateway to be used by the EKS worker to reach the internet"
  default = [
    "nat-lalala",
    "nat-scraly"
    ]
}

variable "internet_gateway_name" {
  default = "ig_scraly
}

variable "route_table_name_public" {
  default = "scraly
}

variable "route_table_name_private" {
  default = [
    "scraly-private-eu-central-1a",
    "scraly-private-eu-central-1b",
    "scraly-private-eu-central-1c"
  ]
}

# -------------------------------------------
# EKS
# -------------------------------------------

variable "eks_cluster_name" {
  default = "eks-scraly-cluster"
}

variable "eks_version" {
  default = "1.15" # Kubernetes v1.15
}

variable "eks_resource_prefix" {
  default = "eks-scraly-cluster"
}

variable "eks_public_access_cidrs" {
  description = "List of CIDR blocks which can access the EKS public API server endpoint when enabled"
  default = [
    "xx.xx.xx.xx/32",
    "xx.xx.xx.xx/32"
    ]
}

# -------------------------------------------
# Tags
# -------------------------------------------

variable "tag_application" {
  default = "eks-scraly-cluster"
}

variable "tag_contact" {
  default = "scraly@mail.com"
}

Наши ресурсы были определены, так что теперь единственное, что нужно сделать, это развернуть нашу инфраю с этими следующими командными линиями:

$ cd "terraform/"

$ terraform init -reconfigure

# Apply
$ terraform apply -input=false -auto-approve

data.external.thumb: Refreshing state...
data.aws_caller_identity.current: Refreshing state...
data.aws_route_table.eks-route-table-public: Refreshing state...
data.aws_availability_zones.az_list: Refreshing state...
data.aws_internet_gateway.internet-gateway: Refreshing state...
data.aws_route_table.eks-route-table-private[1]: Refreshing state...
data.aws_route_table.eks-route-table-private[2]: Refreshing state...
data.aws_route_table.eks-route-table-private[0]: Refreshing state...
data.aws_route53_zone.scraly-hosted-zone: Refreshing state...
data.aws_nat_gateway.nat-gateway[0]: Refreshing state...
data.aws_nat_gateway.nat-gateway[2]: Refreshing state...
data.aws_nat_gateway.nat-gateway[1]: Refreshing state...
data.aws_vpc.eks-vpc: Refreshing state...
aws_iam_policy.certmanager_route53_iam_policy: Creating...
...
aws_eks_node_group.eks-scraly-cluster-node-group: Still creating... [5m40s elapsed]
aws_eks_node_group.eks-scraly-cluster-node-group: Creation complete after 5m50s [id=eks-scraly-cluster:eks-scraly-cluster-node-group]

Apply complete! Resources: 33 added, 0 changed, 0 destroyed.

Outputs:

arn_external_dns_role = arn:aws:iam:::role/eks-scraly-cluster-ServiceRole-ExternalDns
endpoint = https://1234567891234.ab1.eu-central-1.eks.amazonaws.com
kubeconfig =
 ...

Круто, у нас есть кластер EKS Kubernetes, развернутый в нашей учетной записи AWS!

Давайте подтвердим это в вашем интерфейсе консоли AWS (или с AWS CLI).

Pfiou … это было не проще, чем с EKSCTL, но с Terraform теперь мы можем развернуть множественные кластеры EKS с различными переменными, в разных средах, с разными версиями Kubernetes, в разных сетевых VPC/IG/подсетях…

Давайте перейдем к второй части этой статьи для конфигурации EKS, развертывание Traefik, внешнее DNS, HTTPBIN, обеспеченные через MTLS.

И если вы заинтересованы в Terraform, я создал шпаргалку:

https://github.com/scraly/terraform-cheat-sheet/blob/master/terraform-cheat-sheet.pdf

Оригинал: «https://dev.to/aurelievache/deploying-a-kubernetes-cluster-aws-eks-an-api-gateway-secured-by-mtls-with-terraform-external-dns-traefik-part-1-34nd»