破除對 AWS Fargate 的幻覺
我在 $work 建立了一個基於 Kubernetes 的平台已經快一年了,而且有點像 Kubernetes 的佈道者了。真的,我認為這項技術太棒了。然而我並沒有對它的運營和維護的困難程度抱過什麼幻想。今年早些時候我讀了這樣的一篇文章,並對其中某些觀點深以為然。如果我在一家規模較小的、有 10 到 15 個工程師的公司,假如有人建議管理和維護一批 Kubernetes 集群,那我會感到害怕的,因為它的運維開銷太高了!
儘管我現在對 Kubernetes 的一切都很感興趣,但我仍然對「 無伺服器 」計算會消滅運維工程師的說法抱有好奇。這種好奇心主要來源於我希望在未來仍然能有一份有報酬的工作 —— 如果我們前景光明的未來不需要運維工程師,那我得明白到底是怎麼回事。我已經在 Lamdba 和Google Cloud Functions 上做了一些實驗,結果讓我印象十分深刻,但我仍然堅信無伺服器解決方案只是解決了一部分問題。
我關注 AWS Fargate 已經有一段時間了,這就是 $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 文檔並瀏覽 入門 文檔,這些就馬上就展現在了我面前:
等等,只是讓我的容器運行就至少要有三個步驟?這完全不像我所想的,不過還是讓我們開始吧。
任務定義
「 任務定義 」用來定義要運行的實際容器。我在這裡遇到的問題是,任務定義這件事非常複雜。這裡有很多選項都很簡單,比如指定 Docker 鏡像和內存限制,但我還必須定義一個網路模型以及我並不熟悉的其它各種選項。真需要這樣嗎?如果我完全沒有 AWS 方面的知識就進入到這個過程里,那麼在這個階段我會感覺非常的不知所措。可以在 AWS 頁面上找到這些 參數 的完整列表,這個列表很長。我知道我的容器需要一些環境變數,它需要暴露一個埠。所以我首先在一個神奇的 terraform 模塊 的幫助下定義了這一點,這真的讓這件事更容易了。如果沒有這個模塊,我就得手寫 JSON 來定義我的容器定義。
首先我定義了一些環境變數:
container_environment_variables = [
{
name = "USER"
value = "${var.user}"
},
{
name = "PASSWORD"
value = "${var.password}"
}
]
然後我使用上面提及的模塊組成了任務定義:
module "container_definition_app" {
source = "cloudposse/ecs-container-definition/aws"
version = "v0.7.0"
container_name = "${var.name}"
container_image = "${var.image}"
container_cpu = "${var.ecs_task_cpu}"
container_memory = "${var.ecs_task_memory}"
container_memory_reservation = "${var.container_memory_reservation}"
port_mappings = [
{
containerPort = "${var.app_port}"
hostPort = "${var.app_port}"
protocol = "tcp"
},
]
environment = "${local.container_environment_variables}"
}
在這一點上我非常困惑,我需要在這裡定義很多配置才能運行,而這時什麼都沒有開始呢,但這是必要的 —— 運行 Docker 容器肯定需要了解一些容器配置的知識。我 之前寫過 關於 Kubernetes 和配置管理的問題的文章,在這裡似乎遇到了同樣的問題。
接下來,我在上面的模塊中定義了任務定義(幸好從我這裡抽象出了所需的 JSON —— 如果我不得不手寫JSON,我可能已經放棄了)。
當我定義模塊參數時,我突然意識到我漏掉了一些東西。我需要一個 IAM 角色!好吧,讓我來定義:
resource "aws_iam_role" "ecs_task_execution" {
name = "${var.name}-ecs_task_execution"
assume_role_policy = <<EOF
{
"Version": "2008-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Effect": "Allow"
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
count = "${length(var.policies_arn)}"
role = "${aws_iam_role.ecs_task_execution.id}"
policy_arn = "${element(var.policies_arn, count.index)}"
}
這樣做是有意義的,我需要在 Kubernetes 中定義一個 RBAC 策略,所以在這裡我還未完全錯失或獲得任何東西。這時我開始覺得從 Kubernetes 的角度來看,這種感覺非常熟悉。
resource "aws_ecs_task_definition" "app" {
family = "${var.name}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "${var.ecs_task_cpu}"
memory = "${var.ecs_task_memory}"
execution_role_arn = "${aws_iam_role.ecs_task_execution.arn}"
task_role_arn = "${aws_iam_role.ecs_task_execution.arn}"
container_definitions = "${module.container_definition_app.json}"
}
現在,為了運行起來我已經寫了很多行代碼,我閱讀了很多 ECS 文檔,我所做的就是定義一個任務定義。我還沒有讓這個東西運行起來。在這一點上,我真的很困惑這個基於 Kubernetes 的平台到底增值了什麼,但我繼續前行。
服務
服務,一定程度上是將容器如何暴露給外部,另外是如何定義它擁有的副本數量。我的第一個想法是「啊!這就像一個 Kubernetes 服務!」我開始著手映射埠等。這是我第一次在 terraform 上跑:
resource "aws_ecs_service" "app" {
name = "${var.name}"
cluster = "${module.ecs.this_ecs_cluster_id}"
task_definition = "${data.aws_ecs_task_definition.app.family}:${max(aws_ecs_task_definition.app.revision, data.aws_ecs_task_definition.app.revision)}"
desired_count = "${var.ecs_service_desired_count}"
launch_type = "FARGATE"
deployment_maximum_percent = "${var.ecs_service_deployment_maximum_percent}"
deployment_minimum_healthy_percent = "${var.ecs_service_deployment_minimum_healthy_percent}"
network_configuration {
subnets = ["${values(local.private_subnets)}"]
security_groups = ["${module.app.this_security_group_id}"]
}
}
當我必須定義允許訪問所需埠的安全組時,我再次感到沮喪,當我這樣做了並將其插入到網路配置中後,我就像被扇了一巴掌。
我還需要定義自己的負載均衡器?
什麼?
當然不是嗎?
負載均衡器從未遠離
老實說,我很滿意,我甚至不確定為什麼。我已經習慣了 Kubernetes 的服務和 Ingress 對象,我一心認為用 Kubernetes 將我的應用程序放到網上是多麼容易。當然,我們在 $work 花了幾個月的時間建立一個平台,以便更輕鬆。我是 external-dns 和 cert-manager 的重度用戶,它們可以自動填充 Ingress 對象上的 DNS 條目並自動化 TLS 證書,我非常了解進行這些設置所需的工作,但老實說,我認為在 Fargate 上做這件事會更容易。我認識到 Fargate 並沒有聲稱自己是「如何運行應用程序」這件事的全部和最終目的,它只是抽象出節點管理,但我一直被告知這比 Kubernetes 更加容易。我真的很驚訝。定義負載均衡器(即使你不想使用 Ingress 和 Ingress Controller)也是向 Kubernetes 部署服務的重要組成部分,我不得不在這裡再次做同樣的事情。這一切都讓人覺得如此熟悉。
我現在意識到我需要:
- 一個負載均衡器
- 一個 TLS 證書
- 一個 DNS 名字
所以我著手做了這些。我使用了一些流行的 terraform 模塊,並做了這個:
# Define a wildcard cert for my app
module "acm" {
source = "terraform-aws-modules/acm/aws"
version = "v1.1.0"
create_certificate = true
domain_name = "${var.route53_zone_name}"
zone_id = "${data.aws_route53_zone.this.id}"
subject_alternative_names = [
"*.${var.route53_zone_name}",
]
tags = "${local.tags}"
}
# Define my loadbalancer
resource "aws_lb" "main" {
name = "${var.name}"
subnets = [ "${values(local.public_subnets)}" ]
security_groups = ["${module.alb_https_sg.this_security_group_id}", "${module.alb_http_sg.this_security_group_id}"]
}
resource "aws_lb_target_group" "main" {
name = "${var.name}"
port = "${var.app_port}"
protocol = "HTTP"
vpc_id = "${local.vpc_id}"
target_type = "ip"
depends_on = [ "aws_lb.main" ]
}
# Redirect all traffic from the ALB to the target group
resource "aws_lb_listener" "main" {
load_balancer_arn = "${aws_lb.main.id}"
port = "80"
protocol = "HTTP"
default_action {
target_group_arn = "${aws_lb_target_group.main.id}"
type = "forward"
}
}
resource "aws_lb_listener" "main-tls" {
load_balancer_arn = "${aws_lb.main.id}"
port = "443"
protocol = "HTTPS"
certificate_arn = "${module.acm.this_acm_certificate_arn}"
default_action {
target_group_arn = "${aws_lb_target_group.main.id}"
type = "forward"
}
}
我必須承認,我在這裡搞砸了好幾次。我不得不在 AWS 控制台中四處翻弄,以弄清楚我做錯了什麼。這當然不是一個「輕鬆」的過程,而且我之前已經做過很多次了。老實說,在這一點上,Kubernetes 看起來對我很有啟發性,但我意識到這是因為我對它非常熟悉。幸運的是我能夠使用託管的 Kubernetes 平台(預裝了 external-dns 和 cert-manager),我真的很想知道我漏掉了 Fargate 什麼增值的地方。它真的感覺不那麼簡單。
經過一番折騰,我現在有一個可以工作的 ECS 服務。包括服務在內的最終定義如下所示:
data "aws_ecs_task_definition" "app" {
task_definition = "${var.name}"
depends_on = ["aws_ecs_task_definition.app"]
}
resource "aws_ecs_service" "app" {
name = "${var.name}"
cluster = "${module.ecs.this_ecs_cluster_id}"
task_definition = "${data.aws_ecs_task_definition.app.family}:${max(aws_ecs_task_definition.app.revision, data.aws_ecs_task_definition.app.revision)}"
desired_count = "${var.ecs_service_desired_count}"
launch_type = "FARGATE"
deployment_maximum_percent = "${var.ecs_service_deployment_maximum_percent}"
deployment_minimum_healthy_percent = "${var.ecs_service_deployment_minimum_healthy_percent}"
network_configuration {
subnets = ["${values(local.private_subnets)}"]
security_groups = ["${module.app_sg.this_security_group_id}"]
}
load_balancer {
target_group_arn = "${aws_lb_target_group.main.id}"
container_name = "app"
container_port = "${var.app_port}"
}
depends_on = [
"aws_lb_listener.main",
]
}
我覺得我已經接近完成了,但後來我記起了我只完成了最初的「入門」文檔中所需的 3 個步驟中的 2 個,我仍然需要定義 ECS 群集。
集群
感謝 定義模塊,定義要所有這些運行的集群實際上非常簡單。
module "ecs" {
source = "terraform-aws-modules/ecs/aws"
version = "v1.1.0"
name = "${var.name}"
}
這裡讓我感到驚訝的是為什麼我必須定義一個集群。作為一個相當熟悉 ECS 的人,你會覺得你需要一個集群,但我試圖從一個必須經歷這個過程的新人的角度來考慮這一點 —— 對我來說,Fargate 標榜自己「 Serverless」而你仍需要定義集群,這似乎很令人驚訝。當然這是一個小細節,但它確實盤旋在我的腦海里。
告訴我你的 Secret
在這個階段,我很高興我成功地運行了一些東西。然而,我的原始的成功標準缺少一些東西。如果我們回到任務定義那裡,你會記得我的應用程序有一個存放密碼的環境變數:
container_environment_variables = [
{
name = "USER"
value = "${var.user}"
},
{
name = "PASSWORD"
value = "${var.password}"
}
]
如果我在 AWS 控制台中查看我的任務定義,我的密碼就在那裡,明晃晃的明文。我希望不要這樣,所以我開始嘗試將其轉化為其他東西,類似於 Kubernetes 的Secrets管理。
AWS SSM
Fargate / ECS 執行 secret 管理 部分的方式是使用 AWS SSM(此服務的全名是 AWS 系統管理器參數存儲庫 Systems Manager Parameter Store,但我不想使用這個名稱,因為坦率地說這個名字太愚蠢了)。
AWS 文檔很好的涵蓋了這個內容,因此我開始將其轉換為 terraform。
指定秘密信息
首先,你必須定義一個參數並為其命名。在 terraform 中,它看起來像這樣:
resource "aws_ssm_parameter" "app_password" {
name = "${var.app_password_param_name}" # The name of the value in AWS SSM
type = "SecureString"
value = "${var.app_password}" # The actual value of the password, like correct-horse-battery-stable
}
顯然,這裡的關鍵部分是 「SecureString」 類型。這會使用默認的 AWS KMS 密鑰來加密數據,這對我來說並不是很直觀。這比 Kubernetes 的 Secret 管理具有巨大優勢,默認情況下,這些 Secret 在 etcd 中是不加密的。
然後我為 ECS 指定了另一個本地值映射,並將其作為 Secret 參數傳遞:
container_secrets = [
{
name = "PASSWORD"
valueFrom = "${var.app_password_param_name}"
},
]
module "container_definition_app" {
source = "cloudposse/ecs-container-definition/aws"
version = "v0.7.0"
container_name = "${var.name}"
container_image = "${var.image}"
container_cpu = "${var.ecs_task_cpu}"
container_memory = "${var.ecs_task_memory}"
container_memory_reservation = "${var.container_memory_reservation}"
port_mappings = [
{
containerPort = "${var.app_port}"
hostPort = "${var.app_port}"
protocol = "tcp"
},
]
environment = "${local.container_environment_variables}"
secrets = "${local.container_secrets}"
出了個問題
此刻,我重新部署了我的任務定義,並且非常困惑。為什麼任務沒有正確拉起?當新的任務定義(版本 8)可用時,我一直在控制台中看到正在運行的應用程序仍在使用先前的任務定義(版本 7)。解決這件事花費的時間比我預期的要長,但是在控制台的事件屏幕上,我注意到了 IAM 錯誤。我錯過了一個步驟,容器無法從 AWS SSM 中讀取 Secret 信息,因為它沒有正確的 IAM 許可權。這是我第一次真正對整個這件事情感到沮喪。從用戶體驗的角度來看,這裡的反饋非常糟糕。如果我沒有發覺的話,我會認為一切都很好,因為仍然有一個任務正在運行,我的應用程序仍然可以通過正確的 URL 訪問 —— 只不過是舊的配置而已。
在 Kubernetes 里,我會清楚地看到 pod 定義中的錯誤。Fargate 可以確保我的應用不會停止,這絕對是太棒了,但作為一名運維,我需要一些關於發生了什麼的實際反饋。這真的不夠好。我真的希望 Fargate 團隊的人能夠讀到這篇文章,改善這種體驗。
就這樣了
到這裡就結束了,我的應用程序正在運行,也符合我的所有標準。我確實意識到我做了一些改進,其中包括:
- 定義一個 cloudwatch 日誌組,這樣我就可以正確地寫日誌了
- 添加了一個 route53 託管區域,使整個事情從 DNS 角度更容易自動化
- 修復並重新調整了 IAM 許可權,這裡太寬泛了
但老實說,現在我想反思一下這段經歷。我寫了一個關於我的經歷的 推特會話,然後花了其餘時間思考我在這裡的真實感受。
代價
經過一夜的反思,我意識到無論你是使用 Fargate 還是 Kubernetes,這個過程都大致相同。最讓我感到驚訝的是,儘管我經常聽說 Fargate 「更容易」,但我真的沒有看到任何超過 Kubernetes 平台的好處。現在,如果你正在構建 Kubernetes 集群,我絕對可以看到這裡的價值 —— 管理節點和控制面板只是不必要的開銷,問題是 —— 基於 Kubernetes 的平台的大多數消費者都沒有這樣做。如果你很幸運能夠使用 GKE,你幾乎不需要考慮集群的管理,你可以使用單個 gcloud
命令來運行集群。我經常使用 Digital Ocean 的 Kubernetes 託管服務,我可以肯定地說它就像操作 Fargate 集群一樣簡單,實際上在某種程度上它更容易。
必須定義一些基礎設施來運行你的容器就是此時的代價。谷歌本周可能剛剛使用他們的 Google Cloud Run 產品改變了遊戲規則,但他們在這一領域的領先優勢遠遠領先於其他所有人。
從這整個經歷中,我可以肯定的說:大規模運行容器仍然很難。它需要思考,需要領域知識,需要運維和開發人員之間的協作。它還需要一個基礎來構建 —— 任何基於 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,下面的漫畫這總結了我的感受:
via: https://leebriggs.co.uk/blog/2019/04/13/the-fargate-illusion.html
作者:Lee Briggs 選題:lujun9972 譯者:Bestony 校對:wxy, 臨石(阿里雲智能技術專家)
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive