Saturday, April 18, 2020

HPA based on external metric in cloudwatch

CA(Cluster Autoscaler)

EKS worker node in general:


One thing to note is that while Managed Node Groups provides a managed experience for the provisioning and lifecycle of EC2 instances, they do not configure horizontal auto-scaling or vertical auto-scaling. This means that you still need to use a service like Kubernetes Cluster Autoscaler to implement auto-scaling of the underlying ASG.

EKS managed node groups for Cluster Autoscaler:


Enable CA for managed node group setup (v1, works):


Enable CA for managed node group setup (following its instruction and use code from v1 below):

Working code:




HPA (horizontal pod autoscaler)

This is the best link I have so far talking about HPA and custom metrics


Limitations

Terraform does not support external metrics for HPA:


MSK does not support consumer group lag in CloudWatch as Dec 2019:




Install Metrics server:


DOWNLOAD_URL=$(curl -Ls "https://api.github.com/repos/kubernetes-sigs/metrics-server/releases/latest" | jq -r .tarball_url)
DOWNLOAD_VERSION=$(grep -o '[^/v]*$' <<< $DOWNLOAD_URL)
curl -Ls $DOWNLOAD_URL -o metrics-server-$DOWNLOAD_VERSION.tar.gz
mkdir metrics-server-$DOWNLOAD_VERSION
tar -xzf metrics-server-$DOWNLOAD_VERSION.tar.gz --directory metrics-server-$DOWNLOAD_VERSION --strip-components 1
kubectl apply -f metrics-server-$DOWNLOAD_VERSION/deploy/1.8+/

LT-2018-9999:custom_hpa_metrics jzeng$ kubectl get pod -n kube-system
NAME                              READY   STATUS    RESTARTS   AGE
aws-node-2m8s5                    1/1     Running   0          5d2h
aws-node-bdbzq                    1/1     Running   0          5d2h
aws-node-drcs5                    1/1     Running   0          5d2h
coredns-74dd858ddc-64jl6          1/1     Running   0          5d2h
coredns-74dd858ddc-f9s8t          1/1     Running   0          5d2h
kube-proxy-2snmp                  1/1     Running   0          5d2h
kube-proxy-q8qfz                  1/1     Running   0          5d2h
kube-proxy-w9vr6                  1/1     Running   0          5d2h
metrics-server-7fcf9cc98b-9fqx9   1/1     Running   0          41s


External Metrics


AWS CloudWatch Metrics Adapter for K8s (k8s-cloudwatch-adapter) for external metrics



Generate external metric:

LT-2018-9999:dev jzeng$ cat metrics_kafka_lag.yaml
apiVersion: metrics.aws/v1alpha1
kind: ExternalMetric
metadata:
  name: metrics-kafka-lag
spec:
  name: metrics-kafka-lag
  resource:
    resource: "deployment"
  queries:
    - id: metrics_kafka_lag
      metricStat:
        metric:
          namespace: "co-ec-eks-cluster-vpc-05b52a0b999999999"
          metricName: "KafkaTopicTotalLag"
          dimensions:
              - name: QueueName
                value: "task"
              - name: metric_type
                value: "counter"
        period: 600
        stat: Average
        unit: None
      returnData: true


Generate hpa for such external metric:

LT-2018-9999:dev jzeng$ cat hpa_kafka_lag.yaml
kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta1
metadata:
  name: external-kafka-lag-scaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1beta1
    kind: Deployment
    name: executor-co-ec-eks-cluster-vpc-05b52a0b999999999
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: External
    external:
      metricName: metrics-kafka-lag
      targetAverageValue: 3


Custom metrics with Promethus:



Multiple metrics for same pod’s HPA:



Troubleshooting:

Check external metrics:

LT-2018-9999:dev jzeng$ kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" |jq
{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "external.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "metrics-kafka-lag",
      "singularName": "",
      "namespaced": true,
      "kind": "ExternalMetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "hello-queue-length",
      "singularName": "",
      "namespaced": true,
      "kind": "ExternalMetricValueList",
      "verbs": [
        "get"
      ]
    }
  ]
}

List metrics from cloudwatch:

LT-2018-9999:dev jzeng$ aws cloudwatch list-metrics --region us-east-1 --namespace co-ec-eks-cluster-vpc-05b52a0b999999999
{
    "Metrics": [
        {
            "Namespace": "co-ec-eks-cluster-vpc-05b52a0b999999999",
            "Dimensions": [
                {
                    "Name": "metric_type",
                    "Value": "counter"
                },
                {
                    "Name": "QueueName",
                    "Value": "task"
                }
            ],
            "MetricName": "KafkaTopic#TotalLag"
        },
        {
            "Namespace": "co-ec-eks-cluster-vpc-05b52a0b999999999",
            "Dimensions": [
                {
                    "Name": "metric_type",
                    "Value": "counter"
                },
                {
                    "Name": "QueueName",
                    "Value": "task"
                }
            ],
            "MetricName": "KafkaTopicTotalLag"
        }
    ]
}

Query a metric (if--unit is not passed as parameter, all data that was collected with any unit will be returned):

LT-2018-9999:dev jzeng$ aws cloudwatch get-metric-statistics  --start-time 2020-04-15T04:00:00Z --end-time 2020-04-19T04:00:00Z --region us-east-1 --namespace co-ec-eks-cluster-vpc-05b52a0b999999999 --metric-name KafkaTopicTotalLag --period 300 --statistics Average --dimensions Name=QueueName,Value=task Name=metric_type,Value=counter
{
    "Datapoints": [
        {
            "Timestamp": "2020-04-18T20:55:00Z",
            "Average": 1211.0,
            "Unit": "None"
        },
        {
            "Timestamp": "2020-04-18T05:20:00Z",
            "Average": 1211.0,
            "Unit": "None"
        },

Or:

LT-2018-9999:dev jzeng$ cat cwquery.json
[
    {
        "Id": "metrics_kafka_lag",
        "MetricStat": {
            "Metric": {
                "Namespace": "co-ec-eks-cluster-vpc-05b52a0b999999999",
                "MetricName": "KafkaTopicTotalLag",
                "Dimensions": [
                    {
                        "Name": "QueueName",
                        "Value": "task"
                    },
                    {
                        "Name": "metric_type",
                        "Value": "counter"
                    }
                ]
            },
            "Period": 600,
            "Stat": "Sum",
            "Unit": "None"
        },
        "ReturnData": true
    }
]

LT-2018-9999:dev jzeng$ aws cloudwatch get-metric-data --metric-data-queries file://./cwquery.json --start-time 2020-04-18T04:00:00Z --end-time 2020-04-18T05:00:00Z --region us-east-1
{
    "Messages": [],
    "MetricDataResults": [
        {
            "Timestamps": [
                "2020-04-18T04:50:00Z",
                "2020-04-18T04:40:00Z",
                "2020-04-18T04:30:00Z",
                "2020-04-18T04:20:00Z",
                "2020-04-18T04:10:00Z",
                "2020-04-18T04:00:00Z"
            ],
            "StatusCode": "Complete",
            "Values": [
                12110.0,
                12110.0,
                12110.0,
                12110.0,
                12110.0,
                12110.0
            ],
            "Id": "metrics_kafka_lag",
            "Label": "KafkaTopicTotalLag"
        }
    ]
}





Important thing to remember:

The maximum number of data points returned from a single call is 1,440. If you request more than 1,440 data points, CloudWatch returns an error (actually reuturn nothing instead of error). To reduce the number of data points, you can narrow the specified time range and make multiple requests across adjacent time ranges, or you can increase the specified period. 










Saturday, April 11, 2020

EKS HPA based on basic metric such as CPU


1.Terraform code in service.tf:


       

resource "kubernetes_horizontal_pod_autoscaler" "co-ec-hpa" {
  for_each = var.service_parameters
  metadata {
    name = each.value.desc
  }
  spec {
    max_replicas                      = each.value.service_hpa_max
    min_replicas                      = each.value.service_hpa_min
    target_cpu_utilization_percentage = 60 // TODO
    scale_target_ref {
      api_version = "extensions/v1beta1"
      kind        = "Deployment"
      name        = "${each.value.name}-${var.eks_cluster_name}"
    }
  }
}

       
 

2. Install Metrics Server


DOWNLOAD_URL=$(curl -Ls "https://api.github.com/repos/kubernetes-sigs/metrics-server/releases/latest" | jq -r .tarball_url)DOWNLOAD_VERSION=$(grep -o '[^/v]*$' <<< $DOWNLOAD_URL)curl -Ls $DOWNLOAD_URL -o metrics-server-$DOWNLOAD_VERSION.tar.gz
mkdir metrics-server-$DOWNLOAD_VERSION
tar -xzf metrics-server-$DOWNLOAD_VERSION.tar.gz --directory metrics-server-$DOWNLOAD_VERSION --strip-components 1kubectl apply -f metrics-server-$DOWNLOAD_VERSION/deploy/1.8+/
rm metrics-server-$DOWNLOAD_VERSION.tar.gz
rm -rf metrics-server-$DOWNLOAD_VERSION


3. Check the pod under 'kube-system' name space

kube-system         metrics-server-7fcf9cc98b-eeeee                                   1/1     Running   0          47h


Reference



(For HPA based on external metrics, please contact me)


EKS security


1. Limit the access to cluster api server

Use UI or following command:

aws eks update-cluster-config \
    --region us-east-1 \
    --name co-ec-eks-cluster-vpc-05b52a0ba174eeeee \
--resources-vpc-config endpointPublicAccess=true,publicAccessCidrs="19.19.19.19/32",endpointPrivateAccess=true

Use UI (EKS Networking section) or following command to check change result:
aws eks describe-cluster --name co-ec-eks-cluster-vpc-05b52a0ba174eeeee --region us-east-1

We can also do this from terraform through following code.  Sample code:

resource "aws_eks_cluster" "co-ec-eks-cluster" {
 
name     = local.eks_cluster_name
  role_arn
= aws_iam_role.co-ec-eks-cluster-iam-role.arn

 
vpc_config {
   
security_group_ids      = [aws_security_group.co-ec-eks-cluster-security-group.id]
   
subnet_ids              = local.subnet_ids
    endpoint_private_access
= true // allow access to EKS network
    // https://www.cloudflare.com/ips-v4 for list of IPs from Cloudflare
   
public_access_cidrs = toset(concat(data.cloudflare_ip_ranges.cloudflare.ipv4_cidr_blocks, local.workstation-external-cidr))
  }

 
depends_on = [
    aws_iam_role_policy_attachment.co-ec-eks-cluster-AmazonEKSClusterPolicy,
    aws_iam_role_policy_attachment.co-ec-eks-cluster-AmazonEKSServicePolicy,
  ]
}


2. Access control to EKS cluster node (so the services running on it will have access to the resources) through terraform:



resource "aws_iam_role" "fs-ec-eks-node-iam-role" {
  name = "fs-ec-eks-node-iam-role-${local.vpc_id}"
  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_role_policy" "fs-ec-eks-node-auto-scale-policy" {
  name = "fs-ec-eks-node-auto-scale-policy"  role = aws_iam_role.fs-ec-eks-node-iam-role.id
  policy = <<-EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "autoscaling:DescribeAutoScalingGroups",
                "autoscaling:DescribeAutoScalingInstances",
                "autoscaling:DescribeLaunchConfigurations",
                "autoscaling:DescribeTags",
                "autoscaling:SetDesiredCapacity",
                "autoscaling:TerminateInstanceInAutoScalingGroup"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

resource "aws_iam_role_policy" "fs-ec-eks-node-metrics-access-policy" {
  name = "fs-ec-eks-node-metrics-access-policy"  role = aws_iam_role.fs-ec-eks-node-iam-role.id
  policy = <<-EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cloudwatch:GetMetricData",
        "cloudwatch:GetMetricStatistics",
        "cloudwatch:ListMetrics"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "fs-ec-eks-node-AmazonEKSWorkerNodePolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"  role       = aws_iam_role.fs-ec-eks-node-iam-role.name}

resource "aws_iam_role_policy_attachment" "fs-ec-eks-node-AmazonEKS_CNI_Policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"  role       = aws_iam_role.fs-ec-eks-node-iam-role.name}

resource "aws_iam_role_policy_attachment" "fs-ec-eks-node-AmazonEC2ContainerRegistryReadOnly" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"  role       = aws_iam_role.fs-ec-eks-node-iam-role.name}

resource "aws_iam_role_policy_attachment" "fs-ec-eks-node-CloudWatchAgentServerPolicy" {
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"  role       = aws_iam_role.fs-ec-eks-node-iam-role.name}

resource "aws_iam_role_policy_attachment" "fs-ec-eks-node-AmazonDynamoDBFullAccess" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"  role       = aws_iam_role.fs-ec-eks-node-iam-role.name}


# Using the new feature from reinvent:19 to provisioning node automatically without the need# for EC2 provisioning.  EKS-optimized AMIs will be used automatically for each node.# Nodes launched as part of a managed node group are automatically tagged for auto-discovery# by k8s cluster autoscaler.# https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html# https://www.terraform.io/docs/providers/aws/r/eks_node_group.htmlresource "aws_eks_node_group" "fs-ec-eks-node-group" {
  cluster_name    = aws_eks_cluster.fs-ec-eks-cluster.name  node_group_name = "fs-ec-eks-node-group-${local.vpc_id}"  node_role_arn   = aws_iam_role.fs-ec-eks-node-iam-role.arn  subnet_ids      = local.subnet_ids  instance_types  = [var.instance_type]

  scaling_config {
    desired_size = 3    max_size     = 8 // TODO    min_size     = 3  }

  depends_on = [
    aws_iam_role_policy_attachment.fs-ec-eks-node-AmazonEKSWorkerNodePolicy,
    aws_iam_role_policy_attachment.fs-ec-eks-node-AmazonEKS_CNI_Policy,
    aws_iam_role_policy_attachment.fs-ec-eks-node-AmazonEC2ContainerRegistryReadOnly,
    aws_iam_role_policy_attachment.fs-ec-eks-node-CloudWatchAgentServerPolicy,
    aws_iam_role_policy_attachment.fs-ec-eks-node-AmazonDynamoDBFullAccess,
  ]
}


3. Control the access to MSK(Kafka):

resource "aws_security_group" "fs-ec-msk-cluster-security-group" {
  name        = "fs-ec-msk-cluster-security-group-${local.vpc_id}"  description = "Cluster communication with worker nodes"  vpc_id      = local.vpc_id
  egress {
    from_port   = 0    to_port     = 0    protocol    = "-1"    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "fs-ec-msk-cluster-${local.vpc_id}"  }
}

# allow access from every host in the same vpc.// TODOresource "aws_security_group_rule" "fs-ec-msk-cluster-ingress-workstation-http" {
  cidr_blocks       = [local.vpc_cidr]
  description       = "Allow access to Kafka from same VPC"  from_port         = 9092  protocol          = "tcp"  security_group_id = aws_security_group.fs-ec-msk-cluster-security-group.id  to_port           = 9092  type              = "ingress"}
resource "aws_security_group_rule" "fs-ec-msk-cluster-ingress-workstation-https" {
  cidr_blocks       = [local.vpc_cidr]
  description       = "Allow access to Kafka from same VPC"  from_port         = 9094  protocol          = "tcp"  security_group_id = aws_security_group.fs-ec-msk-cluster-security-group.id  to_port           = 9094  type              = "ingress"}
resource "aws_security_group_rule" "fs-ec-msk-cluster-ingress-workstation-zookeeper" {
  cidr_blocks       = [local.vpc_cidr]
  description       = "Allow access to Zookeeper from same VPC"  from_port         = 2181  protocol          = "tcp"  security_group_id = aws_security_group.fs-ec-msk-cluster-security-group.id  to_port           = 2181  type              = "ingress"}

resource "aws_kms_key" "fs-ec-kms" {
  description = "KMS key"}

resource "aws_msk_cluster" "fs-ec-msk-cluster" {
  cluster_name           = "fs-ec-msk-cluster-${local.vpc_id}"  kafka_version          = var.kafka_version  number_of_broker_nodes = length(local.subnets_ids)

  configuration_info {
    arn      = aws_msk_configuration.fs-ec-msk-configuration.arn    revision = aws_msk_configuration.fs-ec-msk-configuration.latest_revision  }

  broker_node_group_info {
    instance_type   = var.broker_type    ebs_volume_size = var.broker_ebs_size    client_subnets  = local.subnets_ids
    security_groups = [aws_security_group.fs-ec-msk-cluster-security-group.id]
  }

  encryption_info {
    encryption_at_rest_kms_key_arn = aws_kms_key.fs-ec-kms.arn    encryption_in_transit {
      client_broker = "TLS" // PLAINTEXT"        in_cluster = true    }
  }

  tags = {
    Name = "fs-ec-msk-cluster-${local.vpc_id}"  }
}

// it is not possible to destroy cluster configs so a random number is usedresource "random_id" "msk" {
  byte_length = 4}

resource "aws_msk_configuration" "fs-ec-msk-configuration" {
  kafka_versions = [var.kafka_version]
  name           = "${var.msk_config_name_prefix}fs-ec-msk-configuration-${local.vpc_id}-${random_id.msk.hex}"
  server_properties = <<PROPERTIES
auto.create.topics.enable = true
delete.topic.enable = false
num.partitions = 96
PROPERTIES
}




Reference: