Terraform: створення EKS, частина 4 – установка контролерів

Автор |  14/09/2023
 

Остання, четверта частина, в якій ми встановимо решту контроллерів і додамо пару корисних дрібниць.

Попередні частини:

Планування

Що нам залишилось зробити:

Структура файлів зараз виглядає так:

$ tree .
.
├── backend.tf
├── configs
│   └── karpenter-provisioner.yaml.tmpl
├── eks.tf
├── karpenter.tf
├── main.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
├── variables.tf
└── vpc.tf

Поїхали.

EBS CSI driver

Цей аддон можна встановити з Amazon EKS Blueprints Addons, котрий далі будемо використовувати для ExternalDNS, але раз уж ставимо аддони через cluster_addons в модулі EKS, то давайте і цей зробимо таким же чином.

Для aws-ebs-csi-driver ServiceAccount  нам знадобиться окрема IAM Role – створимо її за допомогою IRSA Terraform Module.

Приклад для EBS CSI є тут – ebs_csi_irsa_role.

Створюємо файл iam.tf – тут будемо тримати окремі ресурси, пов’язані з AWS IAM:

module "ebs_csi_irsa_role" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

  role_name             = "${local.env_name}-ebs-csi-role"
  attach_ebs_csi_policy = true

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

Виконуємо terraform init та деплоїмо:

Далі, знаходимо актуальну версію аддону для EKS 1.27:

$ aws eks describe-addon-versions --addon-name aws-ebs-csi-driver --kubernetes-version 1.27 --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" --output text
v1.22.0-eksbuild.2
True
v1.21.0-eksbuild.1
False
...

В модулі EKS аддони встановлюються за допомогою ресурсу aws_eks_addon, див. main.tf.

Для версій можемо передати most_recent або addon_version. Краще, звісно, задавати версію явно.

Створимо окрему змінну для версій аддонів EKS – додаємо до variables.tf:

...

variable "eks_addons_version" {
  description = "EKS Add-on versions, will be used in the EKS module for the cluster_addons"
  type        = map(string)
}

Додаємо значення до tfvars:

...

eks_addons_version = {
  coredns            = "v1.10.1-eksbuild.3"
  kube_proxy         = "v1.27.4-eksbuild.2"
  vpc_cni            = "v1.14.1-eksbuild.1"
  aws_ebs_csi_driver = "v1.22.0-eksbuild.2"
}

Додаємо aws-ebs-csi-driver до cluster_addons в eks.tf:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.0"
    ...
    vpc-cni = {
      addon_version = var.eks_addons_version.vpc_cni
    }
    aws-ebs-csi-driver = {
      addon_version            = var.eks_addons_version.aws_ebs_csi_driver
      service_account_role_arn = module.ebs_csi_irsa_role.iam_role_arn
    }
  }
...

Деплоїмо та перевіряємо поди:

$ kk -n kube-system get pod | grep csi
ebs-csi-controller-787874f4dd-nlfn2   5/6     Running   0          19s
ebs-csi-controller-787874f4dd-xqbcs   5/6     Running   0          19s
ebs-csi-node-fvscs                    3/3     Running   0          20s
ebs-csi-node-jz2ln                    3/3     Running   0          20s

Тестування EBS CSI

Створюємо маніфест пода з PVC:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: demo-pv
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 1Gi
  storageClassName: standard
  hostPath:
    path: /tmp/demo-pv
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-dynamic
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: gp2
---
apiVersion: v1
kind: Pod
metadata:
  name: pvc-pod
spec:
  containers:
    - name: pvc-pod-container
      image: nginx:latest
      volumeMounts:
        - mountPath: /data
          name: data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pvc-dynamic

Деплоїмо, та перевіряємо статус:

$ kk get pod
NAME      READY   STATUS    RESTARTS   AGE
pvc-pod   1/1     Running   0          106s

$ kk get pvc
NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-dynamic   Bound    pvc-a83b3021-03d8-458f-ad84-98805ec4963d   1Gi        RWO            gp2            119s

$ kk get pv pvc-a83b3021-03d8-458f-ad84-98805ec4963d
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   REASON   AGE
pvc-a83b3021-03d8-458f-ad84-98805ec4963d   1Gi        RWO            Delete           Bound    default/pvc-dynamic   gp2                     116s

