Web 应用想弹性扩缩容还必须用 AWS?在 Amazon Elastic Kubernetes Service (EKS) 上部署一个典型 Web App 的笔记
本文是一篇笔记,方便后人踩坑,也方便自己在之后踩坑了之后回顾整个流程,阅读本文需要有以下预备知识/准备:
- 一个没有欠费的 AWS 帐号
- 已经可以容器化运行的应用程序,并且镜像已经推到了对应区域的 ECR 中或者为公开镜像
作为一个典型的 Web App,肯定是由 App Server ,Web Server 和 Database 构成,为了能做到比较可靠地弹性扩缩容,这里全部使用 AWS 平台,对应的就变成了 Container,Application Load Balancer 和 AWS RDS。
为了在 AWS 运行自己的容器,我们有如下的选择:
- 自己开 EC2 上面装 Docker 跑(那这个性价比差的不如直接用 Hetzner)
- 用 Amazon Elastic Container Service (Amazon ECS) (配置起来有点麻烦,不工业风)
- 用 Amazon App Runner(这个很灵性,只能设置一个容器,而且和已有的 VPC 不通,用不了 RDS)
- 用 Amazon Elastic Kubernetes Service (EKS) (也是本文的主要重点)
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
这里需要保证 eksctl
, kubectl
和 aws
已经安装,可以参考 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 平台,资源不用了及时删除,祝大家玩的开心!