[{"data":1,"prerenderedAt":568},["ShallowReactive",2],{"portfolio-\u002Fportfolio\u002Faws-terraform-iac":3},{"id":4,"title":5,"award":6,"body":7,"cover":547,"description":31,"extension":548,"featured":408,"link":6,"meta":549,"navigation":408,"order":116,"path":550,"pinned":551,"repo":552,"role":553,"seo":554,"stack":555,"stem":564,"summary":565,"year":566,"__hash__":567},"portfolio\u002Fportfolio\u002Faws-terraform-iac.md","AWS 基礎設施即程式碼（Terraform 模組）",null,{"type":8,"value":9,"toc":538},"minimark",[10,14,18,21,32,50,53,56,206,232,236,239,294,309,313,327,501,508,511,528,531,534],[11,12,13],"h2",{"id":13},"專案概述",[15,16,17],"p",{},"建立公司的 Terraform 標準規範：一套遠端 State 後端，以及一系列可重複使用、附\n完整文件與測試的模組，讓各團隊能以一致的方式建置 AWS 基礎設施，而非各自土法\n煉鋼。",[11,19,20],{"id":20},"專案結構",[22,23,28],"pre",{"className":24,"code":26,"language":27},[25],"language-text","infrastracture\u002F\n  global\u002F         # 只需要存在一份的共用資源：State 後端、鎖表、IAM Role\n  development\u002F\n  stage\u002F\n  production\u002F\nmodules\u002F\n  aws\u002F\n    alb\u002F\n    cloudfront\u002F\n    ec2\u002F\n    s3\u002F\n","text",[29,30,26],"code",{"__ignoreMap":31},"",[15,33,34,37,38,41,42,45,46,49],{},[29,35,36],{},"global"," 存放 Terraform 自身需要的基礎設施——狀態儲存與鎖定機制，",[29,39,40],{},"development","\n／",[29,43,44],{},"stage","／",[29,47,48],{},"production"," 則各自獨立，同一份模組在不同環境用不同的變數值套用，\n彼此的 State 完全隔離。",[11,51,52],{"id":52},"遠端後端與存取控制",[15,54,55],{},"Bootstrap 一次性建立好 Terraform 自己需要的基礎設施後，往後所有模組都指向同\n一組遠端後端：",[22,57,61],{"className":58,"code":59,"language":60,"meta":31,"style":31},"language-hcl shiki shiki-themes github-dark","terraform {\n  backend \"s3\" {\n    bucket         = \"tom-terraform-backend\"\n    key            = \"global\u002Fs3\u002Ftom-terraform-backend\u002Fterraform.tfstate\"\n    region         = \"ap-northeast-1\"\n    dynamodb_table = \"tom-terraform-backend-lock\"\n    encrypt        = true\n    assume_role = {\n      role_arn    = \"arn\u002FS_Terraform_Role\"\n      policy_arns = [\"arn\u002FS_Terraform_Role-policy\"]\n    }\n  }\n}\n","hcl",[29,62,63,76,88,102,114,125,137,149,159,171,188,194,200],{"__ignoreMap":31},[64,65,68,72],"span",{"class":66,"line":67},"line",1,[64,69,71],{"class":70},"svObZ","terraform",[64,73,75],{"class":74},"s95oV"," {\n",[64,77,79,82,86],{"class":66,"line":78},2,[64,80,81],{"class":70},"  backend",[64,83,85],{"class":84},"sDLfK"," \"s3\"",[64,87,75],{"class":74},[64,89,91,94,98],{"class":66,"line":90},3,[64,92,93],{"class":74},"    bucket",[64,95,97],{"class":96},"snl16","         =",[64,99,101],{"class":100},"sU2Wk"," \"tom-terraform-backend\"\n",[64,103,105,108,111],{"class":66,"line":104},4,[64,106,107],{"class":74},"    key",[64,109,110],{"class":96},"            =",[64,112,113],{"class":100}," \"global\u002Fs3\u002Ftom-terraform-backend\u002Fterraform.tfstate\"\n",[64,115,117,120,122],{"class":66,"line":116},5,[64,118,119],{"class":74},"    region",[64,121,97],{"class":96},[64,123,124],{"class":100}," \"ap-northeast-1\"\n",[64,126,128,131,134],{"class":66,"line":127},6,[64,129,130],{"class":74},"    dynamodb_table",[64,132,133],{"class":96}," =",[64,135,136],{"class":100}," \"tom-terraform-backend-lock\"\n",[64,138,140,143,146],{"class":66,"line":139},7,[64,141,142],{"class":74},"    encrypt",[64,144,145],{"class":96},"        =",[64,147,148],{"class":84}," true\n",[64,150,152,155,157],{"class":66,"line":151},8,[64,153,154],{"class":74},"    assume_role",[64,156,133],{"class":96},[64,158,75],{"class":74},[64,160,162,165,168],{"class":66,"line":161},9,[64,163,164],{"class":74},"      role_arn    ",[64,166,167],{"class":96},"=",[64,169,170],{"class":100}," \"arn\u002FS_Terraform_Role\"\n",[64,172,174,177,179,182,185],{"class":66,"line":173},10,[64,175,176],{"class":74},"      policy_arns ",[64,178,167],{"class":96},[64,180,181],{"class":74}," [",[64,183,184],{"class":100},"\"arn\u002FS_Terraform_Role-policy\"",[64,186,187],{"class":74},"]\n",[64,189,191],{"class":66,"line":190},11,[64,192,193],{"class":74},"    }\n",[64,195,197],{"class":66,"line":196},12,[64,198,199],{"class":74},"  }\n",[64,201,203],{"class":66,"line":202},13,[64,204,205],{"class":74},"}\n",[207,208,209,225],"ul",{},[210,211,212,216,217,220,221,224],"li",{},[213,214,215],"strong",{},"S3"," 儲存 State、",[213,218,219],{},"DynamoDB"," 做 State Lock，避免多人同時 ",[29,222,223],{},"apply"," 造成競\n爭寫入",[210,226,227,228,231],{},"所有操作都透過 ",[29,229,230],{},"assume_role"," 走一個專屬的 Terraform 執行角色，而不是用長期\n存在的 IAM User AccessKey，降低憑證外洩風險",[11,233,235],{"id":234},"模組文件terraform-registry-風格","模組文件（Terraform Registry 風格）",[15,237,238],{},"每個模組（ALB、CloudFront、EC2、S3）都附上 Registry 慣用的四段式文件：",[240,241,242,255],"table",{},[243,244,245],"thead",{},[246,247,248,252],"tr",{},[249,250,251],"th",{},"項目",[249,253,254],{},"說明",[256,257,258,270,278,286],"tbody",{},[246,259,260,264],{},[261,262,263],"td",{},"Usage",[261,265,266,269],{},[29,267,268],{},"module"," 區塊的最小可用範例",[246,271,272,275],{},[261,273,274],{},"Requirements",[261,276,277],{},"Terraform／Provider 版本限制",[246,279,280,283],{},[261,281,282],{},"Inputs",[261,284,285],{},"每個變數的型別、預設值、是否必填",[246,287,288,291],{},[261,289,290],{},"Outputs",[261,292,293],{},"模組回傳的屬性（如 ARN、ID）",[15,295,296,297,300,301,304,305,308],{},"以 S3 模組為例，Inputs 包含 ",[29,298,299],{},"s3_bucket_name","（必填）、\n",[29,302,303],{},"s3_bucket_versioning","（預設開啟版本控制）、",[29,306,307],{},"prevent_destroy","（預設鎖住刪\n除，避免誤刪正式環境的 bucket），呼叫端不需要讀原始碼就知道怎麼用。",[11,310,312],{"id":311},"用-goterratest驗證模組","用 Go（Terratest）驗證模組",[15,314,315,316,319,320,322,323,326],{},"不是只做 ",[29,317,318],{},"terraform validate","，而是每個模組都有一支 Go 整合測試，實際\n",[29,321,223],{}," 一份範例設定、檢查輸出的屬性符合預期、再 ",[29,324,325],{},"destroy"," 乾淨：",[22,328,332],{"className":329,"code":330,"language":331,"meta":31,"style":31},"language-go shiki shiki-themes github-dark","func TestS3Example(t *testing.T) {\n    ops := &terraform.Options{TerraformDir: \"..\u002Fexample\"}\n    defer terraform.Destroy(t, ops)\n\n    terraform.InitAndApply(t, ops)\n\n    bucketArn := terraform.Output(t, ops, \"bucket_arn\")\n    bucketTags := terraform.OutputMap(t, ops, \"tags\")\n    assert.NotNil(t, bucketArn)\n    assert.Equal(t, \"expected-bucket-name\", bucketTags[\"Name\"])\n}\n","go",[29,333,334,364,390,404,410,420,424,445,464,475,497],{"__ignoreMap":31},[64,335,336,339,342,345,349,352,355,358,361],{"class":66,"line":67},[64,337,338],{"class":96},"func",[64,340,341],{"class":70}," TestS3Example",[64,343,344],{"class":74},"(",[64,346,348],{"class":347},"s9osk","t",[64,350,351],{"class":96}," *",[64,353,354],{"class":70},"testing",[64,356,357],{"class":74},".",[64,359,360],{"class":70},"T",[64,362,363],{"class":74},") {\n",[64,365,366,369,372,375,377,379,382,385,388],{"class":66,"line":78},[64,367,368],{"class":74},"    ops ",[64,370,371],{"class":96},":=",[64,373,374],{"class":96}," &",[64,376,71],{"class":70},[64,378,357],{"class":74},[64,380,381],{"class":70},"Options",[64,383,384],{"class":74},"{TerraformDir: ",[64,386,387],{"class":100},"\"..\u002Fexample\"",[64,389,205],{"class":74},[64,391,392,395,398,401],{"class":66,"line":90},[64,393,394],{"class":96},"    defer",[64,396,397],{"class":74}," terraform.",[64,399,400],{"class":70},"Destroy",[64,402,403],{"class":74},"(t, ops)\n",[64,405,406],{"class":66,"line":104},[64,407,409],{"emptyLinePlaceholder":408},true,"\n",[64,411,412,415,418],{"class":66,"line":116},[64,413,414],{"class":74},"    terraform.",[64,416,417],{"class":70},"InitAndApply",[64,419,403],{"class":74},[64,421,422],{"class":66,"line":127},[64,423,409],{"emptyLinePlaceholder":408},[64,425,426,429,431,433,436,439,442],{"class":66,"line":139},[64,427,428],{"class":74},"    bucketArn ",[64,430,371],{"class":96},[64,432,397],{"class":74},[64,434,435],{"class":70},"Output",[64,437,438],{"class":74},"(t, ops, ",[64,440,441],{"class":100},"\"bucket_arn\"",[64,443,444],{"class":74},")\n",[64,446,447,450,452,454,457,459,462],{"class":66,"line":151},[64,448,449],{"class":74},"    bucketTags ",[64,451,371],{"class":96},[64,453,397],{"class":74},[64,455,456],{"class":70},"OutputMap",[64,458,438],{"class":74},[64,460,461],{"class":100},"\"tags\"",[64,463,444],{"class":74},[64,465,466,469,472],{"class":66,"line":161},[64,467,468],{"class":74},"    assert.",[64,470,471],{"class":70},"NotNil",[64,473,474],{"class":74},"(t, bucketArn)\n",[64,476,477,479,482,485,488,491,494],{"class":66,"line":173},[64,478,468],{"class":74},[64,480,481],{"class":70},"Equal",[64,483,484],{"class":74},"(t, ",[64,486,487],{"class":100},"\"expected-bucket-name\"",[64,489,490],{"class":74},", bucketTags[",[64,492,493],{"class":100},"\"Name\"",[64,495,496],{"class":74},"])\n",[64,498,499],{"class":66,"line":190},[64,500,205],{"class":74},[15,502,503,504,507],{},"這讓模組的正確性不是靠人工檢查 ",[29,505,506],{},".tf"," 檔案，而是每次修改都能重新驗證「這個模組\n套用後，AWS 上真的長出預期的資源與屬性」。",[11,509,510],{"id":510},"實作內容",[207,512,513,516,519,522,525],{},[210,514,515],{},"使用 S3 + DynamoDB + IAM 建立 Terraform 遠端後端，並以 tfvars 管理可編輯人員",[210,517,518],{},"製作 ALB、CloudFront、EC2、S3 等可重複使用模組，依環境（development／\nstage／production）切分",[210,520,521],{},"用 Go（Terratest）為模組撰寫 Apply → Assert → Destroy 的整合測試",[210,523,524],{},"撰寫 Registry 風格的模組使用說明文件與內部 Terraform 教學訓練",[210,526,527],{},"盤點並調整公司現有 IAM User，改用 AssumeRole 降低常駐憑證數量",[11,529,530],{"id":530},"成果",[15,532,533],{},"讓每個團隊都能以一致、經過測試的方式建置 AWS 基礎設施，取代過去各專案各自為\n政、缺乏文件與遠端後端管理的 Terraform 用法。",[535,536,537],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":31,"searchDepth":78,"depth":78,"links":539},[540,541,542,543,544,545,546],{"id":13,"depth":78,"text":13},{"id":20,"depth":78,"text":20},{"id":52,"depth":78,"text":52},{"id":234,"depth":78,"text":235},{"id":311,"depth":78,"text":312},{"id":510,"depth":78,"text":510},{"id":530,"depth":78,"text":530},"\u002Fimages\u002Fportfolio\u002Faws-web-services-logo.svg","md",{},"\u002Fportfolio\u002Faws-terraform-iac",false,"https:\u002F\u002Fgithub.com\u002Ftom3019\u002FIaC.Aws","DevOps \u002F SRE ‧ 房仲業",{"title":5,"description":31},[556,557,558,559,219,215,560,561,562,563],"Terraform","AWS","Go","Terratest","IAM","EC2","CloudFront","ALB","portfolio\u002Faws-terraform-iac","依環境切分的 Terraform 專案結構，附上 Terraform Registry 風格的模組文件（Requirements／Providers／Inputs／Outputs）與 Go（Terratest）整合測試。",2025,"wMzMBY2PaZh_D-oJxL3LoM2wVsYnXoXfFC4YZd5Ydh0",1792959149881]