З цим все готово.

Terraform та ExternalDNS

Для ExternalDNS спробуємо Amazon EKS Blueprints Addons. На відміну від того, як ми робили EBS CSI, тут нам не потрібно буде окремо створювати IAM Role, бо модуль створить її сам.

Приклади є у файлі tests/complete/main.tf.

Правда, в документації чомусь вказана передача параметрів для чарту external_dns_helm_config (UPD – поки писав цей пост, вже видалили сторінку взагалі), хоча на ділі це призводить до помилки “An argument named “external_dns_helm_config” is not expected here“.

Щоб знайти, як жеж нам передати параметри – йдемо на сторінку модулю в eks-blueprints-addons, і дивимось які інпути є для external_dns:

Далі перевіряємо файл main.tf модулю, де бачимо змінну var.external_dns, в якій можна передати всі параметри.

Дефолтні версії чартів задаються у тому ж файлі, але вони місцями застарілі, теж задамо свої.

Знаходимо останню версію для ЕxternalDNS:

$ helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
"external-dns" has been added to your repositories

$ helm search repo external-dns/external-dns --versions
NAME                            CHART VERSION   APP VERSION     DESCRIPTION                                       
external-dns/external-dns       1.13.1          0.13.6          ExternalDNS synchronizes exposed Kubernetes Ser...
...

Додаємо змінну для версій чартів:

variable "helm_release_versions" {
  description = "Helm Chart versions to be deployed into the EKS cluster"
  type        = map(string)
}

У файл terraform.tfvars додаємо значення:

...

helm_release_versions = {
  karpenter    = "v0.30.0"
  external_dns = "1.13.1"
}

Зона у нас на кожне оточення своя, одна, тож просто задамо її у varables.tf як string:

...

variable "external_dns_zone" {
  type        = string
  description = "AWS Route53 zone to be used by ExternalDNS in domainFilters and its IAM Policy"
}

І значення в tfvars:

...

external_dns_zone = "dev.example.co"

Створюємо файл controllers.tf, і описуємо деплой ExternalDNS.

З data "aws_route53_zone" отримуємо інформацію про нашу Route53 Hosted Zone, і її ARN передаємо в параметр external_dns_route53_zone_arns.

Так як ми використовуємо VPC Endpoint для STS, то в аннотації ServiceAccount передаємо eks.amazonaws.com/sts-regional-endpoints="true" – аналогічно тому, як робили для Karpenter.

У external_dns.values передаємо бажані параметри – policy, в domainFilters наш домен, та задаємо tolerations, щоб под запускався на наших дефолтних нодах:

data "aws_route53_zone" "example" {
  name = var.external_dns_zone
}

module "eks_blueprints_addons" {
  source  = "aws-ia/eks-blueprints-addons/aws"
  version = "~> 1.7.2"

  cluster_name      = module.eks.cluster_name
  cluster_endpoint  = module.eks.cluster_endpoint
  cluster_version   = module.eks.cluster_version
  oidc_provider_arn = module.eks.oidc_provider_arn

  enable_external_dns = true
  external_dns = {
    namespace     = "kube-system"
    chart_version = var.helm_release_versions.external_dns
    values = [
      <<-EOT
        policy: upsert-only
        domainFilters: [ ${data.aws_route53_zone.example.name} ]
        tolerations:
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoExecute
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoSchedule          
      EOT
    ]
    set = [
      {
        name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/sts-regional-endpoints"
        value = "true"
        type  = "string"
      }
    ]
  }
  external_dns_route53_zone_arns = [
    data.aws_route53_zone.example.arn
  ]
}

Деплоїмо, та перевіряємо под:

$ kk get pod -l app.kubernetes.io/instance=external-dns
NAME                            READY   STATUS    RESTARTS   AGE
external-dns-597988f847-rxqds   1/1     Running   0          66s

Тестування ExternalDNS

Створюємо Kubernetes Service з типом LoadBalancer:

apiVersion: v1
kind: Service
metadata:
  name: mywebapp
  labels:
    service: nginx
  annotations:
    external-dns.alpha.kubernetes.io/hostname: test-dns.dev.example.co
spec:
  selector:
    service: nginx
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 80

Деплоїмо, ті перевіряємо логи ExternalDNS:

...
time="2023-09-13T12:01:39Z" level=info msg="Desired change: CREATE cname-test-dns.dev.example.co TXT [Id: /hostedzone/Z09***BL9]"
time="2023-09-13T12:01:39Z" level=info msg="Desired change: CREATE test-dns.dev.example.co A [Id: /hostedzone/Z09***BL9]"
time="2023-09-13T12:01:39Z" level=info msg="Desired change: CREATE test-dns.dev.example.co TXT [Id: /hostedzone/Z09***BL9]"
time="2023-09-13T12:01:39Z" level=info msg="3 record(s) in zone dev.example.co. [Id: /hostedzone/Z09***BL9] were successfully updated

Готово.

Terraform та AWS Load Balancer Controller

Робимо аналогічно з ExternalDNS – будемо встановлювати з модуля Amazon EKS Blueprints Addons.

Спочатку нам треба протегати публічні і приватні сабнети – див. Subnet Auto Discovery.

Також перевірте, щоб на них був тег kubernetes.io/cluster/${cluster-name} = owned (має бути, якщо деплоїли з Terraform модулем EKS, як це робили в першій частині).

Додаємо теги через public_subnet_tags та private_subnet_tags:

...
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.1.1"
  ...
  public_subnet_tags = {
    "kubernetes.io/role/elb" = 1
  }

  private_subnet_tags = {
    "karpenter.sh/discovery"          = local.eks_cluster_name
    "kubernetes.io/role/internal-elb" = 1
  }
  ...
}
...

Створення модулю описано тут>>>.

Знаходимо актуальну версію чарту:

$ helm repo add aws-eks-charts https://aws.github.io/eks-charts
"aws-eks-charts" has been added to your repositories

$ helm search repo aws-eks-chart/aws-load-balancer-controller --versions
NAME                                            CHART VERSION   APP VERSION     DESCRIPTION                                       
aws-eks-chart/aws-load-balancer-controller      1.6.1           v2.6.1          AWS Load Balancer Controller Helm chart for Kub...
aws-eks-chart/aws-load-balancer-controller      1.6.0           v2.6.0          AWS Load Balancer Controller Helm chart for Kub...
...

Додаємо версію чарту до змінної helm_release_versions:

...

helm_release_versions = {
  karpenter                             = "0.16.3"
  external_dns                          = "1.13.1"
  aws_load_balancer_controller          = "1.6.1"
}

До файлу controllers.tf додаємо ресурс aws_load_balancer_controller:

...

  enable_aws_load_balancer_controller = true
  aws_load_balancer_controller = {
    namespace     = "kube-system"
    chart_version = var.helm_release_versions.aws_load_balancer_controller
    values = [
      <<-EOT
        tolerations:
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoExecute
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoSchedule          
      EOT
    ]
    set = [
      {
        name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/sts-regional-endpoints"
        value = "true"
        type  = "string"
      }
    ]
  }
}

Деплоїмо, перевіряємо под:

$ kk get pod | grep aws
aws-load-balancer-controller-7bb9d7d8-4m4kz   1/1     Running   0          99s
aws-load-balancer-controller-7bb9d7d8-8l58n   1/1     Running   0          99s

Тестування AWS Load Balancer Controller

Додаємо Pod, Service та Ingress:

---
apiVersion: v1
kind: Pod
metadata:
  name: hello-pod
  labels:
    app: hello
spec:
  containers:
    - name: hello-container
      image: nginxdemos/hello
      ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hello-service
spec:
  type: NodePort
  selector:
    app: hello
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-ingress
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  ingressClassName: alb
  rules:
    - host: test-dns.dev.example.co
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello-service
                port:
                  number: 80

Деплоїмо, перевіряємо Ingress – чи додався до нього Load Balancer:

$ kk get ingress
NAME            CLASS   HOSTS                     ADDRESS                                                 PORTS   AGE
hello-ingress   alb     test-dns.dev.example.co   k8s-kubesyst-helloing-***.us-east-1.elb.amazonaws.com   80      45s

З цим теж готово.

Terraform та SecretStore CSI Driver і ASCP

SecretStore CSI Driver та AWS Secrets and Configuration Provider (ASCP) нам потрібні, щоб в Kubernetes підключати значення з AWS Secrets Manager та Parameter Store, див. AWS: Kubernetes – інтеграція AWS Secrets Manager та Parameter Store.

