[{"data":1,"prerenderedAt":845},["ShallowReactive",2],{"article-\u002Farticles\u002Fterraform-remote-state-modules":3},{"id":4,"title":5,"body":6,"date":831,"description":133,"excerpt":832,"extension":833,"meta":834,"navigation":369,"path":835,"seo":836,"stem":837,"tags":838,"__hash__":844},"articles\u002Farticles\u002Fterraform-remote-state-modules.md","Terraform 團隊協作的兩個基本功：S3 + DynamoDB 遠端 State，以及可重複使用的模組設計",{"type":7,"value":8,"toc":820},"minimark",[9,14,32,43,64,75,79,82,109,113,120,127,234,280,285,295,299,306,309,317,324,328,334,462,465,470,609,615,620,674,681,686,693,698,761,768,773,784,788,791,810,813,816],[10,11,13],"h2",{"id":12},"先搞懂-state-是什麼","先搞懂 State 是什麼",[15,16,17,18,22,23,26,27,31],"p",{},"Terraform 是宣告式的：你描述「想要有一台 EC2、一個 S3 Bucket」，Terraform 負責把現實變成你描述的樣子。要做到這點，它需要知道「",[19,20,21],"strong",{},"現在已經建立了哪些資源","」，這份紀錄就是 ",[19,24,25],{},"State","（",[28,29,30],"code",{},"terraform.tfstate","）。",[15,33,34,35,38,39,42],{},"每次執行 ",[28,36,37],{},"plan"," 或 ",[28,40,41],{},"apply","，Terraform 會比對三個東西：",[44,45,46,53,58],"ol",{},[47,48,49,52],"li",{},[19,50,51],{},"程式碼","：你想要的狀態",[47,54,55,57],{},[19,56,25],{},"：Terraform 記得它上次建立了什麼，以及每個資源在雲端上的 ID",[47,59,60,63],{},[19,61,62],{},"雲端實際狀態","：透過 API 查詢資源目前真正的樣子",[15,65,66,67,70,71,74],{},"State 是 Terraform 認得「程式碼裡的這個 ",[28,68,69],{},"aws_s3_bucket.logs","，就是雲端上那個 Bucket」的唯一依據。",[19,72,73],{},"State 遺失了，Terraform 就不認得任何既有的資源","，下一次 apply 會想要重新建立一份；State 損壞了，可能會刪掉不該刪的東西。",[10,76,78],{"id":77},"本機-state-的問題","本機 State 的問題",[15,80,81],{},"預設情況下，State 存在執行 Terraform 的那台電腦上：",[83,84,85,91,97,103],"ul",{},[47,86,87,90],{},[19,88,89],{},"只有一個人有","：同事要修改基礎設施，得先跟你拿 State 檔",[47,92,93,96],{},[19,94,95],{},"沒有鎖","：兩個人同時 apply，兩邊都讀到舊的 State、各自改了資源、各自寫回 State，後寫的蓋掉先寫的，State 和雲端實際狀態對不起來",[47,98,99,102],{},[19,100,101],{},"含有機密","：State 會以明文保存資源的屬性，資料庫密碼、存取金鑰都可能出現在裡面，放在個人電腦上或 commit 進 Git 都很危險",[47,104,105,108],{},[19,106,107],{},"CI 無法執行","：Pipeline 拿不到 State，就無法自動化 apply",[10,110,112],{"id":111},"遠端後端s3-存-statedynamodb-做鎖","遠端後端：S3 存 State、DynamoDB 做鎖",[15,114,115,116,119],{},"AWS 上最常見的遠端後端組合，是 ",[19,117,118],{},"S3 存放 State 檔、DynamoDB 做狀態鎖","：",[15,121,122],{},[123,124],"img",{"alt":125,"src":126},"Terraform 遠端 State 示意圖：工程師 A 對 production 執行 apply，先在 DynamoDB 取得該 State 的鎖；工程師 B 同時對 production apply 時，因為鎖被佔用而等待或失敗；CI Pipeline 對 development apply，取得的是另一份 State 的鎖，互不影響；持有鎖才能讀寫 S3 上的 State 檔，S3 開啟版本控制、加密並封鎖公開存取；production 與 development 使用同一組模組但不同的變數值，State 完全隔離，在 dev 下的指令不可能動到 prod","\u002Fimages\u002Farticles\u002Fterraform-remote-state.svg",[128,129,134],"pre",{"className":130,"code":131,"language":132,"meta":133,"style":133},"language-hcl shiki shiki-themes github-dark","terraform {\n  backend \"s3\" {\n    bucket         = \"company-terraform-backend\"\n    key            = \"production\u002Fapp\u002Fterraform.tfstate\"\n    region         = \"ap-northeast-1\"\n    dynamodb_table = \"company-terraform-lock\"\n    encrypt        = true\n  }\n}\n","hcl","",[28,135,136,149,161,175,187,198,210,222,228],{"__ignoreMap":133},[137,138,141,145],"span",{"class":139,"line":140},"line",1,[137,142,144],{"class":143},"svObZ","terraform",[137,146,148],{"class":147},"s95oV"," {\n",[137,150,152,155,159],{"class":139,"line":151},2,[137,153,154],{"class":143},"  backend",[137,156,158],{"class":157},"sDLfK"," \"s3\"",[137,160,148],{"class":147},[137,162,164,167,171],{"class":139,"line":163},3,[137,165,166],{"class":147},"    bucket",[137,168,170],{"class":169},"snl16","         =",[137,172,174],{"class":173},"sU2Wk"," \"company-terraform-backend\"\n",[137,176,178,181,184],{"class":139,"line":177},4,[137,179,180],{"class":147},"    key",[137,182,183],{"class":169},"            =",[137,185,186],{"class":173}," \"production\u002Fapp\u002Fterraform.tfstate\"\n",[137,188,190,193,195],{"class":139,"line":189},5,[137,191,192],{"class":147},"    region",[137,194,170],{"class":169},[137,196,197],{"class":173}," \"ap-northeast-1\"\n",[137,199,201,204,207],{"class":139,"line":200},6,[137,202,203],{"class":147},"    dynamodb_table",[137,205,206],{"class":169}," =",[137,208,209],{"class":173}," \"company-terraform-lock\"\n",[137,211,213,216,219],{"class":139,"line":212},7,[137,214,215],{"class":147},"    encrypt",[137,217,218],{"class":169},"        =",[137,220,221],{"class":157}," true\n",[137,223,225],{"class":139,"line":224},8,[137,226,227],{"class":147},"  }\n",[137,229,231],{"class":139,"line":230},9,[137,232,233],{"class":147},"}\n",[83,235,236,253,266],{},[47,237,238,241,242,245,246,249,250],{},[19,239,240],{},"S3 Bucket","：存放 State。務必開啟",[19,243,244],{},"版本控制","（State 被寫壞時可以回到上一版）、",[19,247,248],{},"伺服器端加密","，並",[19,251,252],{},"封鎖所有公開存取",[47,254,255,258,259,262,263,265],{},[19,256,257],{},"DynamoDB 資料表","：以 ",[28,260,261],{},"LockID","（字串）為主鍵。執行 ",[28,264,41],{}," 前，Terraform 會在表中寫入一筆以 State 路徑為鍵的紀錄，代表「我正在使用這份 State」；其他人此時執行，會看到鎖被佔用而等待或失敗；執行結束後自動釋放",[47,267,268,271,272,275,276,279],{},[19,269,270],{},"存取權限","：用 IAM 限制誰能讀寫哪些 State 路徑，例如開發人員只能操作 ",[28,273,274],{},"development\u002F"," 下的 State，",[28,277,278],{},"production\u002F"," 只有部署用的 Role 可以寫入",[281,282,284],"h3",{"id":283},"雞生蛋的問題誰來建立後端","雞生蛋的問題：誰來建立後端？",[15,286,287,288,291,292,31],{},"存放 State 的 Bucket 和鎖表本身也是 AWS 資源，也想用 Terraform 管理，但它們必須在「其他所有 Terraform 專案之前」就存在。常見的做法是做一個獨立的 ",[19,289,290],{},"Bootstrap"," 專案：先用本機 State 建立 Bucket 與鎖表，建好之後再把這個專案自己的 State 也遷移到剛建立的遠端後端（",[28,293,294],{},"terraform init -migrate-state",[10,296,298],{"id":297},"環境隔離每個環境一份-state","環境隔離：每個環境一份 State",[15,300,301,302,305],{},"開發、測試、正式環境",[19,303,304],{},"一定要使用不同的 State","。如果所有環境共用一份 State，一次在開發環境的誤操作，就可能動到正式環境的資源。",[15,307,308],{},"常見的目錄結構：",[128,310,315],{"className":311,"code":313,"language":314,"meta":133},[312],"language-text","infrastructure\u002F\n├── global\u002F          ← 只需要一份的共用資源：State 後端、IAM Role\n├── development\u002F\n│   └── main.tf      ← backend key = development\u002F…\u002Fterraform.tfstate\n├── stage\u002F\n└── production\u002F\n    └── main.tf      ← backend key = production\u002F…\u002Fterraform.tfstate\nmodules\u002F\n└── aws\u002F\n    ├── alb\u002F\n    ├── ec2\u002F\n    └── s3\u002F\n","text",[28,316,313],{"__ignoreMap":133},[15,318,319,320,323],{},"每個環境的目錄有自己的後端設定與 State，彼此完全隔離；資源的寫法則抽成共用的",[19,321,322],{},"模組","，環境之間的差異只在傳入的變數值。",[10,325,327],{"id":326},"模組設計讓每個環境都用同一份寫法","模組設計：讓每個環境都用同一份寫法",[15,329,330,331,333],{},"如果每個環境各自複製一份 ALB、EC2 的寫法，一開始一模一樣，幾個月後就會各自長出不同的修改，誰也說不清楚正式環境和測試環境到底差在哪。",[19,332,322],{},"讓「怎麼建立一個 ALB」只寫一次：",[128,335,337],{"className":130,"code":336,"language":132,"meta":133,"style":133},"# production\u002Fmain.tf\nmodule \"web_alb\" {\n  source = \"..\u002F..\u002Fmodules\u002Faws\u002Falb\"\n\n  name              = \"web-prod\"\n  vpc_id            = data.aws_vpc.main.id\n  subnet_ids        = data.aws_subnets.public.ids\n  certificate_arn   = var.certificate_arn\n  enable_access_log = true\n}\n",[28,338,339,345,355,365,371,382,408,432,448,457],{"__ignoreMap":133},[137,340,341],{"class":139,"line":140},[137,342,344],{"class":343},"sAwPA","# production\u002Fmain.tf\n",[137,346,347,350,353],{"class":139,"line":151},[137,348,349],{"class":143},"module",[137,351,352],{"class":157}," \"web_alb\"",[137,354,148],{"class":147},[137,356,357,360,362],{"class":139,"line":163},[137,358,359],{"class":147},"  source",[137,361,206],{"class":169},[137,363,364],{"class":173}," \"..\u002F..\u002Fmodules\u002Faws\u002Falb\"\n",[137,366,367],{"class":139,"line":177},[137,368,370],{"emptyLinePlaceholder":369},true,"\n",[137,372,373,376,379],{"class":139,"line":189},[137,374,375],{"class":147},"  name",[137,377,378],{"class":169},"              =",[137,380,381],{"class":173}," \"web-prod\"\n",[137,383,384,387,389,392,395,398,400,403,405],{"class":139,"line":200},[137,385,386],{"class":147},"  vpc_id",[137,388,183],{"class":169},[137,390,391],{"class":147}," data",[137,393,394],{"class":169},".",[137,396,397],{"class":147},"aws_vpc",[137,399,394],{"class":169},[137,401,402],{"class":147},"main",[137,404,394],{"class":169},[137,406,407],{"class":147},"id\n",[137,409,410,413,415,417,419,422,424,427,429],{"class":139,"line":212},[137,411,412],{"class":147},"  subnet_ids",[137,414,218],{"class":169},[137,416,391],{"class":147},[137,418,394],{"class":169},[137,420,421],{"class":147},"aws_subnets",[137,423,394],{"class":169},[137,425,426],{"class":147},"public",[137,428,394],{"class":169},[137,430,431],{"class":147},"ids\n",[137,433,434,437,440,443,445],{"class":139,"line":224},[137,435,436],{"class":147},"  certificate_arn",[137,438,439],{"class":169},"   =",[137,441,442],{"class":147}," var",[137,444,394],{"class":169},[137,446,447],{"class":147},"certificate_arn\n",[137,449,450,453,455],{"class":139,"line":230},[137,451,452],{"class":147},"  enable_access_log",[137,454,206],{"class":169},[137,456,221],{"class":157},[137,458,460],{"class":139,"line":459},10,[137,461,233],{"class":147},[15,463,464],{},"一個好維護的模組，通常會有這幾個特徵：",[15,466,467],{},[19,468,469],{},"輸入有型別、有驗證、有合理的預設值",[128,471,473],{"className":130,"code":472,"language":132,"meta":133,"style":133},"variable \"name\" {\n  description = \"ALB 名稱，會作為相關資源的名稱前綴\"\n  type        = string\n\n  validation {\n    condition     = length(var.name) \u003C= 28\n    error_message = \"name 長度不可超過 28 個字元（ALB 名稱上限為 32）。\"\n  }\n}\n\nvariable \"enable_access_log\" {\n  description = \"是否開啟存取日誌\"\n  type        = bool\n  default     = false\n}\n",[28,474,475,485,495,505,509,516,541,551,555,559,563,573,583,593,604],{"__ignoreMap":133},[137,476,477,480,483],{"class":139,"line":140},[137,478,479],{"class":143},"variable",[137,481,482],{"class":157}," \"name\"",[137,484,148],{"class":147},[137,486,487,490,492],{"class":139,"line":151},[137,488,489],{"class":147},"  description",[137,491,206],{"class":169},[137,493,494],{"class":173}," \"ALB 名稱，會作為相關資源的名稱前綴\"\n",[137,496,497,500,502],{"class":139,"line":163},[137,498,499],{"class":147},"  type",[137,501,218],{"class":169},[137,503,504],{"class":169}," string\n",[137,506,507],{"class":139,"line":177},[137,508,370],{"emptyLinePlaceholder":369},[137,510,511,514],{"class":139,"line":189},[137,512,513],{"class":143},"  validation",[137,515,148],{"class":147},[137,517,518,521,524,527,530,532,535,538],{"class":139,"line":200},[137,519,520],{"class":147},"    condition",[137,522,523],{"class":169},"     =",[137,525,526],{"class":157}," length",[137,528,529],{"class":147},"(var",[137,531,394],{"class":169},[137,533,534],{"class":147},"name) ",[137,536,537],{"class":169},"\u003C=",[137,539,540],{"class":157}," 28\n",[137,542,543,546,548],{"class":139,"line":212},[137,544,545],{"class":147},"    error_message",[137,547,206],{"class":169},[137,549,550],{"class":173}," \"name 長度不可超過 28 個字元（ALB 名稱上限為 32）。\"\n",[137,552,553],{"class":139,"line":224},[137,554,227],{"class":147},[137,556,557],{"class":139,"line":230},[137,558,233],{"class":147},[137,560,561],{"class":139,"line":459},[137,562,370],{"emptyLinePlaceholder":369},[137,564,566,568,571],{"class":139,"line":565},11,[137,567,479],{"class":143},[137,569,570],{"class":157}," \"enable_access_log\"",[137,572,148],{"class":147},[137,574,576,578,580],{"class":139,"line":575},12,[137,577,489],{"class":147},[137,579,206],{"class":169},[137,581,582],{"class":173}," \"是否開啟存取日誌\"\n",[137,584,586,588,590],{"class":139,"line":585},13,[137,587,499],{"class":147},[137,589,218],{"class":169},[137,591,592],{"class":169}," bool\n",[137,594,596,599,601],{"class":139,"line":595},14,[137,597,598],{"class":147},"  default",[137,600,523],{"class":169},[137,602,603],{"class":157}," false\n",[137,605,607],{"class":139,"line":606},15,[137,608,233],{"class":147},[15,610,611,612,614],{},"錯誤的參數在 ",[28,613,37],{}," 階段就被擋下來，而不是 apply 到一半才收到 AWS API 的錯誤。",[15,616,617],{},[19,618,619],{},"輸出其他模組需要的值",[128,621,623],{"className":130,"code":622,"language":132,"meta":133,"style":133},"output \"arn\"      { value = aws_lb.this.arn }\noutput \"dns_name\" { value = aws_lb.this.dns_name }\n",[28,624,625,651],{"__ignoreMap":133},[137,626,627,630,633,636,638,641,643,646,648],{"class":139,"line":140},[137,628,629],{"class":143},"output",[137,631,632],{"class":157}," \"arn\"",[137,634,635],{"class":147},"      { value",[137,637,206],{"class":169},[137,639,640],{"class":147}," aws_lb",[137,642,394],{"class":169},[137,644,645],{"class":147},"this",[137,647,394],{"class":169},[137,649,650],{"class":147},"arn }\n",[137,652,653,655,658,661,663,665,667,669,671],{"class":139,"line":151},[137,654,629],{"class":143},[137,656,657],{"class":157}," \"dns_name\"",[137,659,660],{"class":147}," { value",[137,662,206],{"class":169},[137,664,640],{"class":147},[137,666,394],{"class":169},[137,668,645],{"class":147},[137,670,394],{"class":169},[137,672,673],{"class":147},"dns_name }\n",[15,675,676,677,680],{},"模組之間透過輸出值串接，例如 CloudFront 模組需要 ALB 的 ",[28,678,679],{},"dns_name"," 作為來源。",[15,682,683],{},[19,684,685],{},"單一職責、適當的抽象層級",[15,687,688,689,692],{},"模組應該代表一個有意義的元件（一個 ALB 加上它的監聽器與安全群組），而不是把整個環境塞進一個模組，也不要包裝到只剩一個 ",[28,690,691],{},"resource"," 的薄殼——那樣只是多了一層間接，沒有帶來任何價值。",[15,694,695],{},[19,696,697],{},"鎖定版本",[128,699,701],{"className":130,"code":700,"language":132,"meta":133,"style":133},"terraform {\n  required_version = \">= 1.5\"\n  required_providers {\n    aws = { source = \"hashicorp\u002Faws\", version = \"~> 5.0\" }\n  }\n}\n",[28,702,703,709,719,726,753,757],{"__ignoreMap":133},[137,704,705,707],{"class":139,"line":140},[137,706,144],{"class":143},[137,708,148],{"class":147},[137,710,711,714,716],{"class":139,"line":151},[137,712,713],{"class":147},"  required_version",[137,715,206],{"class":169},[137,717,718],{"class":173}," \">= 1.5\"\n",[137,720,721,724],{"class":139,"line":163},[137,722,723],{"class":143},"  required_providers",[137,725,148],{"class":147},[137,727,728,731,733,736,739,742,745,747,750],{"class":139,"line":177},[137,729,730],{"class":147},"    aws",[137,732,206],{"class":169},[137,734,735],{"class":147}," { source ",[137,737,738],{"class":169},"=",[137,740,741],{"class":173}," \"hashicorp\u002Faws\"",[137,743,744],{"class":147},", version ",[137,746,738],{"class":169},[137,748,749],{"class":173}," \"~> 5.0\"",[137,751,752],{"class":147}," }\n",[137,754,755],{"class":139,"line":189},[137,756,227],{"class":147},[137,758,759],{"class":139,"line":200},[137,760,233],{"class":147},[15,762,763,764,767],{},"模組放在獨立的 Git Repo 時，使用端以 ",[28,765,766],{},"?ref=v1.2.0"," 指定版本，模組升級不會意外影響所有環境。",[15,769,770],{},[19,771,772],{},"文件與測試",[15,774,775,776,779,780,783],{},"每個模組附上說明輸入、輸出、使用範例的 README（可以用 ",[28,777,778],{},"terraform-docs"," 自動產生），並用 ",[19,781,782],{},"Terratest","（Go）撰寫整合測試：真的在測試帳號中建立資源、驗證行為、再自動刪除，確保模組修改後仍然能正常運作。",[10,785,787],{"id":786},"使用情境建立公司的-terraform-規範","使用情境：建立公司的 Terraform 規範",[15,789,790],{},"在房仲公司導入 Terraform 時，主要的工作就是上面這兩件事：",[83,792,793,800,807],{},[47,794,795,796,799],{},"以 Bootstrap 專案建立 ",[19,797,798],{},"S3 + DynamoDB"," 的遠端後端與 IAM Role，所有專案統一使用，State 不再散落在個人電腦上",[47,801,802,803,806],{},"依環境切分目錄，",[19,804,805],{},"每個環境一份 State","，搭配 IAM 權限控制誰能操作正式環境",[47,808,809],{},"把 ELB、CloudFront、EC2、S3、EKS 等常用資源做成模組，附上文件與 Terratest 整合測試，各團隊以一致的方式建立基礎設施",[10,811,812],{"id":812},"一句話總結",[15,814,815],{},"State 是 Terraform 認得雲端資源的唯一依據，團隊協作時要放在 S3 這類遠端後端，並用 DynamoDB 加鎖避免同時 apply，再開啟版本控制、加密與權限控管；每個環境各自一份 State 確保互不影響，資源的寫法則抽成有型別驗證、有輸出、有版本、有文件和測試的模組，讓所有環境都用同一份程式碼建立。",[817,818,819],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":133,"searchDepth":151,"depth":151,"links":821},[822,823,824,827,828,829,830],{"id":12,"depth":151,"text":13},{"id":77,"depth":151,"text":78},{"id":111,"depth":151,"text":112,"children":825},[826],{"id":283,"depth":163,"text":284},{"id":297,"depth":151,"text":298},{"id":326,"depth":151,"text":327},{"id":786,"depth":151,"text":787},{"id":812,"depth":151,"text":812},"2024-05-21",null,"md",{},"\u002Farticles\u002Fterraform-remote-state-modules",{"title":5,"description":133},"articles\u002Fterraform-remote-state-modules",[839,840,841,842,843],"Terraform","IaC","AWS","DevOps","基礎設施","_mv9kWzgByleELRNTBGbmb7uMghhEmV3h7mi2fhcZ1Y",1792959150384]