完成經典的 Hello World,我們來做點正經事啦~(本日程式碼)
把「build docker image 並 push 到 ECR repository」的工作丟給 Gitlab Pipeline 做~~,然後我們就可以去打電動/追劇惹~~~
延續昨天的 .gitlab-ci.yml
,在 deploy job 的 script 寫上 build 跟 push image 的指令們:
stages:
- deploy
deploy:
stage: deploy
image: cjwind/gitlab-executor:docker-awscli-0.1.0
variables:
ECR_REPO_URI: $ECR_BASE/$ECR_REPO
script:
- cat $ENV >> .env
- IMAGE_TAG=`date +%Y%m%d-%H%M%S`
- docker build . -t $ECR_REPO_URI:$IMAGE_TAG
- docker tag $ECR_REPO_URI:$IMAGE_TAG $ECR_REPO_URI:latest
- aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $ECR_BASE
- docker push $ECR_REPO_URI:$IMAGE_TAG
- docker push $ECR_REPO_URI:latest
- docker rmi $ECR_REPO_URI:$IMAGE_TAG
- docker rmi $ECR_REPO_URI:latest
指令從 docker build
到 docker push
都是前面手動做過的,相信沒有太大問題。最後兩個 docker rmi
是做 image 的清除,不然 runner 上的 docker image 會越來越多、越來越多直到把硬碟塞爆,然後 pipeline job 就會出各種奇奇怪怪的 error 說它硬碟太撐了。
第二行指令是我們想以日期跟時間作為版號,所以把日期時間的字串 assign 給 IMAGE_TAG
變數,讓後面 docker build
可以上 tag。
那第一行 cat $ENV >> .env
在做什麼呢?看起來是把變數 ENV
的檔案內容附加到 .env
檔案的最後,為什麼要這麼做呢?因為 .env
裡有些敏感資訊,像是資料庫的帳密。我們不會把 .env
commit 進 repository,不然各種帳號、密碼或者 api key 都會被看光光而且永存於 git history 裡,這樣很不好捏~
但是 docker image 要執行 Laravel 又需要 .env
內的資訊,怎麼辦呢?我們可以把敏感資訊寫在 Gitlab CI/CD Variable(下面會講),然後在 job script 裡把敏感資訊們 inject 進 .env
。
欸?那如果 docker image 整包被偷走,敏感資訊就被偷走了耶?
是的,沒錯。但這問題的解決方式是不能把敏感資訊放在 .env
內,要在執行期間透過其他方式取得,不過這情境不在本系列文的討論範圍,有興趣的朋友可以研究看看 Laravel 如何在執行期間才透過其他服務,如 AWS Secret Manager,取得需要的敏感資訊。
再來是 hello world 沒有的兩個設定:image
跟 variables
。
variables
很好猜(?),就是定義變數嘛~這邊只是把 ECR_BASE
跟 ECR_REPO
兩個從 CI/CD variable 來的變數組合成 ECR_REPO_URI
方便後面使用而已。
最後是 image
,記得 register runner 的時候有指定一個預設 image 嗎?我們也可以在 job 裡用 image
指定這個 job 要使用的 image。這個 image 必須有能力執行 job script 的指令們,像這邊它要能執行 docker 跟 awscli。如果沒有現成的 image 就要自己做,這邊筆者做了一個包含 docker 以及 awscli 的 image 放在 Docker Hub 上。
在執行 job 的 container 內要能執行 docker
也有一點小眉角~~(aka 容易遇到各種問題)~~,比較簡單的方式是 register runner 時加上 --docker-volumes
將 runner 的 /var/run/docker.sock
掛載進 container,讓 container 透過 host 的 docker daemon 做事。gitlab runner register 指令的 --docker-volumes
相當於每次啟動 container 時加上 -v
,這個參數會把指定的 host(啟動 container 的機器)的路徑掛載到 container 的指定路徑,像 /var/run/docker.sock:/var/run/docker.sock
就是把 host 的 /var/run/docker.sock
掛進 container 的 /var/run/docker.sock
。利用這個小技巧,container 的 docker 指令要用這個檔案跟 docker daemon 溝通時,實際上是跟 runner 的 docker daemon 溝通(這段暫時看不懂、只知道這樣設會動也沒關係,有興趣可以把參數拿掉看看 pipeline job 會怎麼樣)。
延續前面操作,我們到 Gitlab Settings ⇒ CI/CD ⇒ Variables 區塊 Add variable 增加 script 中用到的三個變數:
ECR_BASE
:填 [YOUR_AWS_ACCOUNT_ID].dkr.ecr.[REGION].amazonaws.com
ECR_REPO
:填前面開的 ECR repository 名稱,例如 my-app
ENV
:複製 Laravel 的 .env.example
內容並填入對應需要的設定,type 選 FILE。(欸…對啦這邊筆者偷懶,直接把整份 .env
放在 CI/CD variable,也不管是不是 credential 之類的敏感資料)
另外也要設定給 awscli 用的 credential 及預設 region:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION
credential 要填擁有 ecr:GetAuthorizationToken
跟 push image 權限的 IAM user 的 access key 跟 secret。預設 region 則填 ap-northeast-1
。
所有變數的 Environment scope 先選 All (default),如果預設有勾選 protect variable,先取消打勾。
Project 的 CI/CD variable 預設 type 是 variable,也就是一般的變數,在 job 裡用 variable key 取得 variable value 使用。
另一種 type 是 file,它由 variable key、variable value 以及一個檔案組成。在 job script 使用時 variable value 會被存在一個暫存檔裡,variable key 則是 environment variable name,environment variable 的值則是這個暫存檔的路徑,所以在 script 內要以操作檔案的方式使用 file type 的 CI/CD variable。
我們的變數 ENV
便是 file 類型,在 job script 裡 $ENV
是暫存檔的路徑,暫存檔的內容是 CI/CD variable 設定的、我們想要的 .env
檔案內容,所以操作 $ENV
要用對待檔案路徑的方式來操作,在這裡我們用 cat 把它 append 到 .env
。有興趣的朋友可以在 script echo $ENV
看看內容。
把修改 commit 並 push 上 gitlab 的 repository 來啟動 pipeline,觀察 job 的執行狀況:
最後有 Job succeeded
就是成功了!
最後介紹一下在 Gitlab 怎麼區分「環境」~
通常開發 web service 的開發者會有自己的開發環境。部屬出去讓一般使用者使用的是正式環境,可能稱為 production 或簡寫 prod。一般會再架設一個跟正式環境幾乎一樣的環境來做整體測試(包含應用程式的邏輯流程以及部屬),這個環境稱為 stage 或 staging。
stage 跟 prod 的差別只是上面運行的程式還在開發與測試,並且使用的 infrastructure 規模比較小(例如機器比較少或比較小台),它們擁有相同的部屬方式跟服務,我們利用 stage 來確認在 prod 環境上應用程式跟 infrastructure 的設置與運作是正常的,盡量降低東西上到 prod 才發現不會動的機率。當然 stage 終究會有些地方跟 prod 不同,例如介接第三方 API 時 stage 只能使用第三方的測試環境、不能接正式環境來測試(總不能接金流的正式環境來測試…🫠),之後 prod 上線依然要對這些部份進行測試。
既然要用相同方式 deploy prod 跟 stage 環境,我們應該用一樣的 Gitlab Pipeline 來 deploy,但是兩個環境的 .env
、credential、ECR repository 等等不會一樣啊~如何用同一份 .gitlab-ci.yml
deploy 兩個「程式碼相同但設定與 deploy target 不同」的環境?
這要用到 Gitlab 的 Environment 功能啦~
一個 Environment 包含程式 deploy 到的環境的各種資訊,像是 CI/CD variable、有哪些 deployment、環境的網址等等。來看看實際上怎麼用!
Environment 功能在 Operate 底下:
建立兩個 environment 分別叫 staging
跟 prod
:
建好的 staging
environment,還沒有任何 deployment:
再來是調整 CI/CD variable 的 environment scope 讓我們可以在不同環境使用相同變數名稱但不同值(不同 credential、env、ECR repository 等等)。點進某個 CI/CD variable 修改 environment scope:
這樣修改後,staging environment 的 pipeline 拿到的 $ECR_REPO
值會是 my-app
。新增另一個 key 也叫 ECR_REPO
的 CI/CD variable,設定 value 為 prod-app
、environment scope 為 prod
,如此 prod environment 的 pipeline 拿到的 $ECR_REPO
值會是 prod-app
。
最後是 .gitlab-ci.yml
內 job 的小調整,我們要讓 deploy 相關的 job 知道它要 deploy 到哪個 environment。在 job 加上 environment
這個參數:
deploy:
stage: deploy
environment: staging
# ...
欸~這樣不就對每個 environment 都要寫一個 deploy job?script 會重複吧?說好的同一份 deployment script 呢?
這個問題的解決方式有很多,筆者介紹最懶惰的(喂):開兩個 branch 分別叫 staging
跟 prod
,這兩個 branch 專門用來 deploy,所以可以利用 Gitlab 預先定義好的變數 CI_COMMIT_BRANCH
來當作 job 的 environment:
deploy:
stage: deploy
environment: $CI_COMMIT_BRANCH
# ...