Тут теж зробимо з Amazon EKS Blueprints Addons – див. secrets_store_csi_driver та secrets_store_csi_driver_provider_aws.

Ніяк налаштувань тут не треба – тільки додати Tolerations.

Знаходимо версії:

$ helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
"secrets-store-csi-driver" has been added to your repositories

$ helm repo add secrets-store-csi-driver-provider-aws https://aws.github.io/secrets-store-csi-driver-provider-aws
"secrets-store-csi-driver-provider-aws" has been added to your repositories

$ helm search repo secrets-store-csi-driver/secrets-store-csi-driver
NAME                                                    CHART VERSION   APP VERSION     DESCRIPTION                                       
secrets-store-csi-driver/secrets-store-csi-driver       1.3.4           1.3.4           A Helm chart to install the SecretsStore CSI Dr...

$ helm search repo secrets-store-csi-driver-provider-aws/secrets-store-csi-driver-provider-aws
NAME                                                    CHART VERSION   APP VERSION     DESCRIPTION                                       
secrets-store-csi-driver-provider-aws/secrets-s...      0.3.4                           A Helm chart for the AWS Secrets Manager and Co...

Додаємо нові значення до змінної helm_release_versions у terraform.tfvars:

...

helm_release_versions = {
  karpenter                             = "0.16.3"
  external_dns                          = "1.13.1"
  aws_load_balancer_controller          = "1.6.1"
  secrets_store_csi_driver              = "1.3.4"
  secrets_store_csi_driver_provider_aws = "0.3.4"
}

Додаємо самі модулі до controllers.tf:

...

  enable_secrets_store_csi_driver = true
  secrets_store_csi_driver = {
    namespace     = "kube-system"
    chart_version = var.helm_release_versions.secrets_store_csi_driver
    values = [
      <<-EOT
        syncSecret:
          enabled: true
        tolerations:
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoExecute
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoSchedule          
      EOT
    ]
  }

  enable_secrets_store_csi_driver_provider_aws = true
  secrets_store_csi_driver_provider_aws = {
    namespace     = "kube-system"
    chart_version = var.helm_release_versions.secrets_store_csi_driver_provider_aws
    values = [
      <<-EOT
        tolerations:
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoExecute
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoSchedule          
      EOT
    ]
  }
}

IAM Policy

Для IAM Roles, які ми потім будемо додавати до сервісів, якім потрібен доступ до AWS SecretsManager/ParameterStore треба буде підключати політику, яка дозволяє доступ до відповідних AWS API викликів.

Створимо її разом з EKS в нашому файлі iam.tf:

resource "aws_iam_policy" "sm_param_access" {
  name        = "sm-and-param-store-ro-access-policy"
  description = "Additional policy to access Serets Manager and Parameter Store"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "secretsmanager:DescribeSecret",
          "secretsmanager:GetSecretValue",
          "ssm:DescribeParameters",
          "ssm:GetParameter",
          "ssm:GetParameters",
          "ssm:GetParametersByPath"
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

Деплоїмо, та перевіряємо поди:

$ kk -n kube-system get pod | grep secret
secrets-store-csi-driver-bl6s5                3/3     Running   0          86s
secrets-store-csi-driver-krzvp                3/3     Running   0          86s
secrets-store-csi-driver-provider-aws-7v7jq   1/1     Running   0          102s
secrets-store-csi-driver-provider-aws-nz5sz   1/1     Running   0          102s
secrets-store-csi-driver-provider-aws-rpr54   1/1     Running   0          102s
secrets-store-csi-driver-provider-aws-vhkwl   1/1     Running   0          102s
secrets-store-csi-driver-r4rrr                3/3     Running   0          86s
secrets-store-csi-driver-r9428                3/3     Running   0          86s

Тестування SecretStore CSI Driver

Для доступа поду до SecretStore потрібно додати ServiceAccount з IAM-ролью.

Політику вже створили – додамо IAM Role.

Описуємо її Trust policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Principal": {
        "Federated": "arn:aws:iam::492***148:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/268***3CE"
      },
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/268***3CE:aud": "sts.amazonaws.com",
          "oidc.eks.us-east-1.amazonaws.com/id/268***3CE:sub": "system:serviceaccount:default:ascp-test-serviceaccount"
        }
      }
    }
  ]
}

