Nova Kwok's Awesome Blog

Web 应用想弹性扩缩容还必须用 AWS?在 Amazon Elastic Kubernetes Service (EKS) 上部署一个典型 Web App 的笔记

本文是一篇笔记,方便后人踩坑,也方便自己在之后踩坑了之后回顾整个流程,阅读本文需要有以下预备知识/准备:

作为一个典型的 Web App,肯定是由 App Server ,Web Server 和 Database 构成,为了能做到比较可靠地弹性扩缩容,这里全部使用 AWS 平台,对应的就变成了 Container,Application Load Balancer 和 AWS RDS。

为了在 AWS 运行自己的容器,我们有如下的选择:

Amazon Elastic Kubernetes Service (EKS)

这个是 Amazon 维护的 Kubernetes 服务,我们都知道自己安装/维护一个 Kubernetes 肯定是个吃力不讨好的事情,节点证书续签,集群升级,网络插件等等都是摆在 K8s 初学者面前的一个个槛,考虑到我对 Kubernetes 一窍不通,且在 PingCAP 工作的时候干过很多这种奇葩事情,所以既然这里选择了用 K8s ,那还是专注 kubectl 一顿 apply 就好,剩下的事情和锅全部丢给服务商(a.k.a,AWS)来背。

关于价格,文档上是这么说的: You pay $0.10 per hour for each Amazon EKS cluster that you create.,也就是说一个月它的控制面就会直接吃掉你的 72USD ,此外机器的钱是另算的,相比较 DigitalOcean/Vultr/Linode 这种免费控制面的服务商来说, AWS 贵了不少。

Create Cluster

既然是记录,我们就尽快开始我们的整个流程,这里主要参考了以下两个文档:

感觉上述两个文档对于 Quick Start 而言比 AWS 官方文档不知道高到哪儿去了。

eksctl, kubectl and aws

这里需要保证 eksctlkubectlaws 已经安装,可以参考 AWS 的两篇文章:

eksctl 的官网上虽然说的是 The official CLI for Amazon EKS,但是底下有一句 created by Weaveworks and it welcomes contributions from the community,非常灵性。

如果你和我一样用的 Linux 的话,直接复制我的指令吧~

curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
sudo mv /tmp/eksctl /usr/bin
eksctl version

curl -o kubectl https://s3.us-west-2.amazonaws.com/amazon-eks/1.24.7/2022-10-31/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/bin/

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

AWS Credentials

首先你需要登录你的控制台拿到 Credentials 并放到 ~/.aws/credentials 文件中(也就是 GitHub 上经常看到别人泄漏的这个部分:

[default]
aws_access_key_id = AKHAHAHAHAHAH6HPS6R
aws_secret_access_key = NOjlIsTHAHAHAHAHAHAHAHAHAHAHSOUsX
region = ap-northeast-1

[nova]
aws_access_key_id = AKHBABABABABABH6HPS6R
aws_secret_access_key = NOjlIsTHAABABABABAAHAHSOUsX
region = ap-southeast-1

Cluster

如果你的 ~/.aws/credentials 文件中上上述例子中有多个 Profile 的话,可以通过在命令前面加入 AWS_PROFILE=<name> 来使用对应 Profile。

通过这个命令可以快速创建一个叫 novacluster 的 EKS Cluster, eksctl 背后会创建一个 Cloudformation 来处理所有我们不想自己处理的细节(什么 IAM 啦,什么 Tag 啦),这一步一般需要等 10+ 分钟。

eksctl create cluster --name=novacluster --without-nodegroup --region=ap-southeast-1

接下来我们需要创建一个 OIDC identity provider。

eksctl utils associate-iam-oidc-provider \
    --region ap-southeast-1 \
    --cluster novacluster \
    --approve

然后我们创建一个叫 worker-group 的 NodeGroup,也就是实际用来跑负载的机器:

eksctl create nodegroup --cluster=novacluster --region=ap-southeast-1 --name=worker-group --node-type=t3.medium --nodes=3 --nodes-min=2 --nodes-max=10 --node-volume-size=20 --managed --asg-access --full-ecr-access --alb-ingress-access

这里有个建议,不要用 Free Tier 的 t3.micro,不然后续在部署 cluster-autoscaler 的时候会遇到 Insufficient memory 的问题,因为它需要 600Mi 的内存,而 t3.micro 啥都不跑的时候就只剩下 400Mi 了。

如果你搞错了,那建议先创建一个配置更高新的 Nodegroup ,然后用 eksctl delete nodegroup --cluster=novacluster --name=worker-group --region=ap-southeast-1 删掉老的 Nodegroup

这个类似核酸检测,要先考虑全部场所取消核酸检测核验之后再去撤掉核酸检测点,不能反过来,但是有些人就是想不明白,导致的后果就是你的 Pod 会和市民一样在寒风中 Pending 很久。

这个时候我们的集群已经创建好了,为了让本地 kubectl 可以使用,我们需要用 AWS Cli 来获得 kubeconfig,指令是这样的:

aws eks update-kubeconfig --region ap-southeast-1 --name novacluster

这个时候我们 kubectl 应该已经可以用了, 试试看:

kubectl get no
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-26-67.ap-southeast-1.compute.internal    Ready    <none>   98m   v1.23.13-eks-fb459a0
ip-192-168-42-176.ap-southeast-1.compute.internal   Ready    <none>   75m   v1.23.13-eks-fb459a0
ip-192-168-46-84.ap-southeast-1.compute.internal    Ready    <none>   98m   v1.23.13-eks-fb459a0
ip-192-168-72-96.ap-southeast-1.compute.internal    Ready    <none>   75m   v1.23.13-eks-fb459a0
ip-192-168-75-202.ap-southeast-1.compute.internal   Ready    <none>   98m   v1.23.13-eks-fb459a0

此时,我们的集群,机器都已经可用了,同时 Eksctl 也创建了一堆 VPC 和 Subnet。

我们注意到这里默认的 Subnet 的 VPC 是 vpc-1f2a1f78 ,对应的段是 172.31.0.0/20 ,而 eksctl 搞出来的段是 192.168.0.0/19 ,这也导致了之后配置 App 连接 RDS 时不能像 EC2 连接 RDS 一样在一个 VPC 下内网互通,而需要使用 VPC Peering。

Node AutoScale

如果你想手动给集群扩缩容 Node 的话,可以用这个指令:

eksctl scale nodegroup --cluster=novacluster --region ap-southeast-1 --nodes=5 worker-group

我知道我们在创建集群的时候已经指定了 --nodes=3 --nodes-min=2 --nodes-max=10,这个时候你可能会想:

「啊,那 AWS 肯定就会自动在 2 ~ 10 个节点之间根据集群情况自动弹性扩缩容吧?」

如果你想让 Node 自动扩缩容的话,需要手动搞一个 cluster-autoscaler

真的,EKS 已经 Manage 这么多了,为什么 Scale 机器不能集成做成一个 Addon 点一下自动安装,或者像 DigitalOcean DOKS 一样默认自带?

指令如下,先创建一个 Policy 允许 Autoscale,创建一个叫 cluster-autoscaler-policy.json 的文件,内容如下:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "autoscaling:SetDesiredCapacity",
                "autoscaling:TerminateInstanceInAutoScalingGroup"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/k8s.io/cluster-autoscaler/my-cluster": "owned"
                }
            }
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "autoscaling:DescribeAutoScalingInstances",
                "autoscaling:DescribeAutoScalingGroups",
                "ec2:DescribeLaunchTemplateVersions",
                "autoscaling:DescribeTags",
                "autoscaling:DescribeLaunchConfigurations"
            ],
            "Resource": "*"
        }
    ]
}

记得把 my-cluster 改为你自己 EKS Cluster 的名字,不然等着报错~

之后用 AWS 工具给 Apply 上去:

aws iam create-policy \
    --policy-name AmazonEKSClusterAutoscalerPolicy \
    --policy-document file://cluster-autoscaler-policy.json

然后用 eksctl 创建一个 IAM Service Account 并 Attach 上刚刚的 Policy

eksctl create iamserviceaccount \
  --cluster=novacluster --region=ap-southeast-1 \
  --namespace=kube-system \
  --name=cluster-autoscaler \
  --attach-policy-arn=arn:aws:iam::111122223333:policy/AmazonEKSClusterAutoscalerPolicy \
  --override-existing-serviceaccounts \
  --approve

之后就开始安装 cluster-autoscaler, 非常 Cloud Naive 。

curl -o cluster-autoscaler-autodiscover.yaml https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
sed -i.bak -e 's|<YOUR CLUSTER NAME>|novacluster|' ./cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml

kubectl annotate serviceaccount cluster-autoscaler \
  -n kube-system \
  eks.amazonaws.com/role-arn=arn:aws:iam::111122223333:role/AmazonEKSClusterAutoscalerRole

kubectl patch deployment cluster-autoscaler \
  -n kube-system \
  -p '{"spec":{"template":{"metadata":{"annotations":{"cluster-autoscaler.kubernetes.io/safe-to-evict": "false"}}}}}'

之后就可以通过 kubectl -n kube-system logs -f deployment.apps/cluster-autoscaler 看 Log 来判断 cluster-autoscaler 是否已经正常工作了,此时如果有什么没法 Schedule 的 Pod ,这个东西就会自动给你开新的机器用来扩容了。类似的,如果你的资源很少的话,它会帮你把你的 Node 给动态清零。

Application Load Balancer Controller

我们需要对外暴露我们的应用,所以需要一个 Load Balancer,价格是 $0.0225 per Application Load Balancer-hour (or partial hour) + $0.008 per LCU-hour (or partial hour),就是说你即使啥流量都没有,也要 18USD/mo。

要在 EKS 中集成对于 ALB(Application Load Balancer) ,需要手动安装 Application Load Balancer Controller 并给对应的 subnet 打 tag ,在上述 eksctl create nodegroup 中我们看到有一个 --alb-ingress-access 只是帮我们做了后半部分,安装 Controller 还是得自己手动来,具体流程如下,

创建 IAMPolicy:

curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.4/docs/install/iam_policy.json
aws iam create-policy \
    --policy-name AWSLoadBalancerControllerIAMPolicy \
    --policy-document file://iam_policy.json

先用 eksctl 创建一个叫 aws-load-balancer-controller 的 ServiceAccount 供后续 ALB Controller 使用,记得替换 111122223333 为你的 Account ID。

eksctl create iamserviceaccount \
  --cluster=novacluster \
  --region=ap-southeast-1 \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --role-name "AmazonEKSLoadBalancerControllerRole" \
  --attach-policy-arn=arn:aws:iam::111122223333:policy/AWSLoadBalancerControllerIAMPolicy \
  --approve

然后安装 cert-manager 和 Controller,纯粹看着文档复制粘贴即可。

kubectl apply \
    --validate=false \
    -f https://github.com/jetstack/cert-manager/releases/download/v1.5.4/cert-manager.yaml
curl -Lo v2_4_4_full.yaml https://github.com/kubernetes-sigs/aws-load-balancer-controller/releases/download/v2.4.4/v2_4_4_full.yaml
sed -i.bak -e '480,488d' ./v2_4_4_full.yaml
sed -i.bak -e 's|your-cluster-name|novacluster|' ./v2_4_4_full.yaml

kubectl apply -f v2_4_4_full.yaml
kubectl apply -f https://github.com/kubernetes-sigs/aws-load-balancer-controller/releases/download/v2.4.4/v2_4_4_ingclass.yaml

此时我们可以验证一下这个 Controller 是否已经正确安装:

kubectl get deployment -n kube-system aws-load-balancer-controller
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
aws-load-balancer-controller   1/1     1            1           95m

完整文档在 Installing the AWS Load Balancer Controller add-on,如果你不放心的话可以复制粘贴那个文档上的,但是真的,EKS 已经 Manage 这么多了,为什么 ALB 集成不能做成一个 Addon 点一下自动安装?

此时我们看看 AWS 网页上面,你的集群中应该有如下 Deployments 了:

Application

终于,有了上述的集群准备和 Load balancer Controller 之后,可以部署我们的应用啦,为了方便和干净起见,我们整一个叫 novaapp 的 Namespace 并且把所有资源都丢到这个 Namespace 下,并让 AWS 自动给我们加上 Load Balancer 用于对外访问:

---
apiVersion: v1
kind: Namespace
metadata:
  name: novaapp
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: novaapp
  name: novaapp-mini-deployment
  labels:
    app: novaapp-mini
spec:
  replicas: 2
  selector:
    matchLabels:
      app: novaapp-mini
  template:
    metadata:
      labels:
        app: novaapp-mini
    spec:
      containers:
        - name: novaapp
          imagePullPolicy: Always
          image: '111122223333.dkr.ecr.ap-southeast-1.amazonaws.com/novaapp:latest'
          env:
            - name: DB_HOST
              value: "novards.c4s0xipwdxny.ap-southeast-1.rds.amazonaws.com"
            - name: APP_DEBUG
              value: "true"
            - name: DB_PORT
              value: "3306"
            - name: DB_DATABASE
              value: "novaapp"
            - name: DB_USERNAME
              value: "admin"
            - name: DB_PASSWORD
              value: "password"
          resources:
            limits:
              cpu: 500m
---
apiVersion: v1
kind: Service
metadata:
  namespace: novaapp
  name: novaapp-mini-service
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort
  selector:
    app: novaapp-mini
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: novaapp
  name: ingress-novaapp
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: novaapp-mini-service
              port:
                number: 80
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  namespace: novaapp
  name: novaapp-mini-autoscaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: novaapp-mini-deployment
  minReplicas: 2
  maxReplicas: 20
  targetCPUUtilizationPercentage: 10

看上去很长?是的,这就是大家推崇的 Cloud Naive Way!

其实只要仔细看就会发现这是一个整套组件,从上到下分为 Namespace,Deployment(实际的应用),Service(用 NodePort 暴露整个应用并提供负载均衡,Ingress(让 AWS 创建一个 ALB 来把流量引到 Service 上,并显式指定了 /healthz 作为健康检查,不然如果你的 / 会返回 404 的话服务会一直 503),HorizontalPodAutoscaler(如果一个 Pod CPU 使用率大于 10% 就自动创建更多的 Pod,最多创建 20 个,用于弹性扩容 Pod)。

这里也同时可以测试一下比如修改 Replica 到一个比较大的值,观察在所有 Node 都已经满了的时候 cluster-autoscaler 是否能正常创建新的 Node 加入到集群中使用。

ALB SSL

在上面的案例中,我们的 ALB 配置是这样的:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: novaapp
  name: ingress-novaapp
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: novaapp-mini-service
              port:
                number: 80

这个时候我们已经可以通过访问 ALB 的地址来直接访问我们的应用了,但是,没有 SSL 怎么行?而且最终我们需要做一个 CNAME 解析到这个地址上,用我们自己的域名来 Serve 整个 App。

所以这里我们需要先到 AWS 的 ACM 打一局 ACM/ICPC上弄一个 SSL 证书:

此时你可以获得一个 ARN,比如我这里是 arn:aws:acm:ap-southeast-1:111122223333:certificate/b9480e8e-c0e6-4cec-9ac4-38715ad35888,等证书验证通过了之后我们把 ALB 的配置修改一下,修改成如下样子:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: novaapp
  name: ingress-novaapp
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:111122223333:certificate/b9480e8e-c0e6-4cec-9ac4-38715ad35888
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: novaapp-mini-service
              port:
                number: 80

然后给 Apply 到集群中即可~这个时候我们看 Load Balancer 页面应该已经可以看到正常显示了 80,443 端口,且证书已经正确配置了:

这个时候我们去 Cloudflare 上弄个 CNAME 记录解析到这个地址上,并开启 Cloudflare 的 Proxy,就成了~

RDS & Peering

创建 RDS 的过程非常简单,我们可以直接在 RDS 控制面板上创建即可,创建完成之后我们需要在它的 Security Group 中允许一下来自 eksctl 搞出来的 subnet 的流量:

下一步我们需要把这两个段给 Peer 到一起,不然容器下方的 EC2 会连接不上 RDS

我们创建一个 VPC Peer ,并把这两个 VPC 先 Peer 到一起。

创建完之后记得点一下 Accept Peer。

Peer 成功之后就需要在两边通报对方路由,在 Default VPC 上通报一下 EKS 那个 VPC 的段:

反之在 EKS 的那堆 Route tables 上通报一下 Default VPC 的段:

此时,你的应用应该就可以正常连接到 RDS 使用了~

Monitoring && Logging

人生苦短,别自己折腾了监控组件了,这里直接 Datadog 的栈就好了,可以参考: Install the Datadog Agent on Kubernetes, 要安装只要以下三步:

helm repo add datadog https://helm.datadoghq.com
helm install my-datadog-operator datadog/datadog-operator
kubectl create secret generic datadog-secret --from-literal api-key=<DATADOG_API_KEY> --from-literal app-key=<DATADOG_APP_KEY>

弄个配置文件,比如 datadog-agent.yaml, 内容如下:

apiVersion: datadoghq.com/v1alpha1
kind: DatadogAgent
metadata:
  name: datadog
spec:
  credentials:
    apiSecret:
      secretName: datadog-secret
      keyName: api-key
    appSecret:
      secretName: datadog-secret
      keyName: app-key
  agent:
    image:
      name: "gcr.io/datadoghq/agent:latest"
  clusterAgent:
    image:
      name: "gcr.io/datadoghq/cluster-agent:latest"

然后: kubectl apply -f /path/to/your/datadog-agent.yaml 就齐活了~

以上,我们的应用就已经跑起来了,如果需要给容器换镜像的话暂时可以本地改 Deployment 文件之后 kubectl apply 一把梭,后续还有 CI/CD,监控和报警相关的内容由于本文篇幅限制(加上我也还在学习中),就暂时不在本文中涵盖了~

时刻谨记这个 AWS 平台,资源不用了及时删除,祝大家玩的开心!

References

  1. The Architecture Behind A One-Person Tech Startup
  2. eksctl - The official CLI for Amazon EKS
  3. Creating or updating a kubeconfig file for an Amazon EKS cluster
  4. Installing the AWS Load Balancer Controller add-on

#Chinese #AWS