歡迎光臨
每天分享高質量文章

破除對 AWS Fargate 的幻覺 | Linux 中國

我仍然相信 Fargate 是一項出色的技術,AWS 團隊對 ECS/Fargate 所做的工作確實非常出色。然而,我的觀點是,這絕對不比 Kubernetes “更容易”,只是……難點不同。

— Lee Briggs

 

我在 $work 建立了一個基於 Kubernetes 的平臺已經快一年了,而且有點像 Kubernetes 的佈道者了。真的,我認為這項技術太棒了。然而我並沒有對它的運營和維護的困難程度抱過什麼幻想。今年早些時候我讀了這樣[1]的一篇文章,並對其中某些觀點深以為然。如果我在一家規模較小的、有 10 到 15 個工程師的公司,假如有人建議管理和維護一批 Kubernetes 叢集,那我會感到害怕的,因為它的運維開銷太高了!

儘管我現在對 Kubernetes 的一切都很感興趣,但我仍然對“無伺服器Serverless”計算會消滅運維工程師的說法抱有好奇。這種好奇心主要來源於我希望在未來仍然能有一份有報酬的工作 —— 如果我們前景光明的未來不需要運維工程師,那我得明白到底是怎麼回事。我已經在 Lamdba 和Google Cloud Functions 上做了一些實驗,結果讓我印象十分深刻,但我仍然堅信無伺服器解決方案只是解決了一部分問題。

我關註 AWS Fargate[2] 已經有一段時間了,這就是 $work 的開發人員所推崇為“無伺服器計算”的東西 —— 主要是因為 Fargate,用它你就可以無需管理底層節點而執行你的 Docker 容器。我想看看它到底意味著什麼,所以我開始嘗試從頭開始在 Fargate 上執行一個應用,看看是否可以成功。這裡我對成功的定義是一個與“生產級”應用程式相近的東西,我想應該包含以下內容:

◈ 一個在 Fargate 上執行的容器
◈ 配置資訊以環境變數的形式下推
◈ “秘密資訊” 不能是明文的
◈ 位於負載均衡器之後
◈ 有效的 SSL 證書的 TLS 通道

我以“基礎設施即程式碼”的角度來開始整個任務,不遵循預設的 AWS 控制檯嚮導,而是使用 terraform 來定義基礎架構。這很可能讓整個事情變得複雜,但我想確保任何部署都是可重現的,任何想要遵循此步驟的人都可發現我的結論。

上述所有標準通常都可以透過基於 Kubernetes 的平臺使用一些外部的附加元件和外掛來實現,所以我確實是以一種比較的心態來處理整個任務的,因為我要將它與我的常用工作流程進行比較。我的主要標的是看看Fargate 有多容易,特別是與 Kubernetes 相比時。結果讓我感到非常驚訝。

AWS 是有開銷的

我有一個乾凈的 AWS 賬戶,並決定從零到部署一個 webapp。與 AWS 中的其它基礎設施一樣,我必須首先使基本的基礎設施正常工作起來,因此我需要先定義一個 VPC。

遵循最佳實踐,因此我將這個 VPC 劃分為跨可用區(AZ)的子網,一個公共子網和私有子網。這時我想到,只要這種設定基礎設施的需求存在,我就能找到一份這種工作。AWS 是運維的這一概念一直讓我感到憤怒。開發者社群中的許多人理所當然地認為在設定和定義一個設計良好的 AWS 賬戶和基礎設施是不需要付出多少工作和努力的。而這種想當然甚至發生在開始談論多帳戶架構之前就有了——現在我仍然使用單一帳戶,我已經必須定義好基礎設施和傳統的網路裝置。

這裡也值得記住,我已經做了很多次,所以我很清楚該做什麼。我可以在我的帳戶中使用預設的 VPC 以及預先提供的子網,我覺得很多剛開始的人也可以使用它。這大概花了我半個小時才執行起來,但我不禁想到,即使我想執行 lambda 函式,我仍然需要某種連線和網路。定義 NAT 閘道器和在 VPC 中路由根本不會讓你覺得很“Serverless”,但要往下進行這就是必須要做的。

執行簡單的容器

在我啟動運行了基本的基礎設施之後,現在我想讓我的 Docker 容器執行起來。我開始翻閱 Fargate 檔案並瀏覽 入門[3] 檔案,這些就馬上就展現在了我面前:

等等,只是讓我的容器執行就至少要有三個步驟?這完全不像我所想的,不過還是讓我們開始吧。

任務定義

任務定義Task Definition”用來定義要執行的實際容器。我在這裡遇到的問題是,任務定義這件事非常複雜。這裡有很多選項都很簡單,比如指定 Docker 映象和記憶體限制,但我還必須定義一個網路模型以及我並不熟悉的其它各種選項。真需要這樣嗎?如果我完全沒有 AWS 方面的知識就進入到這個過程裡,那麼在這個階段我會感覺非常的不知所措。可以在 AWS 頁面上找到這些 引數[4] 的完整串列,這個串列很長。我知道我的容器需要一些環境變數,它需要暴露一個埠。所以我首先在一個神奇的 terraform 模組[5] 的幫助下定義了這一點,這真的讓這件事更容易了。如果沒有這個模組,我就得手寫 JSON 來定義我的容器定義。

首先我定義了一些環境變數:

  1. container_environment_variables = [
  2. {
  3. name = "USER"
  4. value = "${var.user}"
  5. },
  6. {
  7. name = "PASSWORD"
  8. value = "${var.password}"
  9. }
  10. ]

然後我使用上面提及的模組組成了任務定義:

  1. module "container_definition_app" {
  2. source = "cloudposse/ecs-container-definition/aws"
  3. version = "v0.7.0"
  4. container_name = "${var.name}"
  5. container_image = "${var.image}"
  6. container_cpu = "${var.ecs_task_cpu}"
  7. container_memory = "${var.ecs_task_memory}"
  8. container_memory_reservation = "${var.container_memory_reservation}"
  9. port_mappings = [
  10. {
  11. containerPort = "${var.app_port}"
  12. hostPort = "${var.app_port}"
  13. protocol = "tcp"
  14. },
  15. ]
  16. environment = "${local.container_environment_variables}"
  17. }

在這一點上我非常困惑,我需要在這裡定義很多配置才能執行,而這時什麼都沒有開始呢,但這是必要的 —— 執行 Docker 容器肯定需要瞭解一些容器配置的知識。我 之前寫過[6] 關於 Kubernetes 和配置管理的問題的文章,在這裡似乎遇到了同樣的問題。

接下來,我在上面的模組中定義了任務定義(幸好從我這裡抽象出了所需的 JSON —— 如果我不得不手寫JSON,我可能已經放棄了)。

當我定義模組引數時,我突然意識到我漏掉了一些東西。我需要一個 IAM 角色!好吧,讓我來定義:

  1. resource "aws_iam_role" "ecs_task_execution" {
  2. name = "${var.name}-ecs_task_execution"
  3. assume_role_policy = <<EOF
  4. {
  5. "Version": "2008-10-17",
  6. "Statement": [
  7. {
  8. "Action": "sts:AssumeRole",
  9. "Principal": {
  10. "Service": "ecs-tasks.amazonaws.com"
  11. },
  12. "Effect": "Allow"
  13. }
  14. ]
  15. }
  16. EOF
  17. }
  18. resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
  19. count = "${length(var.policies_arn)}"
  20. role = "${aws_iam_role.ecs_task_execution.id}"
  21. policy_arn = "${element(var.policies_arn, count.index)}"
  22. }

這樣做是有意義的,我需要在 Kubernetes 中定義一個 RBAC 策略,所以在這裡我還未完全錯失或獲得任何東西。這時我開始覺得從 Kubernetes 的角度來看,這種感覺非常熟悉。

  1. resource "aws_ecs_task_definition" "app" {
  2. family = "${var.name}"
  3. network_mode = "awsvpc"
  4. requires_compatibilities = ["FARGATE"]
  5. cpu = "${var.ecs_task_cpu}"
  6. memory = "${var.ecs_task_memory}"
  7. execution_role_arn = "${aws_iam_role.ecs_task_execution.arn}"
  8. task_role_arn = "${aws_iam_role.ecs_task_execution.arn}"
  9. container_definitions = "${module.container_definition_app.json}"
  10. }

