昨天我們把 Caddy 裝進 EC2,網站終於用 Domain 和 HTTPS 打開了!回頭看這一路上,所有東西都是我們自己手動一步一步做出來的:在 Console 上點出 EC2、Security Group、IAM Role,再到終端機裡 build Image、push 到 ECR、在 EC2 上換 container。
那如果之後程式改了,或是想再開一套一樣的環境,那是不是每次都要從頭再來一遍嗎?所以今天先不動手,來認識一下常聽到的 IaC 和 CI/CD,看看這些手動的步驟可以怎麼交給程式處理ㄅ!
把前面做過的事整理一下,大致上可以分成兩種。
第一種是建立資源,像是:
這些大多是在 Console 上點一點、填一填表單就建好了。建好之後基本上都不用動它,偶爾才要回去改一下設定。
第二種是進版,也就是把新版本的程式送上 Server。像是 Day13 我們第一次把 Image 推上 ECR、在 EC2 跑起來;又或是 Day22 為了把 80 port 讓給 Caddy,又重建了一次 container。整個流程大概是這樣:
| 步驟 | 在哪裡做 |
|---|---|
| build Image | 自己的電腦 |
| push 到 ECR | 自己的電腦 → ECR |
| pull Image | EC2 |
| 停掉舊的 container,用新的 Image 再跑一個 | EC2 |
| 確認網站正常 | 瀏覽器或 curl |
跟建立資源不一樣的是,進版不會做一次就結束。之後每隔一陣子程式有改動,這一整套就要重跑一次。
而這兩種手動的事,剛好各有一種自動化的做法:
| 現在的做法 | 自動化的做法 | |
|---|---|---|
| 建立資源 | 在 Console 上點選、填表單 | IaC:把資源寫成設定檔,交給工具建立 |
| 進版 | 在終端機一行一行打指令 | CI/CD:push 程式碼之後,自動 build、部署 |
簡單來說,IaC 管的是「要有哪些資源」,CI/CD 管的是「怎麼把新版本送上去」。兩個常常一起出現,但解決的是不同的問題,而不是二選一喔!
IaC 的全名是基礎設施即程式碼(Infrastructure as Code),意思是把要建立的資源和設定寫成一個設定
檔,再交由工具去建立、修改或刪除
可以把它想成你在組一台電腦時:在 Console 上點選,像是到光華商場跟店家一項一項交代;而 IaC 則是直接先寫好菜單,再接給店家照著組。下次要再裝一台一樣的,就拿著同一張菜單就好。
以 Day22 開放 80 和 443 的 Security Group 為例,同樣的規則用 AWS CloudFormation 來寫,會長這樣:
Resources:
WebServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP and HTTPS from anywhere
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
不用背語法,對照 Day22 的表格就能看懂:port 80、443,來源 0.0.0.0/0,原本在 Console 上填的欄位,都變成了檔案裡的設定。
寫成檔案有什麼好處呢?
常見的 IaC 工具有這幾個:
| 工具 | 用什麼寫 | 特色 |
|---|---|---|
| AWS CloudFormation | YAML 或 JSON | AWS 自己的服務,上面的範例就是它 |
| AWS CDK | TypeScript、Python 等程式語言 | 寫起來就像在寫程式,最後會轉成 CloudFormation |
| Terraform | HCL(Terraform 自己的語法) | 同一套寫法可以管 AWS、GCP 等不同平台,連 Cloudflare 的 DNS 也能管 |
寫前端的人應該會覺得 CDK 最親切。上面那段如果用 CDK 寫,大概是這樣(只截出重點,省略了 import 和 Stack 的設定):
const vpc = ec2.Vpc.fromLookup(this, 'DefaultVpc', { isDefault: true });
const sg = new ec2.SecurityGroup(this, 'WebServerSecurityGroup', { vpc });
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
PS. EC2 中要裝 Docker、Caddy 這類「機器裡的設定」,又是另一類工作。常見做法是讓 EC2 第一次開機時自動執行一段腳本(User data),或是用 Ansible 這類工具,這篇就先不展開。
CI/CD 是兩件事的合稱:
可以把它想成一條設定好的生產線:程式碼放上去之後,後面的加工、檢查、出貨,都照固定的流程自動跑完。
套回我們的進版流程,交給 CI/CD 之後會變成這樣:
| 進版的步驟 | 手動的時候 | 交給 CI/CD 之後 |
|---|---|---|
| build Image | 在自己的電腦 build,Mac 還要加 --platform linux/amd64 |
在 GitHub 提供的機器上 build,它預設就是 x86_64 |
| push 到 ECR | 用自己的 IAM 身分登入 ECR | 每次執行時,向 AWS 拿一組短期權限 |
| pull、換 container | 開 Session Manager 一行一行打 | 透過 SSM 遠端送指令給 EC2 |
| 確認網站 | 自己開瀏覽器或 curl | 最後自動 curl 一次 |
這個系列會用 GitHub Actions 來做。只要在 repo 裡放一個 workflow 檔(放在 .github/workflows/ 底下的 YAML),寫好什麼時候執行、要跑哪些步驟,GitHub 就會在它提供的機器(Runner)上照著做。
兩個都很實用,但這個系列接下來會先做 CI/CD,原因是使用的頻率:進版每改一次程式就要做一次,資源大多建一次就用很久。先自動化最常做、也最容易打錯的事,效果最明顯。
而 IaC 的部分,由於這系列主要是給初學者看的,我覺得先搞懂和習慣這系列所提到的 AWS 功能後再去學 IaC 會比較好。先知道自己到底建了哪些資源、設定是什麼,到時候才不會刪錯東西讓網站掛掉,或是漏刪了還在計費的資源,而收到可怕的帳單。
所以等到需要開第二套環境(例如測試和正式分開)、有好幾個人一起改設定,或是希望隨時能砍掉重建的時候,再來導入 IaC 。
要讓 GitHub Actions 幫我們 push Image、通知 EC2 換版後,第一個問題就來了,就是:GitHub 的機器要怎麼拿到 AWS 的權限?
最直覺的做法,是建一組 access key 存進 GitHub,但這等於把一把長期有效的鑰匙交出去,一旦外洩就麻煩了。下一篇就來看看 OIDC,讓 GitHub 每次部署時,才向 AWS 換一把短期的鑰匙ㄅ!
資安小提醒:IaC 的設定檔和 workflow 檔都會放進 git,裡面不要寫 access key、密碼、token 這類機密。就算是 private repo 也一樣,只要之後改成 Public,歷史紀錄裡的東西都還是看得到喔!