配置語言的黃金時代

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我認爲我們當前所認知的DevOps即將走到盡頭。至少,其中的Ops會如此。隨着雲基礎設施成爲應用程序關注的重點,越來越多的ops任務由雲本身完成或內置於應用程序中。剩下的就是供應和管理應用程序所需的基礎設施。這關係到所有的相關附屬內容,例如安全性和網絡。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於一般用例來說,所有這些正在形成的模式都將使基礎設施供應與應用程序本身難以區分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,在大多數公司中,傳統的IT團隊已經將自己更名爲DevOps,並廣泛採用AWS,而不是本地的VMWare集羣。他們使用Terraform而不是bash腳本,並且通常更爲敏捷,採用了許多開發實踐。他們都是些熟悉網絡的專業人員,瞭解IAM在AWS中的工作方式。他們負責搭建網絡和基礎設施環境,保障其安全性,並將其移交給使用它們的應用程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"開發人員多半會覺得這樣很不錯,因爲他們不想學習AWS IAM或VPC的複雜之處。我回想起了在2000年進入這個行業時如何處理數據庫的方方面面。那時,應用程序不會涉及數據庫的結構,由DBA在生產系統上運行數據庫腳本。這些腳本將創建數據庫、表、索引,這差不多是整個數據庫結構了。然後,開發人員將這些映射到他們的代碼中,只要在確定的模式(由其他人管理)上運行該應用程序,則執行DML。如今,我對基礎設施有相同的看法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只要ORM和其他各種框架開始管理這一領域,這些問題就會被捆綁到應用程序中。沒有理由認爲在基礎設施上不會發生同樣的事情。唯一的問題是,我們沒有合適的框架來做這件事,但現在我們正在開始得到它們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大規模處理應用程序基礎設施(我認爲這與管理廣告、電子郵件、金融系統之類的核心業務服務基礎設施不同)的需求出現在虛擬化時代,始於CFEngine。向那些不瞭解CFEngine的人介紹一下,CFEngine是我們今天所要了解的配置管理系統中的第一批產品其中的一款,在它之後是Puppet、Chef和其他的配置管理系統。CFEngine出現的年代,大多數供應都是手工完成的,或者是按照現有文檔的一步一步的操作說明來完成的。該提議的核心是馬克·伯吉斯(Mark Burgess)撰寫的一份白皮書"},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fn:1","title":"","type":null},"content":[{"type":"text","text":"1"}]},{"type":"text","text":"。其理論的主線是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當今的計算機系統是脆弱的、不可靠的。在計算機系統運維的每個階段,都有人類參與維護和維修。這麼高度的人類參與在未來將不可能維持下去。同等複雜度甚至更復雜的生物和社會系統具有自我修復過程,這對其生存至關重要。如果我們未來的計算機系統想要在複雜而不利的環境中生存下去,就有必要模擬這樣的系統。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何構建能大規模運行並共享源自於免疫系統的一些思想的自我修復系統?爲破解該難題湧現出了第一批招法,CFEngine正是其中之一。爲了實現這一點,它祭出3個重要法寶:1)它使用DSL來描述所需的狀態,而不是過程式的語言。這將嘗試抽象組件,以使管理員能夠進行參數化和重用。2) CFEngine具有聚合語義,即描述一個系統應該是什麼樣子的,當系統處於那種狀態時CFEngine就變成惰性的。3)在幾個管理單元獨立工作、幾乎沒有機會交流的情況下,它可以防止任務重複和進程掛起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用這些特性,可以實現一個自校正和自修復系統,從而得到一個可保持容錯性的系統。現在有了AWS,我們可以通過利用多區域性的服務來設計一個表現有相同屬性的系統。從本質上講,如果精心設計,這些服務可以將這些屬性傳遞給應用程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此期間或不久之後,出現了許多其他工具,每一種工具的側重點是最初那份價值主張的不同方面。其中,可能最受歡迎的是Puppet和Chef。他們各有所長。在我就職的公司,我們使用Puppet來處理基礎設施配置,主要的原因是非編程人員更容易理解它。從系統管理員的視角來看,在不深入編碼的情況下完成某些工作是很具吸引力的。隨着時間的推移,這被證明是一個錯誤的選擇,它對於我們來說弊大於利。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Puppet有它自己的DSL、它自己的術語和特性。它有自己的工具生態系統、儀表板和擴展,可以幫助你在管理基礎設施方面走得很遠。它的優勢是處理各系統之間不需要分佈式協調的單獨的服務器系統。可以通過導出資源和PuppetDB在多個服務器之間進行協調,但對我來說,這總是讓人覺得很不爽(現在可能和當時有所不同了,但我已經好幾年沒有關注這個領域了)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"#https:\/\/github.com\/voxpupuli\/puppet-minecraft\/blob\/master\/manifests\/user.pp\n\nclass minecraft::user {\n group { $minecraft::group:\n ensure => present,\n system => true,\n }\n\n user { $minecraft::user:\n ensure => present,\n gid => $minecraft::group,\n home => $minecraft::install_dir,\n managehome => true,\n system => true,\n require => Group[$minecraft::group],\n }\n\n # Ensures deletion of install_dir does not break module, setup for plugins\n $dirs = [$minecraft::install_dir, \"${minecraft::install_dir}\/plugins\"]\n\n file { $dirs:\n ensure => directory,\n owner => $minecraft::user,\n group => $minecraft::group,\n require => User[$minecraft::user],\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在處理簡單的基礎設施組件時,這完全沒有問題,我們可以做大量的自動化來管理數百臺服務器。但像大多數事情一樣,讓你痛苦不堪的往往是一些極端情況。由於Puppet語言是一種DSL,簡單的問題開始變成大問題。比如,無法做到基本的for循環,甚至連字符串操作也做不到。大多數配置語言都存在這些問題。最後,你可能還會遇到這樣的情況:你需要擴展它們以涵蓋特定的用例,通常,要做到這一點,就需要編寫真正的代碼。如果不使用“真正的”語言,你能做的事情也就會十分有限,所以我們可能從一開始就應該選擇合適的做法。現在回想起來,也許Chef會是一個更好的選擇。至少使用它的時候,我學到的很多東西可以遷移到我職業生涯的其他地方(Chef使用ruby而不是他們自己的DSL)。我對Ansible和Salt沒有太多的經驗,但我覺得它們都有同樣的缺點和相似的優點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在配置語言的領域中,有一類稍微有所不同,那就是Terraform和AWS Cloud Formation。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"他們都借鑑了配置管理系統的遺風,並試圖與他們的內部觀點保持同步,即所見應該與事實相一致。除了表達意圖的方式(仍然使用DSL,而不是非常成熟的語言)之外,主要的區別在於它們的設計定位是雲提供商層。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正如Puppet和Chef非常擅長管理機器上的典型資源(服務、包、配置文件),Terraform和AWSCloud Formation非常擅長管理雲服務。它們是基於雲基礎設施的概念打造的。這並不意味着你不能使用Puppet、Chef和其他第二代配置語言做同樣的事情。雖說如此,但由於Terraform和AWSCloud Formation 非常快速地適應了雲的現實情況,再加上它們設法通過雲賺錢的方式等等原因,Terraform成爲了基於DSL的雲基礎設施管理領域無可爭議的王者,而Cloud Formation則在AWS領域獨霸天下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有這些工具都採納了CFEngine中最好的地方,其中最重要的是收斂狀態的概念。他們會把你表達的意圖,與機器進行比較,找出任何依賴關係和步驟順序,使資源達到它想要的狀態。通常,它們還包含一個編譯階段,在此階段,它們將DSL映射到內部邏輯並創建執行計劃。這還將捕捉基本的錯誤。這些都是經過實踐檢驗過的好想法,現在已經成爲處理基礎設施的默認方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,隨着我們的進步,我們消費和處理基礎設施組件的方式正在發生根本性的變化。現在你可以利用AWS服務了。你可以構建一個非常複雜的應用程序,使用CloudFront來進行靜態內容分發,使用Lambda的API網關來構建API路由並向其添加業務功能,可以通過Cognito來處理身份管理。在後臺,這些Lambda函數可以與整個基礎設施生態系統直接交互,如RDS或DynamoDB。你可以通過Redshift與分析系統交互,也可以通過QuickSight展示可視化數據。除了使用AWS EMR或Glue處理具有步驟函數的工作流驅動、異步批處理、ETL任務之外,還可以由Lambda處理後臺任務。由於AWS服務很好地集成在一起,只需相對較少的粘合代碼即可組合這些服務以實現巨大的業務價值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當前,正在湧現出新的應用程序類別,它們試圖更好地適應這種環境。比方說,由AWS稱之爲無服務器的這一類。使用Terraform 或Cloud Formation爲這些類型的應用提供服務可能不會那麼順暢。在這樣一個緊密集成的模型中,你的基礎設施將隨着應用程序的發展而發展,因此這可能是最重要的應用程序關注點之一。AWS在這裏推行的是SAM模型,但我可以預見到未來類似Ruby-on-Rails或Django的框架將把AWS基礎設施視爲數據庫。那麼,像“rails migrate”之類的對雲會有什麼影響呢?它對數據模式會有什麼影響呢?這對於Rails開發人員來說,可能相當不錯。無論是好是壞,我認爲我們正在沿着這條路走下去。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼我們做到了嗎?我不這麼想。隨着一些常見抽象概念塵埃落定,你在所有云中可以使用它們了,我們纔可能會做到。大家做過一些嘗試,比如go 世界的“go-cloud”,它試圖在最常見的標準之上構建一個抽象,但這是一場艱苦的戰鬥。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前,我把賭注押在Pulumi和他們的自動化api之上。讓我們稍稍回溯一下。Pulumi是一個框架(你可以稱它爲配置語言框架),它允許你用諸如javascript、typescript、python、go、c#之類的主流語言編寫代碼。它使用的仍然是與其他配置語言相同的概念,而且大多數支持實際上是建立在Terraform之上的。它真正有趣的是,既然你在寫代碼,就真的是在寫代碼。你可以利用你喜歡的所有包和你喜歡的所有編程範例,以及語言生態系統中的IDE和構建工具。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"他們在網站上展示的第一個例子是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"\/\/ Create a serverless REST API\nimport * as awsx from \"@pulumi\/awsx\";\n\n\/\/ Serve a simple REST API at `GET \/hello`.\nlet app = new awsx.apigateway.API(\"my-app\", {\n routes: [{\n path: \"\/hello\",\n method: \"GET\",\n eventHandler: async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify({ hello: \"World!\" }),\n };\n },\n }],\n});\n\nexport let url = app.url;\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它所做的,是在AWS中創建一個帶有“\/hello”路由的API網關,並將其代理給一個用javascript編寫的AWS Lambda函數。這個lambda函數只返回200編碼和一個HTML體,其中包含一個JSON對象,內容爲:{hello: \"World\"!}。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓我們來看另一個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"import * as pulumi from \"@pulumi\/pulumi\";\nimport * as aws from \"@pulumi\/aws\";\nimport * as awsx from \"@pulumi\/awsx\";\n\nexport = async () => {\n const config = new pulumi.Config(\"aws\");\n const providerOpts = { provider: new aws.Provider(\"prov\", { region: config.require(\"envRegion\") }) };\n\n const vpc = awsx.ec2.Vpc.getDefault(providerOpts);\n\n \/\/ Create a security group to let traffic flow.\n const sg = new awsx.ec2.SecurityGroup(\"web-sg\", { vpc }, providerOpts);\n\n const ipv4egress = sg.createEgressRule(\"ipv4-egress\", {\n ports: new awsx.ec2.AllTraffic(),\n location: new awsx.ec2.AnyIPv4Location(),\n });\n const ipv6egress = sg.createEgressRule(\"ipv6-egress\", {\n ports: new awsx.ec2.AllTraffic(),\n location: new awsx.ec2.AnyIPv6Location(),\n });\n\n \/\/ Creates an ALB associated with the default VPC for this region and listen on port 80.\n const alb = new awsx.elasticloadbalancingv2.ApplicationLoadBalancer(\"web-traffic\",\n { vpc, external: true, securityGroups: [ sg ] }, providerOpts);\n const listener = alb.createListener(\"web-listener\", { port: 80 });\n\n \/\/ For each subnet, and each subnet\/zone, create a VM and a listener.\n const publicIps: pulumi.Output[] = [];\n const subnets = await vpc.publicSubnets;\n for (let i = 0; i < subnets.length; i++) {\n const getAmiResult = await aws.getAmi({\n filters: [\n { name: \"name\", values: [ \"ubuntu\/images\/hvm-ssd\/ubuntu-trusty-14.04-amd64-server-*\" ] },\n { name: \"virtualization-type\", values: [ \"hvm\" ] },\n ],\n mostRecent: true,\n owners: [ \"099720109477\" ], \/\/ Canonical\n }, { ...providerOpts, async: true });\n\n const vm = new aws.ec2.Instance(`web-${i}`, {\n ami: getAmiResult.id,\n instanceType: \"m5.large\",\n subnetId: subnets[i].subnet.id,\n availabilityZone: subnets[i].subnet.availabilityZone,\n vpcSecurityGroupIds: [ sg.id ],\n userData: `#!\/bin\/bash\n echo \"Hello World, from Server ${i+1}!\" > index.html\n nohup python -m SimpleHTTPServer 80 &`,\n }, providerOpts);\n publicIps.push(vm.publicIp);\n\n alb.attachTarget(\"target-\" + i, vm);\n }\n\n \/\/ Export the resulting URL so that it's easy to access.\n return { endpoint: listener.endpoint, publicIps: publicIps };\n};\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是一個更復雜的示例2("},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fn:2","title":"","type":null},"content":[{"type":"text","text":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fn:2"}]},{"type":"text","text":"),但是如果感覺太複雜,可以使用Fargate3("},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fn:3","title":"","type":null},"content":[{"type":"text","text":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fn:3"}]},{"type":"text","text":")作爲計算部分,這個版本會更簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓我們來看看它是做了什麼。首先構建一個內部的Pulumi上下文,以瞭解在AWS中使用哪個區域,之後,它將配置AWS VPC的網絡部分。這相當重要,因爲它非常體貼地考慮了所要做的(甚至是手工要做的)AWS內部網絡的領域知識。儘管如此,只需要一行代碼,即可乾淨利落地完成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"import * as pulumi from \"@pulumi\/pulumi\";\nimport * as aws from \"@pulumi\/aws\";\nimport * as awsx from \"@pulumi\/awsx\";\n\nexport = async () => {\n const config = new pulumi.Config(\"aws\");\n const providerOpts = { provider: new aws.Provider(\"prov\", { region: config.require(\"envRegion\") }) };\n\n const vpc = awsx.ec2.Vpc.getDefault(providerOpts);\n\n \/\/ Create a security group to let traffic flow.\n const sg = new awsx.ec2.SecurityGroup(\"web-sg\", { vpc }, providerOpts);\n\n const ipv4egress = sg.createEgressRule(\"ipv4-egress\", {\n ports: new awsx.ec2.AllTraffic(),\n location: new awsx.ec2.AnyIPv4Location(),\n });\n const ipv6egress = sg.createEgressRule(\"ipv6-egress\", {\n ports: new awsx.ec2.AllTraffic(),\n location: new awsx.ec2.AnyIPv6Location(),\n });\n\n \/\/ Creates an ALB associated with the default VPC for this region and listen on port 80.\n const alb = new awsx.elasticloadbalancingv2.ApplicationLoadBalancer(\"web-traffic\",\n { vpc, external: true, securityGroups: [ sg ] }, providerOpts);\n const listener = alb.createListener(\"web-listener\", { port: 80 });\n\n \/\/ For each subnet, and each subnet\/zone, create a VM and a listener.\n const publicIps: pulumi.Output[] = [];\n const subnets = await vpc.publicSubnets;\n for (let i = 0; i < subnets.length; i++) {\n const getAmiResult = await aws.getAmi({\n filters: [\n { name: \"name\", values: [ \"ubuntu\/images\/hvm-ssd\/ubuntu-trusty-14.04-amd64-server-*\" ] },\n { name: \"virtualization-type\", values: [ \"hvm\" ] },\n ],\n mostRecent: true,\n owners: [ \"099720109477\" ], \/\/ Canonical\n }, { ...providerOpts, async: true });\n\n const vm = new aws.ec2.Instance(`web-${i}`, {\n ami: getAmiResult.id,\n instanceType: \"m5.large\",\n subnetId: subnets[i].subnet.id,\n availabilityZone: subnets[i].subnet.availabilityZone,\n vpcSecurityGroupIds: [ sg.id ],\n userData: `#!\/bin\/bash\n echo \"Hello World, from Server ${i+1}!\" > index.html\n nohup python -m SimpleHTTPServer 80 &`,\n }, providerOpts);\n publicIps.push(vm.publicIp);\n\n alb.attachTarget(\"target-\" + i, vm);\n }\n\n \/\/ Export the resulting URL so that it's easy to access.\n return { endpoint: listener.endpoint, publicIps: publicIps };\n};\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一句執行完成之後,你將有一個默認的VPC,包括私有和公共子網,設置一個互聯網網關和路由表,這樣指定的公共子網就有它的默認路由(0.0.0.0)指向互聯網網關。當我們在公共子網中創建EC2實例時,它們將可以從internet訪問,並具有出站internet連接,而私有子網中的實例將只能在VPC中訪問,不可以訪問internet。這是AWS推薦的設置,默認情況下是安全的。接下來,它創建一個安全組(以及AWS EC2特性,它的工作原理類似於防火牆規則),只允許通過ipv6和ipv4向附加了安全組的資源發送web流量。具體在本例中,它將成爲一個負載均衡器(Application Load Balancer是AWS產品名稱,即應用程序負載均衡器)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":" \/\/ Create a security group to let traffic flow.\n const sg = new awsx.ec2.SecurityGroup(\"web-sg\", { vpc }, providerOpts);\n\n const ipv4egress = sg.createEgressRule(\"ipv4-egress\", {\n ports: new awsx.ec2.AllTraffic(),\n location: new awsx.ec2.AnyIPv4Location(),\n });\n const ipv6egress = sg.createEgressRule(\"ipv6-egress\", {\n ports: new awsx.ec2.AllTraffic(),\n location: new awsx.ec2.AnyIPv6Location(),\n });\n\n \/\/ Creates an ALB associated with the default VPC for this region and listen on port 80.\n const alb = new awsx.elasticloadbalancingv2.ApplicationLoadBalancer(\"web-traffic\",\n { vpc, external: true, securityGroups: [ sg ] }, providerOpts);\n const listener = alb.createListener(\"web-listener\", { port: 80 });\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們有了自己的負載均衡器,我們將等待子網的創建(參見await關鍵字)。一旦完成,我們就可以遍歷所有公共子網,並在每個子網中使用ubuntu AMI創建一個EC2實例。出於測試目的,我們將使用userData腳本注入一個小的bash腳本來創建HTML頁面。這將啓動一個python嵌入式web服務器來爲它提供服務。在這裏,我們可以做任何事情(例如,從s3獲取一個spring boot應用程序或者任何類型的應用程序並啓動和運行它)。最後,我們將把EC2實例附加到ELB上,這樣就完成了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"const subnets = await vpc.publicSubnets;\n for (let i = 0; i < subnets.length; i++) {\n const getAmiResult = await aws.getAmi({\n filters: [\n { name: \"name\", values: [ \"ubuntu\/images\/hvm-ssd\/ubuntu-trusty-14.04-amd64-server-*\" ] },\n { name: \"virtualization-type\", values: [ \"hvm\" ] },\n ],\n mostRecent: true,\n owners: [ \"099720109477\" ], \/\/ Canonical\n }, { ...providerOpts, async: true });\n\n const vm = new aws.ec2.Instance(`web-${i}`, {\n ami: getAmiResult.id,\n instanceType: \"m5.large\",\n subnetId: subnets[i].subnet.id,\n availabilityZone: subnets[i].subnet.availabilityZone,\n vpcSecurityGroupIds: [ sg.id ],\n userData: `#!\/bin\/bash\n echo \"Hello World, from Server ${i+1}!\" > index.html\n nohup python -m SimpleHTTPServer 80 &`,\n }, providerOpts);\n\n alb.attachTarget(\"target-\" + i, vm);\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"繼續,在本例中,我們創建了一個AWS VPC,並根據AWS最佳實踐配置了所有網絡。我們建立了一個負載均衡器,確保它不允許非期望的流量,在每個AWS可用性區域部署了幾個AWS EC2實例以獲得容錯性(這也是AWS的最佳實踐),然後部署了我們的網頁。不需要專門的教育、培訓,不需要啓用專門的人才,是麼?我們在沒有“DevOps”工程師的情況下做到了這一點。當然,與任何領域特定的框架一樣,需要一些該領域的知識,但是一旦你學習了一些SDK,雲與你正在使用的任何其他框架沒有什麼不同。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,所有這些都弄好了,但你如何將它融入到你自己的應用中呢?不幸的是,這個問題的答案仍在研究中。但是考慮到它很可能與應用程序使用相同的語言,所以讓所有東西都在相同的repo中會更合理。它仍然需要一個單獨的工具來運行(Pulumi),但你可以把它看作是該工具鏈中的另一個工具。如果是這樣的話,若不使用構建應用程序和在雲基礎設施中所用的程序語言,還有什麼意義呢?例如,如果我不得不使用一個單獨的工具,那麼它與使用Terraform並沒有什麼不同。這就是Pulumi自動化api的由來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"import { InlineProgramArgs, LocalWorkspace } from \"@pulumi\/pulumi\/x\/automation\";\nimport * as aws from \"@pulumi\/aws\";\nimport * as awsx from \"@pulumi\/awsx\";\nimport * as pulumi from \"@pulumi\/pulumi\";\nimport * as mysql from \"mysql\";\n\nconst process = require('process');\n\nconst args = process.argv.slice(2);\nlet destroy = false;\nif (args.length > 0 && args[0]) {\n destroy = args[0] === \"destroy\";\n}\n\n\n\nconst run = async () => {\n \/\/ This is our pulumi program in \"inline function\" form\n const pulumiProgram = async () => {\n const vpc = awsx.ec2.Vpc.getDefault();\n const subnetGroup = new aws.rds.SubnetGroup(\"dbsubnet\", {\n subnetIds: vpc.publicSubnetIds,\n });\n\n \/\/ make a public SG for our cluster for the migration\n const securityGroup = new awsx.ec2.SecurityGroup(\"publicGroup\", {\n egress: [\n {\n protocol: \"-1\",\n fromPort: 0,\n toPort: 0,\n cidrBlocks: [\"0.0.0.0\/0\"],\n }\n ],\n ingress: [\n {\n protocol: \"-1\",\n fromPort: 0,\n toPort: 0,\n cidrBlocks: [\"0.0.0.0\/0\"],\n }\n ]\n });\n\n \/\/ example only, you should change this\n const dbName = \"hellosql\";\n const dbUser = \"hellosql\";\n const dbPass = \"hellosql\";\n\n \/\/ provision our db\n const cluster = new aws.rds.Cluster(\"db\", {\n engine: \"aurora-mysql\",\n engineVersion: \"5.7.mysql_aurora.2.03.2\",\n databaseName: dbUser,\n masterUsername: dbName,\n masterPassword: dbPass,\n skipFinalSnapshot: true,\n dbSubnetGroupName: subnetGroup.name,\n vpcSecurityGroupIds: [securityGroup.id],\n });\n\n const clusterInstance = new aws.rds.ClusterInstance(\"dbInstance\", {\n clusterIdentifier: cluster.clusterIdentifier,\n instanceClass: \"db.t3.small\",\n engine: \"aurora-mysql\",\n engineVersion: \"5.7.mysql_aurora.2.03.2\",\n publiclyAccessible: true,\n dbSubnetGroupName: subnetGroup.name,\n });\n\n return {\n host: pulumi.interpolate`${cluster.endpoint}`,\n dbName,\n dbUser,\n dbPass\n };\n };\n\n \/\/ Create our stack \n const args: InlineProgramArgs = {\n stackName: \"dev\",\n projectName: \"databaseMigration\",\n program: pulumiProgram\n };\n\n \/\/ create (or select if one already exists) a stack that uses our inline program\n const stack = await LocalWorkspace.createOrSelectStack(args);\n\n console.info(\"successfully initialized stack\");\n console.info(\"installing plugins...\");\n await stack.workspace.installPlugin(\"aws\", \"v3.6.1\");\n console.info(\"plugins installed\");\n console.info(\"setting up config\");\n await stack.setConfig(\"aws:region\", { value: \"us-west-2\" });\n console.info(\"config set\");\n console.info(\"refreshing stack...\");\n await stack.refresh({ onOutput: console.info });\n console.info(\"refresh complete\");\n\n if (destroy) {\n console.info(\"destroying stack...\");\n await stack.destroy({ onOutput: console.info });\n console.info(\"stack destroy complete\");\n process.exit(0);\n }\n\n console.info(\"updating stack...\");\n const upRes = await stack.up({ onOutput: console.info });\n console.log(`update summary: \\n${JSON.stringify(upRes.summary.resourceChanges, null, 4)}`);\n console.log(`db host url: ${upRes.outputs.host.value}`);\n console.info(\"configuring db...\");\n\n \/\/ establish mysql client\n const connection = mysql.createConnection({\n host: upRes.outputs.host.value,\n user: upRes.outputs.dbUser.value,\n password: upRes.outputs.dbPass.value,\n database: upRes.outputs.dbName.value\n });\n\n connection.connect();\n\n console.log(\"creating table...\")\n\n \/\/ make sure the table exists\n connection.query(`\n CREATE TABLE IF NOT EXISTS hello_pulumi(\n id int(9) NOT NULL,\n color varchar(14) NOT NULL,\n PRIMARY KEY(id)\n );\n `, function (error, results, fields) {\n if (error) throw error;\n console.log(\"table created!\")\n console.log('Result: ', JSON.stringify(results));\n console.log(\"seeding initial data...\")\n });\n \n \/\/ seed the table with some data to start\n connection.query(`\n INSERT IGNORE INTO hello_pulumi (id, color)\n VALUES\n (1, 'Purple'),\n (2, 'Violet'),\n (3, 'Plum');\n `, function (error, results, fields) {\n if (error) throw error;\n console.log(\"rows inserted!\")\n console.log('Result: ', JSON.stringify(results));\n console.log(\"querying to veryify data...\")\n });\n\n \n \/\/ read the data back\n connection.query(`SELECT COUNT(*) FROM hello_pulumi;`, function (error, results, fields) {\n if (error) throw error;\n console.log('Result: ', JSON.stringify(results));\n console.log(\"database, tables, and rows successfuly configured!\")\n });\n\n connection.end();\n};\n\nrun().catch(err => console.log(err));\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就代碼行數而言,這個例子比較大,但要點是:我們是在相同的代碼庫中創建的基礎結構和表結構。我們來看一下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一部分負責AWS中的網絡設置,並創建一個允許所有訪問的安全組。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":" const vpc = awsx.ec2.Vpc.getDefault();\n const subnetGroup = new aws.rds.SubnetGroup(\"dbsubnet\", {\n subnetIds: vpc.publicSubnetIds,\n });\n\n \/\/ make a public SG for our cluster for the migration\n const securityGroup = new awsx.ec2.SecurityGroup(\"publicGroup\", {\n egress: [\n {\n protocol: \"-1\",\n fromPort: 0,\n toPort: 0,\n cidrBlocks: [\"0.0.0.0\/0\"],\n }\n ],\n ingress: [\n {\n protocol: \"-1\",\n fromPort: 0,\n toPort: 0,\n cidrBlocks: [\"0.0.0.0\/0\"],\n }\n ]\n });"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基礎設施部分的第二部分使用Aurora創建數據庫實例:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":" \/\/ example only, you should change this\n const dbName = \"hellosql\";\n const dbUser = \"hellosql\";\n const dbPass = \"hellosql\";\n\n \/\/ provision our db\n const cluster = new aws.rds.Cluster(\"db\", {\n engine: \"aurora-mysql\",\n engineVersion: \"5.7.mysql_aurora.2.03.2\",\n databaseName: dbUser,\n masterUsername: dbName,\n masterPassword: dbPass,\n skipFinalSnapshot: true,\n dbSubnetGroupName: subnetGroup.name,\n vpcSecurityGroupIds: [securityGroup.id],\n });\n\n const clusterInstance = new aws.rds.ClusterInstance(\"dbInstance\", {\n clusterIdentifier: cluster.clusterIdentifier,\n instanceClass: \"db.t3.small\",\n engine: \"aurora-mysql\",\n engineVersion: \"5.7.mysql_aurora.2.03.2\",\n publiclyAccessible: true,\n dbSubnetGroupName: subnetGroup.name,\n });\n\n return {\n host: pulumi.interpolate`${cluster.endpoint}`,\n dbName,\n dbUser,\n dbPass\n };\n };\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,所有這些都被封裝在Pulumi“棧”中,運行以創建基礎設施:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"\/\/ Create our stack \n const args: InlineProgramArgs = {\n stackName: \"dev\",\n projectName: \"databaseMigration\",\n program: pulumiProgram\n };\n\n \/\/ create (or select if one already exists) a stack that uses our inline program\n const stack = await LocalWorkspace.createOrSelectStack(args);\n\n \/\/load pulumi plugins\n await stack.workspace.installPlugin(\"aws\", \"v3.6.1\");\n \n \/\/set the aws region and logging\n await stack.setConfig(\"aws:region\", { value: \"us-west-2\" });\n await stack.refresh({ onOutput: console.info });\n\n \/\/in case we run the program with the destroy flag to remove everythig\n if (destroy) {\n console.info(\"destroying stack...\");\n await stack.destroy({ onOutput: console.info });\n console.info(\"stack destroy complete\");\n process.exit(0);\n }\n\n \/\/wait for the infra to be provisioned\n const upRes = await stack.up({ onOutput: console.info });\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一旦有了基礎設施,我們就可以開始使用它了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":" \/\/ establish mysql client\n const connection = mysql.createConnection({\n host: upRes.outputs.host.value,\n user: upRes.outputs.dbUser.value,\n password: upRes.outputs.dbPass.value,\n database: upRes.outputs.dbName.value\n });\n\n connection.connect();\n\n\n \/\/ make sure the table exists\n connection.query(`\n CREATE TABLE IF NOT EXISTS hello_pulumi(\n id int(9) NOT NULL,\n color varchar(14) NOT NULL,\n PRIMARY KEY(id)\n );\n `, function (error, results, fields) {\n if (error) throw error;\n console.log(\"table created!\")\n console.log('Result: ', JSON.stringify(results));\n console.log(\"seeding initial data...\")\n });\n \n \/\/ seed the table with some data to start\n connection.query(`\n INSERT IGNORE INTO hello_pulumi (id, color)\n VALUES\n (1, 'Purple'),\n (2, 'Violet'),\n (3, 'Plum');\n `, function (error, results, fields) {\n if (error) throw error;\n console.log(\"rows inserted!\")\n console.log('Result: ', JSON.stringify(results));\n console.log(\"querying to veryify data...\")\n });\n\n \n \/\/ read the data back\n connection.query(`SELECT COUNT(*) FROM hello_pulumi;`, function (error, results, fields) {\n if (error) throw error;\n console.log('Result: ', JSON.stringify(results));\n console.log(\"database, tables, and rows successfuly configured!\")\n });\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該模型真正的亮點是在創建“無服務器”應用程序的時候。在此,開發人員已經創建了非常好的抽象,它使工作流平滑地工作,並向我們展示了未來的方向:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"\/\/ Define a new GET endpoint backed by a lambda function and a static folder mapped to \/ which will be saved in s3.\nconst api = new awsx.apigateway.API(\"test\", {\n routes: [{\n path: \"\/\",\n localPath: \"www\",\n },\n {\n path: \"\/test\",\n method: \"GET\",\n eventHandler: async (event) => {\n \/\/ This code runs in an AWS Lambda anytime `\/test` is hit.\n return {\n statusCode: 200,\n body: \"Hello, API Gateway!\",\n };\n },\n }],\n})\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它所做的是創建一個具有兩個路由的API網關,一個用於根端點(\/),另一個用於\/test端點。當這個程序運行時,\/ 路由將從本地www目錄上傳s3 bucket中的內容。\/test端點的背後是一個lambda函數,其中的上下文取自事件處理程序代碼塊。這例子,但是考慮到我們到目前爲止所討論的內容,它還是非常吸引人的。雖然到目前爲止我的大多數例子都是以Pulumi爲基礎,但它們並不是朝着唯一這個方向發展的。例如,AWS推出了“AWS CDK”"},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fn:4","title":"","type":null},"content":[{"type":"text","text":"4"}]},{"type":"text","text":"或雲開發工具包。這允許你用你選擇的語言編寫代碼,它將在運行時被“合成”進雲結構堆棧。甚至還有一個“構造庫”,允許你使用已經由AWS創建並將其包含在你的代碼庫中的組件。這些組件將許多複雜性(例如網絡)抽象爲易於使用的小單元,它們是安全的,並遵循了最佳實踐。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"import * as core from \"@aws-cdk\/core\";\nimport * as apigateway from \"@aws-cdk\/aws-apigateway\";\nimport * as lambda from \"@aws-cdk\/aws-lambda\";\nimport * as s3 from \"@aws-cdk\/aws-s3\";\n\nexport class WidgetService extends core.Construct {\n constructor(scope: core.Construct, id: string) {\n super(scope, id);\n\n const bucket = new s3.Bucket(this, \"WidgetStore\");\n\n const handler = new lambda.Function(this, \"WidgetHandler\", {\n runtime: lambda.Runtime.NODEJS_10_X, \/\/ So we can use async in widget.js\n code: lambda.Code.asset(\"resources\"),\n handler: \"widgets.main\",\n environment: {\n BUCKET: bucket.bucketName\n }\n });\n\n bucket.grantReadWrite(handler); \/\/ was: handler.role);\n\n const api = new apigateway.RestApi(this, \"widgets-api\", {\n restApiName: \"Widget Service\",\n description: \"This service serves widgets.\"\n });\n\n const getWidgetsIntegration = new apigateway.LambdaIntegration(handler, {\n requestTemplates: { \"application\/json\": '{ \"statusCode\": \"200\" }' }\n });\n\n api.root.addMethod(\"GET\", getWidgetsIntegration); \/\/ GET \/\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是AWS CDK樣品的工作原理,就像我們在Pulumi上所做的一樣。甚至Terraform也在朝着這個方向發展,它有一個基於AWS CDK的項目,你可以用typescript和python編寫腳本。這些構造在底層使用了Terraform模塊,用於跨多個雲提供商提供基礎設施。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不管是好是壞,我認爲我們正朝着一個方向前進,在最好的情況下,基礎設施將與代碼共存,就像構建文件與代碼共存一樣。但是,我認爲大家會嘗試更進一步,將基礎設施代碼集成到實際的應用程序中。所有這些都將由應用程序在運行時自行管理。現在這裏有一個光譜,像大多數東西一樣,不同的應用程序將處於其中某個地方。應用程序的類型將起到決大多數的決定作用。例如,我發現很難想象這對由Postgres實例支持的單體java應用程序的影響會像在AWS中運行的無服務器應用程序的影響那麼大。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於DevOps工程師來說,其含義現在也會有所不同。一些公司的理解是創建一個負責構建ci管道的DevOps工程師角色,與之類似,有些公司的開發人員將接管整個職責,並使用本文提到的工具和實踐來完成所有工作。作爲職業生涯中大部分時間都在參與DevOps的從業者,我的建議是學習typescript並熟悉Pulumi。若不出所料,它會取得成功的,只是還需要些時日。如果這沒有發生,至少,你可以隨時切換成前端開發人員,因爲我相信對他們的需求量會不斷增長的。不過,AWS在用戶體驗方面做得很爛,可能不會來找你工作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/www.usenix.org\/legacy\/event\/lisa98\/full_papers\/burgess\/burgess.pdf","title":"","type":null},"content":[{"type":"text","text":"Computer Immunology - Mark Burgess"}]},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fnref:1","title":"","type":null},"content":[{"type":"text","text":"[return]"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/github.com\/pulumi\/pulumi-awsx\/tree\/master\/nodejs\/examples\/alb\/ec2Instance","title":"","type":null},"content":[{"type":"text","text":"https:\/\/github.com\/pulumi\/pulumi-awsx\/tree\/master\/nodejs\/examples\/alb\/ec2Instance"}]},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fnref:2","title":"","type":null},"content":[{"type":"text","text":"[return]"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/github.com\/pulumi\/pulumi-awsx\/tree\/master\/nodejs\/examples\/alb\/fargate","title":"","type":null},"content":[{"type":"text","text":"https:\/\/github.com\/pulumi\/pulumi-awsx\/tree\/master\/nodejs\/examples\/alb\/fargate"}]},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fnref:3","title":"","type":null},"content":[{"type":"text","text":"[return]"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/aws.amazon.com\/cdk\/","title":"","type":null},"content":[{"type":"text","text":"https:\/\/aws.amazon.com\/cdk\/"}]},{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages\/#fnref:4","title":"","type":null},"content":[{"type":"text","text":"[return]"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/cosminilie.ro\/posts\/evolution-of-configuration-languages","title":"xxx","type":null},"content":[{"type":"text","text":"The golden age of configuration languages"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"譯者簡介:冬雨,小小技術宅一枚,從事研發過程改進及質量改進方面的工作,關注編程、軟件工程、敏捷、DevOps、雲計算等領域,非常樂意將國外新鮮的IT資訊和深度技術文章翻譯分享給大家,已翻譯出版《深入敏捷測試》、《持續交付實戰》。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章