現在,為了執行起來我已經寫了很多行程式碼,我閱讀了很多 ECS 檔案,我所做的就是定義一個任務定義。我還沒有讓這個東西執行起來。在這一點上,我真的很困惑這個基於 Kubernetes 的平臺到底增值了什麼,但我繼續前行。

服務

服務,一定程度上是將容器如何暴露給外部,另外是如何定義它擁有的副本數量。我的第一個想法是“啊!這就像一個 Kubernetes 服務!”我開始著手對映埠等。這是我第一次在 terraform 上跑:

  1. resource "aws_ecs_service" "app" {
  2. name = "${var.name}"
  3. cluster = "${module.ecs.this_ecs_cluster_id}"
  4. task_definition = "${data.aws_ecs_task_definition.app.family}:${max(aws_ecs_task_definition.app.revision, data.aws_ecs_task_definition.app.revision)}"
  5. desired_count = "${var.ecs_service_desired_count}"
  6. launch_type = "FARGATE"
  7. deployment_maximum_percent = "${var.ecs_service_deployment_maximum_percent}"
  8. deployment_minimum_healthy_percent = "${var.ecs_service_deployment_minimum_healthy_percent}"
  9. network_configuration {
  10. subnets = ["${values(local.private_subnets)}"]
  11. security_groups = ["${module.app.this_security_group_id}"]
  12. }
  13. }

當我必須定義允許訪問所需埠的安全組時,我再次感到沮喪,當我這樣做了並將其插入到網路配置中後,我就像被扇了一巴掌。

我還需要定義自己的負載均衡器?

什麼?

當然不是嗎?

負載均衡器從未遠離

老實說,我很滿意,我甚至不確定為什麼。我已經習慣了 Kubernetes 的服務和 Ingress 物件,我一心認為用 Kubernetes 將我的應用程式放到網上是多麼容易。當然,我們在 $work 花了幾個月的時間建立一個平臺,以便更輕鬆。我是 external-dns[7] 和 cert-manager[8] 的重度使用者,它們可以自動填充 Ingress 物件上的 DNS 條目並自動化 TLS 證書,我非常瞭解進行這些設定所需的工作,但老實說,我認為在 Fargate 上做這件事會更容易。我認識到 Fargate 並沒有聲稱自己是“如何執行應用程式”這件事的全部和最終目的,它只是抽象出節點管理,但我一直被告知這比 Kubernetes 更加容易。我真的很驚訝。定義負載均衡器(即使你不想使用 Ingress 和 Ingress Controller)也是向 Kubernetes 部署服務的重要組成部分,我不得不在這裡再次做同樣的事情。這一切都讓人覺得如此熟悉。

我現在意識到我需要:

◈ 一個負載均衡器
◈ 一個 TLS 證書
◈ 一個 DNS 名字

所以我著手做了這些。我使用了一些流行的 terraform 模組,並做了這個:

  1. # Define a wildcard cert for my app
  2. module "acm" {
  3. source = "terraform-aws-modules/acm/aws"
  4. version = "v1.1.0"
  5. create_certificate = true
  6. domain_name = "${var.route53_zone_name}"
  7. zone_id = "${data.aws_route53_zone.this.id}"
  8. subject_alternative_names = [
  9. "*.${var.route53_zone_name}",
  10. ]
  11. tags = "${local.tags}"
  12. }
  13. # Define my loadbalancer
  14. resource "aws_lb" "main" {
  15. name = "${var.name}"
  16. subnets = [ "${values(local.public_subnets)}" ]
  17. security_groups = ["${module.alb_https_sg.this_security_group_id}", "${module.alb_http_sg.this_security_group_id}"]
  18. }
  19. resource "aws_lb_target_group" "main" {
  20. name = "${var.name}"
  21. port = "${var.app_port}"
  22. protocol = "HTTP"
  23. vpc_id = "${local.vpc_id}"
  24. target_type = "ip"
  25. depends_on = [ "aws_lb.main" ]
  26. }
  27. # Redirect all traffic from the ALB to the target group
  28. resource "aws_lb_listener" "main" {
  29. load_balancer_arn = "${aws_lb.main.id}"
  30. port = "80"
  31. protocol = "HTTP"
  32. default_action {
  33. target_group_arn = "${aws_lb_target_group.main.id}"
  34. type = "forward"
  35. }
  36. }
  37. resource "aws_lb_listener" "main-tls" {
  38. load_balancer_arn = "${aws_lb.main.id}"
  39. port = "443"
  40. protocol = "HTTPS"
  41. certificate_arn = "${module.acm.this_acm_certificate_arn}"
  42. default_action {
  43. target_group_arn = "${aws_lb_target_group.main.id}"
  44. type = "forward"
  45. }
  46. }

我必須承認,我在這裡搞砸了好幾次。我不得不在 AWS 控制臺中四處翻弄,以弄清楚我做錯了什麼。這當然不是一個“輕鬆”的過程,而且我之前已經做過很多次了。老實說,在這一點上,Kubernetes 看起來對我很有啟發性,但我意識到這是因為我對它非常熟悉。幸運的是我能夠使用託管的 Kubernetes 平臺(預裝了 external-dns 和 cert-manager),我真的很想知道我漏掉了 Fargate 什麼增值的地方。它真的感覺不那麼簡單。

經過一番折騰,我現在有一個可以工作的 ECS 服務。包括服務在內的最終定義如下所示:

  1. data "aws_ecs_task_definition" "app" {
  2. task_definition = "${var.name}"
  3. depends_on = ["aws_ecs_task_definition.app"]
  4. }
  5. resource "aws_ecs_service" "app" {
  6. name = "${var.name}"
  7. cluster = "${module.ecs.this_ecs_cluster_id}"
  8. task_definition = "${data.aws_ecs_task_definition.app.family}:${max(aws_ecs_task_definition.app.revision, data.aws_ecs_task_definition.app.revision)}"
  9. desired_count = "${var.ecs_service_desired_count}"
  10. launch_type = "FARGATE"
  11. deployment_maximum_percent = "${var.ecs_service_deployment_maximum_percent}"
  12. deployment_minimum_healthy_percent = "${var.ecs_service_deployment_minimum_healthy_percent}"
  13. network_configuration {
  14. subnets = ["${values(local.private_subnets)}"]
  15. security_groups = ["${module.app_sg.this_security_group_id}"]
  16. }
  17. load_balancer {
  18. target_group_arn = "${aws_lb_target_group.main.id}"
  19. container_name = "app"
  20. container_port = "${var.app_port}"
  21. }
  22. depends_on = [
  23. "aws_lb_listener.main",
  24. ]
  25. }

我覺得我已經接近完成了,但後來我記起了我只完成了最初的“入門”檔案中所需的 3 個步驟中的 2 個,我仍然需要定義 ECS 群集。

叢集

感謝 定義模組[9],定義要所有這些執行的叢集實際上非常簡單。

  1. module "ecs" {
  2. source = "terraform-aws-modules/ecs/aws"
  3. version = "v1.1.0"
  4. name = "${var.name}"
  5. }

這裡讓我感到驚訝的是為什麼我必須定義一個叢集。作為一個相當熟悉 ECS 的人,你會覺得你需要一個叢集,但我試圖從一個必須經歷這個過程的新人的角度來考慮這一點 —— 對我來說,Fargate 標榜自己“ Serverless”而你仍需要定義叢集,這似乎很令人驚訝。當然這是一個小細節,但它確實盤旋在我的腦海裡。

告訴我你的 Secret

在這個階段,我很高興我成功地運行了一些東西。然而,我的原始的成功標準缺少一些東西。如果我們回到任務定義那裡,你會記得我的應用程式有一個存放密碼的環境變數:

  1. container_environment_variables = [
  2. {
  3. name = "USER"
  4. value = "${var.user}"
  5. },
  6. {
  7. name = "PASSWORD"
  8. value = "${var.password}"
  9. }
  10. ]

如果我在 AWS 控制臺中檢視我的任務定義,我的密碼就在那裡,明晃晃的明文。我希望不要這樣,所以我開始嘗試將其轉化為其他東西,類似於 Kubernetes 的Secrets管理[10]

AWS SSM

Fargate / ECS 執行secret 管理secret management部分的方式是使用 AWS SSM[11](此服務的全名是 AWS 系統管理器引數儲存庫 Systems Manager Parameter Store,但我不想使用這個名稱,因為坦率地說這個名字太愚蠢了)。

AWS 檔案很好的涵蓋了這個內容[12],因此我開始將其轉換為 terraform。

指定秘密資訊

首先,你必須定義一個引數併為其命名。在 terraform 中,它看起來像這樣:

  1. resource "aws_ssm_parameter" "app_password" {
  2. name = "${var.app_password_param_name}" # The name of the value in AWS SSM
  3. type = "SecureString"
  4. value = "${var.app_password}" # The actual value of the password, like correct-horse-battery-stable
  5. }

顯然,這裡的關鍵部分是 “SecureString” 型別。這會使用預設的 AWS KMS 金鑰來加密資料,這對我來說並不是很直觀。這比 Kubernetes 的 Secret 管理具有巨大優勢,預設情況下,這些 Secret 在 etcd 中是不加密的。

然後我為 ECS 指定了另一個本地值對映,並將其作為 Secret 引數傳遞:

  1. container_secrets = [
  2. {
  3. name = "PASSWORD"
  4. valueFrom = "${var.app_password_param_name}"
  5. },
  6. ]
  7. module "container_definition_app" {
  8. source = "cloudposse/ecs-container-definition/aws"
  9. version = "v0.7.0"
  10. container_name = "${var.name}"
  11. container_image = "${var.image}"
  12. container_cpu = "${var.ecs_task_cpu}"
  13. container_memory = "${var.ecs_task_memory}"
  14. container_memory_reservation = "${var.container_memory_reservation}"
  15. port_mappings = [
  16. {
  17. containerPort = "${var.app_port}"
  18. hostPort = "${var.app_port}"
  19. protocol = "tcp"
  20. },
  21. ]
  22. environment = "${local.container_environment_variables}"
  23. secrets = "${local.container_secrets}"

出了個問題

此刻,我重新部署了我的任務定義,並且非常困惑。為什麼任務沒有正確拉起?當新的任務定義(版本 8)可用時,我一直在控制臺中看到正在執行的應用程式仍在使用先前的任務定義(版本 7)。解決這件事花費的時間比我預期的要長,但是在控制檯的事件螢幕上,我註意到了 IAM 錯誤。我錯過了一個步驟,容器無法從 AWS SSM 中讀取 Secret 資訊,因為它沒有正確的 IAM 許可權。這是我第一次真正對整個這件事情感到沮喪。從使用者體驗的角度來看,這裡的反饋非常糟糕。如果我沒有發覺的話,我會認為一切都很好,因為仍然有一個任務正在執行,我的應用程式仍然可以透過正確的 URL 訪問 —— 只不過是舊的配置而已。

在 Kubernetes 裡,我會清楚地看到 pod 定義中的錯誤。Fargate 可以確保我的應用不會停止,這絕對是太棒了,但作為一名運維,我需要一些關於發生了什麼的實際反饋。這真的不夠好。我真的希望 Fargate 團隊的人能夠讀到這篇文章,改善這種體驗。

就這樣了

到這裡就結束了,我的應用程式正在執行,也符合我的所有標準。我確實意識到我做了一些改進,其中包括:

◈ 定義一個 cloudwatch 日誌組,這樣我就可以正確地寫日誌了
◈ 添加了一個 route53 託管區域,使整個事情從 DNS 角度更容易自動化
◈ 修複並重新調整了 IAM 許可權,這裡太寬泛了

但老實說,現在我想反思一下這段經歷。我寫了一個關於我的經歷的 推特會話[13],然後花了其餘時間思考我在這裡的真實感受。

代價

經過一夜的反思,我意識到無論你是使用 Fargate 還是 Kubernetes,這個過程都大致相同。最讓我感到驚訝的是,儘管我經常聽說 Fargate “更容易”,但我真的沒有看到任何超過 Kubernetes 平臺的好處。現在,如果你正在構建 Kubernetes 叢集,我絕對可以看到這裡的價值 —— 管理節點和控制面板只是不必要的開銷,問題是 —— 基於 Kubernetes 的平臺的大多數消費者都沒有這樣做。如果你很幸運能夠使用 GKE,你幾乎不需要考慮叢集的管理,你可以使用單個 gcloud 命令來執行叢集。我經常使用 Digital Ocean 的 Kubernetes 託管服務,我可以肯定地說它就像操作 Fargate 叢集一樣簡單,實際上在某種程度上它更容易。

必須定義一些基礎設施來執行你的容器就是此時的代價。谷歌本週可能剛剛使用他們的 Google Cloud Run[14] 產品改變了遊戲規則,但他們在這一領域的領先優勢遠遠領先於其他所有人。

從這整個經歷中,我可以肯定的說:大規模執行容器仍然很難。它需要思考,需要領域知識,需要運維和開發人員之間的協作。它還需要一個基礎來構建 —— 任何基於 AWS 的操作都需要事先定義和執行一些基礎架構。我對一些公司似乎渴望的 “NoOps” 概念非常感興趣。我想如果你正在執行一個無狀態應用程式,你可以把它全部放在一個 lambda 函式和一個 API 閘道器中,這可能不錯,但我們是否真的適合在任何一種企業環境中這樣做?我真的不這麼認為。

公平比較

令我印象深刻的另一個現實是,技術 A 和技術 B 之間的比較通常不太公平,我經常在 AWS 上看到這一點。這種實際情況往往與 Jeff Barr 部落格文章截然不同。如果你是一家足夠小的公司,你可以使用 AWS 控制檯在 AWS 中部署你的應用程式並接受所有預設值,這絕對更容易。但是,我不想使用預設值,因為預設值幾乎是不適用於生產環境的。一旦你開始剝離掉雲服務商服務的層面,你就會開始意識到最終你仍然是在執行軟體 —— 它仍然需要設計良好、部署良好、執行良好。我相信 AWS 和 Kubernetes 以及所有其他雲服務商的增值服務使得它更容易執行、設計和操作,但它絕對不是免費的。

Kubernetes 的爭議

最後就是:如果你將 Kubernetes 純粹視為一個容器編排工具,你可能會喜歡 Fargate。然而,隨著我對 Kubernetes 越來越熟悉,我開始意識到它作為一種技術的重要性 —— 不僅因為它是一個偉大的容器編排工具,而且因為它的設計樣式 —— 它是宣告性的、API 驅動的平臺。在整個Fargate 過程期間發生的一個簡單的事情是,如果我刪除這裡某個東西,Fargate 不一定會為我重新建立它。自動縮放很不錯,不需要管理伺服器和作業系統的補丁及更新也很棒,但我覺得因為無法使用 Kubernetes 自我修複和 API 驅動模型而失去了很多。當然,Kubernetes 有一個學習曲線,但從這裡的體驗來看,Fargate 也是如此。

總結

儘管我在這個過程中遭遇了困惑,但我確實很喜歡這種體驗。我仍然相信 Fargate 是一項出色的技術,AWS 團隊對 ECS/Fargate 所做的工作確實非常出色。然而,我的觀點是,這絕對不比 Kubernetes “更容易”,只是……難點不同。

在生產環境中執行容器時出現的問題大致相同。如果你從這篇文章中有所收穫,它應該是這樣的:不管你選擇的哪種方式都有運維開銷。不要相信你選擇一些東西你的世界就變得更輕鬆。我個人的意見是:如果你有一個運維團隊,而你的公司要為多個應用程式團隊部署容器 —— 選擇一種技術並圍繞它構建流程和工具以使其更容易。

人們說的一點肯定沒錯,用點技巧可以更容易地使用某種技術。在這個階段,談到 Fargate,下麵的漫畫這總結了我的感受:

已同步到看一看
贊(0)

分享創造快樂