Створюємо роль:

$ aws iam create-role --role-name ascp-iam-role --assume-role-policy-document file://ascp-trust.json

Підключаємо політику:

$ aws iam attach-role-policy --role-name ascp-iam-role --policy-arn=arn:aws:iam::492***148:policy/sm-and-param-store-ro-access-policy

Створюємо запис в AWS Parameter Store:

$ aws ssm put-parameter --name eks-test-param --value 'paramLine' --type "String"
{
    "Version": 1,
    "Tier": "Standard"
}

Описуємо маніфест SecretProviderClass:

---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: eks-test-secret-provider-class
spec:         
  provider: aws
  parameters:
    objects: |
        - objectName: "eks-test-param"
          objectType: "ssmparameter"
---       
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ascp-test-serviceaccount
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::492***148:role/ascp-iam-role
    eks.amazonaws.com/sts-regional-endpoints: "true"
---
apiVersion: v1
kind: Pod
metadata:
  name: ascp-test-pod
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      command: ['sleep', '36000']
      volumeMounts:
      - name: ascp-test-secret-volume
        mountPath: /mnt/ascp-secret
        readOnly: true
  restartPolicy: Never
  serviceAccountName: ascp-test-serviceaccount
  volumes:
  - name: ascp-test-secret-volume
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: eks-test-secret-provider-class

Деплоїмо (у default неймспейс, бо в Trust policy маємо перевірку subject на "system:serviceaccount:default:ascp-test-serviceaccount"), і перевіряємо файл в поді:

$ kk exec -ti pod/ascp-test-pod -- cat /mnt/ascp-secret/eks-test-param
paramLine

Terraform та Metrics Server

Тут теж зробимо з Amazon EKS Blueprints Addons – див. metrics_server.

Теж ніяк додаткових налаштувань не треба – просто включити, і перевірити. Навіть версію можна не задавати, тільки tolerations.

Додаємо до controllers.tf:

...

  enable_metrics_server = true
  metrics_server = {
    values = [
      <<-EOT
        tolerations:
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoExecute
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoSchedule          
      EOT
    ]    
  }
}

Деплоїмо, перевіряємо под:

$ kk get pod | grep metr
metrics-server-76c55fc4fc-b9wdb               1/1     Running   0          33s

І перевіряємо з kubectl top:

$ kk top node
NAME                          CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%   
ip-10-1-32-148.ec2.internal   53m          2%     656Mi           19%       
ip-10-1-49-103.ec2.internal   56m          2%     788Mi           23%       
...

Terraform та Vertical Pod Autoscaler

Щось забув, що для Horizontal Pod Autoscaler окремого контроллеру не треба, тож тут нам знадобиться тільки додати Vertical Pod Autoscaler.

Беремо знову з Amazon EKS Blueprints Addons, див. vpa.

Знаходимо версію:

$ helm repo add vpa https://charts.fairwinds.com/stable
"vpa" has been added to your repositories

$ helm search repo vpa/vpa
NAME    CHART VERSION   APP VERSION     DESCRIPTION                                       
vpa/vpa 2.5.1           0.14.0          A Helm chart for Kubernetes Vertical Pod Autosc...

Додаємо до terraform.tfvars:

helm_release_versions = {
  ...
  vpa                                   = "2.5.1"
}

Додаємо до controllers.tf:

...

  enable_vpa = true
  vpa = {
    namespace = "kube-system"
    values = [
      <<-EOT
        tolerations:
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoExecute
        - key: CriticalAddonsOnly
          value: "true"
          operator: Equal
          effect: NoSchedule          
      EOT
    ]
  }
}

Деплоїмо, перевіряємо поди:

$ kk get pod | grep vpa
vpa-admission-controller-697d87b47f-tlzmb     1/1     Running             0          70s
vpa-recommender-6fd945b759-6xdm6              1/1     Running             0          70s
vpa-updater-bbf597fdd-m8pjg                   1/1     Running             0          70s

Та CRD:

$ kk get crd | grep vertic
verticalpodautoscalercheckpoints.autoscaling.k8s.io         2023-09-14T11:18:06Z
verticalpodautoscalers.autoscaling.k8s.io                   2023-09-14T11:18:06Z

Тестування Vertical Pod Autoscaler

Описуємо Deployment та VPA:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534 # nobody
      containers:
        - name: hamster
          image: registry.k8s.io/ubuntu-slim:0.1
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"
---
apiVersion: "autoscaling.k8s.io/v1"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]

Деплоїмо, і за хвилину-дві перевіряємо VPA:

$ kk get vpa
NAME          MODE   CPU    MEM         PROVIDED   AGE
hamster-vpa          100m   104857600   True       61s

Та статус подів:

$ kk get pod
NAME                       READY   STATUS        RESTARTS   AGE
hamster-8688cd95f9-hswm6   1/1     Running       0          60s
hamster-8688cd95f9-tl8bd   1/1     Terminating   0          90s

Готово.

Додавання Subscription Filter до Cloudwatch Log Group з Terraform

Майже все зробили. Залишилось дві дрібниці.

Спершу – додати форвадніг логів з EKS Cloudwatch Log Groups до Lambda-функції, в якій працює Promtail, який буде ці логи пересилати до інстансу Grafana Loki.

Наш EKS модуль створює CloudWatch Log Group /aws/eks/atlas-eks-dev-1-27-cluster/cluster зі стрімами:

І виводить ім’я цієї групи через output cloudwatch_log_group_name, який ми можемо використати у aws_cloudwatch_log_subscription_filter, щоб до цієї лог-групи додати фільтр, в якому треба передати destination_arn з ARN нашої Lambda.

Lambda-функція у нас вже є, створюється окремою автоматизацію для моніторинг-стеку. Щоб отримати її ARN – використаємо data "aws_lambda_function", в який передамо ім’я функції, а саме ім’я винесемо у змінні:

variable "promtail_lambda_logger_function_name" {
  type        = string
  description = "Monitoring Stack's Lambda Function with Promtail to collect logs to Grafana Loki"
}

Значення в tfvars:

...

promtail_lambda_logger_function_name  = "grafana-dev-1-26-loki-logger-eks"

Щоб наш Subscription Filter мав змогу звератись до цієї функції – потрібно додати aws_lambda_permission, де в source_arn передаємо ARN нашої лог-групи. Тут зверніть увагу, що ARN передається як arn::name:*.

У principal треба вказати logs.AWS_REGION.amazonaws.com – AWS_REGION отримаємо з data "aws_region".

Описуємо ресурси в eks.tf:

...

data "aws_lambda_function" "promtail_logger" {
  function_name = var.promtail_lambda_logger_function_name
}

data "aws_region" "current" {}

resource "aws_lambda_permission" "allow_cloudwatch_for_promtail" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = var.promtail_lambda_logger_function_name
  principal     = "logs.${data.aws_region.current.name}.amazonaws.com"
  source_arn    = "${module.eks.cloudwatch_log_group_arn}:*"
}

resource "aws_cloudwatch_log_subscription_filter" "eks_promtail_logger" {
  name            = "eks_promtail"
  log_group_name  = module.eks.cloudwatch_log_group_name
  filter_pattern  = ""
  destination_arn = data.aws_lambda_function.promtail_logger.arn
}

Деплоїмо, і перевіряємо Log Group:

І логи в Grafana Loki:

Створення StorageClass з Terraform

В іншому модулі EKS для Terraform – cookpad/terraform-aws-eks – це можна було зробити через файл шаблону storage_classes.yaml.tmpl, але в нашому модулі такого нема.

Втім, робиться це одним маніфестом, так що додаємо його в наш eks.tf:

...

resource "kubectl_manifest" "storageclass_gp2_retain" {

  yaml_body = <<YAML
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: gp2-retain
    provisioner: kubernetes.io/aws-ebs
    reclaimPolicy: Retain
    allowVolumeExpansion: true
    volumeBindingMode: WaitForFirstConsumer  
  YAML
}

Деплоїмо, і перевіряємо доступні StorageClass:

$ kk get storageclass
NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp2 (default)   kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  27h
gp2-retain      kubernetes.io/aws-ebs   Retain          WaitForFirstConsumer   true                   5m46s

Ну і нарешті – на цьому все.

Наче додав все, що потрібно для повноцінного кластеру AWS Elastic Kubernetes Service.