2025年3月7日 星期五

Docker 開發筆記 - 在 Synology NAS 運行 immich 照片服務與 Symbolic link 管理方式 @ Synology DS723+


周邊有強者好友真不錯,時常分享把一堆服務都搞自建方案,目的不是省錢,而是追求資料握在自己手中的架構。挑選 immich 大概算是公認做得不錯的相簿管理服務,其介面跟 Google Photos 很像,且支援人臉辨識也有依照地圖(OpenStreetMap)顯示照片拍攝位置


而 immich 官方文件有非常方便架設 Docker 的筆記:
在 Synology NAS 上,若有支援 Docker 的,可以在 Container Manager 中新增專案,這時要上傳 docker-composer.yml 檔案,則是在剛剛的教學網站有顯示:
在此先決定了 NAS 上的檔案位置,把 Docker 運行所需的環境擺在 /docker/immich-app 目錄(這是 File Station 看到的路徑),他實際的路徑位置會是 /volume1/docker/immich-app 。


這時 docker-compose.yml 可透過 Container Manager 上傳好,而 example.env 則是透過 File Station 拖拉進去改名成 .env

這時直接在 Container Manager 運行時,應當會踩到問題,因為 library 或 postgres 目錄不存在,這時也繼續靠 File Station 建立,後續 Container Manager 運行就會正常了,正常到最後可以用 NAS_IP:2283 瀏覽起來,可以看到註冊畫面等。

這邊有幾個議題記錄一下:
  1. 使用 NAS 反向代理伺服器,提供 https 連線 immich 服務
  2. 在 immich 使用外部圖庫 (External Library) ,將原本的 Synology Photos 匯入
  3. 在 immich External Library 如何使用 Symbolic link 來管理
首先,NAS 反向代理伺服器的設定還滿簡單的,找一個 port 來服務 https 連線,例如 2284 ,直接把它導向到 2283 ,這樣就搞定收工,未來就有 https://NAS_IP:2284 可用了!


接著,使用外部圖庫部分,這邊跟 Docker 設定有關,必須把額外的資料像掛載進來,例如

    volumes:
      # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
      - /var/services/homes/UserID/Photos:/synology-photo:ro

透過把 Synology 在使用者家目錄的位置掛進來,這樣對 immich docker 環境中,就多了 /synology-photo 路徑可以查看資料,這時在 immich 網頁設定上,直接設定外部位置在 /synology-photo ,就可以讓 immich 掃描到照片資料來分析了

然而,對於資料管理上會想要慢慢實驗,例如少量的把目錄資料加入到 immich 外部圖庫,且不需要一直改 YAML volumes?通常對系統熟悉的,就會想試試 symbolic link 架構,還能避免 NAS 上有重複資料佔著空間,然而,symbolic link 在 Docker 環境上有使用限制,有一些討論串:
我這邊的解法,其實是設法先排除 Docker 限制,只要繞過限制後,Symbolic link 還是可以使用的。目前的設計就先在 ~/Photos/ 建立一個目錄,如 immich ,接著在裡面建立 Symbolic link 到上一層 ~/Photos 想要的資料就好,而掛載到 Docker 裡仍維持在 ~/Photos 位置,而 immich 網頁上就設定 /synology-photo/immich 即可。

總結一下 Symbolic link 使用資訊:
  • Docker volumes YAML 設定不變,概念上要把 Symbolic link 來源也包覆到
    • /var/services/homes/UserID/Photos:/synology-photo:ro
  • 建立 /var/services/homes/UserID/Photos/immich 目錄
  • 在 immich 網頁上,將外部圖庫設定在 /synology-photo/immich
  • 未來想動態將資料交給 immich 服務時,就只須在 /var/services/homes/UserID/Photos/immich 內建立向上一層的目錄,確保那些享用 symbolic link 的資料都有在 Docker volumes 內即可

UserID@NAS:~/Photos/immich$ ls -l
lrwxrwxrwx+ 1 UserID users 27 Mar  6 23:25 '2020-01-01' -> '../2020-01-01/'
lrwxrwxrwx  1 UserID users 24 Mar  6 23:28 '2021-02-02' -> '../2021-02-02/'
lrwxrwxrwx  1 UserID users 27 Mar  6 23:28 '2022-03-03' -> '../2022-03-03/'
  
收工

2025年2月26日 星期三

[macOS] 使用 Windows APP (原 Microsoft RDP app) 遠端登入 Ubuntu 24.04 Desktop 遠端桌面 及 Error code: 0x207 問題排除 @ macOS 15.3.1


把自己的 Pi 5 安裝 Ubuntu 24.04 Desktop 後,原本想要裝 VNC 來提供遠端桌面的,搞了半天才發現已經有內建的 RDP 服務,接著當然就是在 macOS 上找尋很多年前已裝過的 Microsoft Windows Remote Desktop app ,找了半天也沒找到,最後才發現 macOS App Store 已更名為 Windows App

第一次在 macOS 上 Windows APP 登入到 Ubuntu 24.04 Desktop 是正常的,但很詭異的是之後一直會收到錯誤訊息:

We couldn't connect to the remote PC. This might be due to an expired password. If this keeps happening, contact your network administrator for assistance. Error code: 0x207

我也是趁這次才知道,原來 Ubuntu 24.04 Desktop 內建的 RDP 服務,必須使用者先登入過桌面才行,並且鎖住螢幕也不行。事後發現有一連串的方式解套。


首先是使用者必須先登入過桌面,那就開啟自動登入就好,剛好在 Pi 5 實驗裝置不會踩到資安議題。接著是鎖住螢幕的問題,有一派是調整成不自動鎖住,此時發現還可以透過額外安裝 GNOME Extension Manager,並且在裡頭下載 Allow Locked Remote Desktop 就好,設定好重啟。

$ sudo apt install gnome-shell-extension-manager

最後,關於 macOS 上第二次起登入 Ubuntu Desktop 總是碰到的 Error code: 0x207 問題,神人解法是先把設定檔匯出,接著修改關鍵設定,把 "use redirection server name:i:0" 更新成 "use redirection server name:i:1" ,再匯入回去即可搞定。有些討論串認為是 macOS - Windows app 的 bug


搞定

ref:

2025年2月24日 星期一

[Linux] 樹莓派5開發筆記 - Raspberry Pi 5 + Pi AI kit + Pi Mini Tower kit @ Ubuntu 24.04 Desktop



二月初手癢,敗了個 Raspberry Pi AI kit Hailo-8 ,接著發現以前買的 Pi 5 小機殼不能用了,裸機運行了一下,就手癢找找有沒有比較大的機殼,就看到了 樹莓派 5 塔式散熱器外殼組 | Raspberry Pi 5 Mini Tower Kit with ICE Tower ,接著再去蝦皮逛一眼,有看到遠方發貨的,就試了一下。


實際上排線是非常緊繃地連著:


有一邊的腳架不鎖(無法鎖):



最終就這樣被我硬拼了出來,上方的 Pi AI kit 的架子有兩隻腳是懸空的,懸空那面剛好有排線針腳固定著也不太會晃,就這樣胡亂用了起來。

這邊就簡單的完成 Mini Tower Kit 的面板(OLED)初始化,順便小改 code 來達成顯示 IP 和收到關機時可正常退出使螢幕變黑。


目前就包成一個 script 來運行:

user@user-desktop:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 24.04.2 LTS
Release: 24.04
Codename: noble
user@user-desktop:~$ curl https://raw.githubusercontent.com/changyy/study-pi5/refs/heads/main/script/pi5-mini-tower-kit-with-oled-only.sh > /tmp/pi5-mini-tower-kit-with-oled-only.sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6184  100  6184    0     0  11814      0 --:--:-- --:--:-- --:--:-- 11801
user@user-desktop:~$ bash /tmp/pi5-mini-tower-kit-with-oled-only.sh 
...

收工

2025年2月21日 星期五

Python 開發筆記 - promptpack-for-code 一個將專案的目錄結構和指定檔案輸出單一文字檔,讓 AI 點評跟建議的小工具


近半年已經極高的比例都會用 claude.ai 做事,最近用量太大後,再加上 grok.com 免費中,就想到如果碰到付費額度用原被凍時,該怎樣快速換另一家 AI 服務使用,也可以避免與 AI 輔助努力了老半天,重啟對話又要打得落落長的內容。


因此就開發了一個小工具:promptpack-for-code

他的用法就是把指定專案的目錄結構、指定的程式碼輸出成 output.txt ,接著就可以上傳 output.txt 跟 AI 互動,成果包括可以請他點評哪邊可以優化、撰寫系統架構、教學文件等等,十分方便

```
% python3 -m venv venv
% source venv/bin/activate
(venv) /tmp % pip install promptpack-for-code
% git clone https://github.com/changyy/py-promptpack-for-code
% promptpack-for-code py-promptpack-for-code -r py-promptpack-for-code 
Successfully created output.txt
```

問問此時此刻(2025-02-21)最強的 Grok 3 AI:

```
### 專案概述

`py-promptpack-for-code` 是一個 Python 命令列工具,旨在將多個程式碼檔案或目錄中的檔案打包成一個結構化的單一文字檔,特別針對與 AI 語言模型進行程式碼審查或分析的需求進行優化。這個工具的主要功能包括:

1. **檔案結構生成**:根據指定的根目錄生成樹狀目錄結構,方便理解專案的組織。
2. **內容整合**:將指定目錄中的程式碼檔案內容合併到輸出檔案,並保留相對路徑。
3. **過濾與忽略**:支援自訂忽略模式(例如 `.git`、`.pyc` 等),避免不必要的檔案內容。
4. **進度顯示**:提供進度條選項,適合處理大型專案時使用。
5. **易於與 AI 整合**:輸出格式規範且結構化,便於直接輸入到 AI 聊天平台進行分析。

### 專案評論

#### 優點
1. **實用性強**:
   - 該工具解決了一個具體且常見的痛點:如何快速準備程式碼供 AI 分析。它不僅合併檔案,還提供了目錄結構,這對理解上下文非常有幫助。
   - 支援多目錄處理、進度條、強制覆蓋等功能,滿足不同使用場景的需求。

2. **結構清晰**:
   - 專案採用標準 Python 包結構(`src/`、`tests/` 等),易於維護和擴展。
   - 文件(如 `README.md` 和 `LICENSE`)完整,提供了詳細的使用說明和授權資訊。

3. **測試覆蓋良好**:
   - `tests/test_core.py` 包含多個單元測試,涵蓋核心功能(樹狀結構生成、檔案處理、忽略模式等),提高了程式碼的可靠性。

4. **部署與分享便利**:
   - 透過 `setup.py` 和 GitHub Actions(`python-publish.yml`)實現 PyPI 發佈,使用者只需 `pip install` 即可使用。
   - MIT 許可證允許廣泛的開源使用和修改。

5. **效能考慮**:
   - 使用 `ThreadPoolExecutor` 實現並行處理檔案,對於大型專案具有一定的效能優勢。

#### 不足之處
1. **功能單一性**:
   - 目前工具主要專注於生成文字輸出,缺乏更高級的分析或格式化選項(例如 JSON 輸出或程式碼摘要生成)。
   - 對於非文字檔案的處理較為簡單(僅跳過),可能無法滿足需要處理混合內容的需求。

2. **錯誤處理有限**:
   - 雖然有基本的日誌記錄(`logging`),但對於檔案編碼錯誤(`UnicodeDecodeError`)或權限問題的處理僅限於跳過或警告,缺乏更靈活的應對策略。

3. **文件不夠詳盡**:
   - `README.md` 提供了基本用法,但對於進階功能(例如自訂忽略模式的具體範例)或輸出檔案的詳細格式說明不夠深入。
   - 缺少 API 文件,對於希望程式化調用的開發者不夠友好。

4. **忽略模式的靈活性**:
   - 雖然支援 `fnmatch` 通配符,但忽略模式僅限於檔案名稱,無法根據檔案內容或大小進行過濾。

### 優化建議

1. **增加輸出格式選項**:
   - 除了目前的純文字輸出,新增 JSON 或 YAML 格式選項,讓使用者可以更方便地將結果用於其他工具或程式化處理。
   - 例如:
     ```json
     {
       "tree": {
         "project-name": {
           "src": {
             "main.py": "def main():\n    print('Hello')\n",
             "utils": {
               "helper.py": "def helper():\n    return True\n"
             }
           }
         }
       }
     }
     ```

2. **增強錯誤處理與回饋**:
   - 提供選項讓使用者決定如何處理非 UTF-8 檔案(例如轉換編碼或以二進位模式顯示部分內容)。
   - 對於權限錯誤,記錄到輸出檔案中,而不是僅在日誌中警告,這樣使用者能更容易追蹤問題。

3. **擴展忽略模式功能**:
   - 支援基於檔案大小、修改時間或內容關鍵字的過濾。例如:
     ```bash
     promptpack-for-code /path/to/src --ignore-size ">10MB" --ignore-modified "<2023-01-01"
     ```
   - 允許從外部檔案(如 `.gitignore`)載入忽略模式,增強與現有工作流的兼容性。

4. **改善文件與範例**:
   - 在 `README.md` 中新增更多實際範例,例如處理大型專案或與 AI 工具(如 ChatGPT 或 GitHub Copilot)配合的具體流程。
   - 提供一份獨立的 `CONTRIBUTING.md`,詳細說明如何提交問題或貢獻程式碼。

5. **新增摘要功能**:
   - 提供選項生成程式碼摘要(例如每個檔案的前幾行或函數簽名),減少輸出檔案的大小,方便快速預覽。
     ```bash
     promptpack-for-code /path/to/src --summary
     ```

6. **支援增量更新**:
   - 增加一個模式,僅處理自上次運行以來有變更的檔案(例如根據檔案修改時間戳記),提高處理效率。
     ```bash
     promptpack-for-code /path/to/src --incremental
     ```

7. **UI 或互動模式**:
   - 開發一個簡單的 TUI(文字使用者介面)或互動模式,讓使用者在命令列中動態選擇要處理的目錄或檔案。

### 總結

`py-promptpack-for-code` 是一個設計良好且實用的工具,特別適合需要與 AI 進行程式碼分析的開發者。其核心功能已經相當成熟,但在靈活性、錯誤處理和文件方面仍有提升空間。透過上述建議,這個專案可以進一步滿足更廣泛的使用場景,並提升使用者的體驗。

如果你有具體的使用需求或想討論某個優化方向,我可以進一步提供更詳細的建議或程式碼範例!
```

2025年2月17日 星期一

Docker 開發筆記 - 使用 aws cli 和 Docker Exec 進入 AWS ECS Container @ macOS

這個議題比我想像中麻煩了點,讓我回憶起 2009 年寫的 AWS 筆記,那時同事一開始還只在用 Firefox extension 管理 AWS EC2 呢 XD 我覺得這理論上要能都透過網頁搞定才對,先把目前研究的過程筆記一下。

總之,要能像 ssh 遠端(docker exec -it ContainerID bash)進去 AWS ECS container 的關鍵之處:
  • 使用 awscli 做事
  • AWS ECS 的 Task 定義,基礎設施需求 -> 任務角色,需要指定一下角色,例如 ecsTaskExecutionRole
  • AWS IAM -> ecsTaskExecutionRole -> 添加 AmazonSSMManagedInstanceCore 權限
  • AWS ECS -> Cluster -> Service ,需要用 awscli 啟動 enable-execute-command ,並且重新更新服務,使之生效
  • 使用 aws 指令登入
首先,先下載 awscli 並且版本要夠新:
安裝後檢查版本,版本太低會無法完成任務:

% aws --version
aws-cli/2.24.5 Python/3.12.6 Darwin/24.3.0 exe/x86_64

接著還要安裝 Session Manager plugin,此例紀錄 Mac with Apple silicon 版:
% curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac_arm64/sessionmanager-bundle.zip" -o "sessionmanager-bundle.zip"
% unzip sessionmanager-bundle.zip
% sudo ./sessionmanager-bundle/install -i /usr/local/sessionmanagerplugin -b /usr/local/bin/session-manager-plugin

基本的環境已準備好了,下一刻是查看自己的 AWS ECS 的任務定義是否有把 任務角色 設定好,這部就維持用網頁吧:



若你的 AWS ECS 上定義的 Task 只有一個,也可以偷懶靠 awscli 操作(在此就不贅述 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION 部分),會用到的指令:

aws ecs list-task-definition-families --status ACTIVE
aws ecs list-task-definitions --family-prefix webapp
aws ecs describe-task-definition --task-definition webapp:3

連續技,快速檢查 taskRoleArn 跟 executionRoleArn:

% NamespaceID=$(aws ecs list-task-definition-families --status ACTIVE | jq -r '.families[0]') ; echo "Namespace: $NamespaceID" ; TaskID=$(aws ecs list-task-definitions --family-prefix "$NamespaceID" | jq -r '.taskDefinitionArns[0]') ; echo "Task: $TaskID" ; aws ecs describe-task-definition --task-definition "$TaskID" | jq ".taskDefinition | { taskRoleArn: .taskRoleArn, executionRoleArn: .executionRoleArn}"
Namespace: myapp-task
Task: arn:aws:ecs:ap-northeast-1:####:task-definition/myapp-task:2
{
  "taskRoleArn": "arn:aws:iam::####:role/ecsTaskExecutionRole",
  "executionRoleArn": "arn:aws:iam::####:role/ecsTaskExecutionRole"
}

接下來,若 AWS ECS Cluster 還沒有建立任何 Service,用指令查:

% aws ecs list-tasks --cluster myapp-cluster  
{
    "taskArns": []
}

接著我們在 AWS ECS 網頁端起了一個 Service 名為 myapp-service ,在創建過程中沒看到啟用 enable-execute-command,這時在網頁上查看也是顯示 "ECS 執行: 關閉"



接下來用 aws cli 查詢:

% aws ecs list-tasks --cluster myapp-cluster   
{
    "taskArns": [
        "arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#"
    ]
}

% TaskID=$(aws ecs list-tasks --cluster myapp-cluster | jq -r '.taskArns[0]') ; echo "TaskID: $TaskID" ; aws ecs describe-tasks --cluster myapp-cluster --tasks $TaskID | jq '.tasks[0] | { "clusterArn": .clusterArn, "taskArn": .taskArn, "taskDefinitionArn": .taskDefinitionArn, "group": .group, "healthStatus": .healthStatus, "desiredStatus": .desiredStatus, "enableExecuteCommand": .enableExecuteCommand, "containers-name": [.containers |.[] | { "name":.name, "runtimeId": .runtimeId} ] }'
TaskID: arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#
{
  "clusterArn": "arn:aws:ecs:ap-northeast-1:####:cluster/myapp-cluster",
  "taskArn": "arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#",
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:####:task-definition/myapp-task:2",
  "group": "service:myapp-service",
  "healthStatus": "HEALTHY",
  "desiredStatus": "RUNNING",
  "enableExecuteCommand": false,
  "containers-name": [
    {
      "name": "php-fpm-docker",
      "runtimeId": "#TASKID#-#ContainerID#"
    },
    {
      "name": "web-docker",
      "runtimeId": "#TASKID#-#ContainerID#"
    }
  ]
}

可以看到 enableExecuteCommand 為 false

這時候,如果透過 aws cli 來設法登入到 Container:

% aws ecs execute-command --cluster myapp-cluster --task #TASKID# --container #containers#name# --command "/bin/bash" --interactive


The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


An error occurred (InvalidParameterException) when calling the ExecuteCommand operation: Unable to start session because the container doesn’t exist. Specify a valid container and try again.

接著,使用 aws cli 來啟動 enableExecuteCommand 吧:

% aws ecs update-service --cluster myapp-cluster --service arn:aws:ecs:ap-northeast-1:####:service/myapp-cluster/myapp-service --enable-execute-command 
{
    "service": {
        ... 
        "enableExecuteCommand": true,
        ...
    }
}

可以看到 enableExecuteCommand 被標記成 true 了,這時還需要重新發布服務,可以重網頁去觸發,或是靠指令觸發:

% aws ecs update-service --cluster myapp-cluster --service arn:aws:ecs:ap-northeast-1:####:service/myapp-cluster/myapp-service --force-new-deployment

當服務發布完畢後,在網頁上就可以看到改變,或是用指令在查一次:

% TaskID=$(aws ecs list-tasks --cluster myapp-cluster | jq -r '.taskArns[0]') ; echo "TaskID: $TaskID" ; aws ecs describe-tasks --cluster myapp-cluster --tasks $TaskID | jq '.tasks[0] | { "clusterArn": .clusterArn, "taskArn": .taskArn, "taskDefinitionArn": .taskDefinitionArn, "group": .group, "healthStatus": .healthStatus, "desiredStatus": .desiredStatus, "enableExecuteCommand": .enableExecuteCommand, "containers-name": [.containers |.[] | { "name":.name, "runtimeId": .runtimeId} ] }'
TaskID: arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#
{
  "clusterArn": "arn:aws:ecs:ap-northeast-1:####:cluster/myapp-cluster",
  "taskArn": "arn:aws:ecs:ap-northeast-1:####:task/myapp-cluster/#TASKID#",
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:####:task-definition/myapp-task:2",
  "group": "service:myapp-service",
  "healthStatus": "HEALTHY",
  "desiredStatus": "RUNNING",
  "enableExecuteCommand": true,
  "containers-name": [
    {
      "name": "php-fpm-docker",
      "runtimeId": "#TASKID#-#CONTAINERID#"
    },
    {
      "name": "web-docker",
      "runtimeId": "#TASKID#-#CONTAINERID#"
    }
  ]
}

如此,就可以正式遠端進去一下:

% aws ecs execute-command --cluster myapp-cluster --task #TASKID# --container web-docker --command "/bin/bash" --interactive

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-################
ip-123-45-6-123:/var/www/html# 

收工

參考資料:

2025年2月16日 星期日

Docker 開發筆記 - AWS ECS 發布 Docker Container 服務和 Gitlab CI/CD 架構實踐


上回在 Docker 開發筆記 - 從 Gitlab 觸發 CI/CD,製作 Docker Image 並發布到 Amazon Elastic Container Service (AWS ECS) 已經提到在 Gitlab.com 完成 Docker Images 的建置、打包、儲存至 Gitlab Container Registry 的架構,以及從 AWS ECS 上如何取得 Gitlab Container Registry 的資料,但沒有紀錄太多 AWS ECS 筆記,這次就完整順一次在 AWS ECS 發布服務時要經歷的事情。我們挑 AWS Japan Region (ap-northeast-1) 來做事。

第一步,可以先在 AWS ECS 建立 Task,例如取名為 myapp-task,過程中會設置 "基礎設施需求" 和 數個 "容器" 設定。




在 基礎設施需求 頁面,有幾個重點要留意:
  • 啟動類型是 AWS Fargate 還是 Amazon EC2 
  • 網路類型
  • 作業系統/架構,CPU, 記憶體
  • 任務角色,是否有從 Container 內發送 AWS API 的需求,以及執行 Container 建置時,要用哪個角色(預設為 ecsTaskExecutionRole)
此例先以 AWS Fargate, Linux/X86_64, 1 vCPU, 2GB 為例。

接下來定義容器的部分,此例會定義兩個容器,一個是 web container ,一個以 nginx 為基礎的 Docker Image,另一個是 app container,一個是 php-fpm 為基礎的 Docker Image

容器 - 1,一個單純的 Laravel framework project

- 名稱: php-fpm-docker
- 映像 URI: registry.gitlab.com/MyGroup/MyProject/myapp-php-fpm:latest
- 基本容器: 是
- 私有登入檔案: On
- Secrets Manager ARN 或名稱: arn:aws:secretsmanager:ap-northeast-1:####:secret:gitlab.com-container-registry
- 連接映射
  - 容器連接 9000, 通訊協定 TCP, 連接阜名稱 9000, 應用程式通訊協定 HTTP
- 唯讀根檔案系統, 唯讀: 否
- CPU: 0.5
- 記憶體硬性限制: 1.5GB, 記憶體軟性限制: 1.5GB
- 環境變數
  - PHPFPM_HOST, localhost
  - PHPFPM_PORT, 9000
- 運作狀態檢查: CMD-SHELL,/usr/local/bin/healthcheck.sh

 

 

容器 - 2,一個單純的 nginx web server

- 名稱: web-docker
- 映像 URI: registry.gitlab.com/MyGroup/MyProject/myapp-nginx:latest
- 基本容器: 是
- 私有登入檔案: On
- Secrets Manager ARN 或名稱: arn:aws:secretsmanager:ap-northeast-1:####:secret:gitlab.com-container-registry
- 連接映射
  - 容器連接 80, 通訊協定 TCP, 連接阜名稱 80, 應用程式通訊協定 HTTP
- 唯讀根檔案系統, 唯讀: 否
- CPU: 0.5
- 記憶體硬性限制: 0.5GB, 記憶體軟性限制: 0.5GB
- 環境變數
  - PHPFPM_HOST, localhost
  - PHPFPM_PORT, 9000
- 運作狀態檢查: CMD-SHELL,/usr/local/bin/healthcheck.sh
- 啟動相依性排序: php-fpm-docker, 條件: Healthy


其中的 私有登入檔案 就是用來存取 registry.gitlab.com Container Registry 的地方,請記得在 Gitlab.com Project 建立 Deploy token 得到一組帳密:



接著到 AWS Secrets Manager 添加一組,在此定為 gitlab.com-container-registry 名稱:

https://ap-northeast-1.console.aws.amazon.com/secretsmanager/listsecrets?region=ap-northeast-1



添加完得到 arn:aws:secretsmanager:ap-northeast-1:####:secret:gitlab.com-container-registry。

下一刻,到 AWS IAM Roles 設定,讓 ecsTaskExecutionRole 可以讀取 secretsmanager 內的資料:

https://us-east-1.console.aws.amazon.com/iam/home?region=ap-northeast-1#/roles



在此透過自訂一個足夠大的權限即可,例如可讀必要的資料即可(建議限制 Resource 可以存取地區甚至命名規則等):


如此,終於把 AWS ECS - Task 運行時所要的設定都安置好了,下一刻就是回到 AWS ECS - Cluster,建立一個 myapp-cluster,其中基礎設施維持使用 AWS Fargate (無伺服器) 模式,接著來建立 Service ,建立 AWS ECS -> Cluster -> Service 時,就比較多要留意的地方



  • 環境
    • 運算組態: 都用預設的,需看一眼確認運算選項: 容量供應商策略
  • 部署組態: 
    • 應用程式類型: 服務
    • 任務定義: myapp-task (也就是剛剛創立的 myapp,內有定義兩個容器)
    • 服務名稱: myapp-service
  • 聯網: 
    • VPC, 子網路: 需要留意, 可自行設計
    • 安全群組: 需要留意, 可自行設計
    • 公有 IP: 開啟
  • Load Balancer
    • 使用負載平衡
    • 負載平衡器類型: Application Load Balancer
    • 容器: web-docker 80:80
    • Application Load Balancer: 建立新的負載平衡器
    • 負載平衡器名稱: myapp-service-load-balanacer
    • 接聽程式: 80, HTTP
    • 目標群組: 建立新的目標群組, 目標群組名稱 ecs-myapp--myapp-service, 通訊協定 HTTP, 取消註冊延遲 300, 運作狀態檢查通訊協定 HTTP, 運作狀態檢查路徑 /



按下建立 myapp-service 後,在 AWS ECS Cluster 頁面上不一定會馬上看到,有時需要重整頁面數次才會看到資訊,接著我們進去 Amazon Elastic Container Service -> 叢集 -> myapp-cluster -> 服務觀看:

https://ap-northeast-1.console.aws.amazon.com/ecs/v2/clusters/myapp-cluster/services?region=ap-northeast-1



再往下點進 myapp-service 進入到運作狀態,如網址 https://ap-northeast-1.console.aws.amazon.com/ecs/v2/clusters/myapp-cluster/services/myapp-service/health?region=ap-northeast-1 等
   
接著也可以點擊 Load Balancer 得知分配到的 DNS name,如 myapp-service-load-balanacer-##########.ap-northeast-1.elb.amazonaws.com 去瀏覽。此外 Load Balancer 這邊也有個小技巧,其實可以讓容器只提供 HTTP 80 服務,單純從 Load Balancer 增加 HTTPS 443 的服務,並把流量導過去 80,這也是讓 Load balancer 幫你扛掉一些算力的架構,但自身的服務也需要一些判斷設定,避免一直無限強制跳轉到 https 等。

如此,就算完成一個服務部署了,練習後想要刪光資源,記得刪掉 Service 後,有用到 Load balancer 的,還要特別去 EC2 Load Balancer 去刪除釋放掉資源,該 Load Balancer 就像一組 EC2 機器,創建多久算多久的錢。

接下來的實驗,就不需依賴 Load Balancer ,可以單純有個 Public IP 可存取即可。刪掉 Service 後,再重新創一個,接著我們要設計 CI/CD 架構,如何通報 AWS ECS 更新服務(Containers),這邊研究一下後,有兩種設計原理:
  1. 不停的去追蹤 registry.gitlab.com/MyGroup/MyProject/myapp-php-fpm:latest 和 registry.gitlab.com/MyGroup/MyProject/myapp-nginx:latest 是否有更新,有更新時透過 aws cli 通報 AWS ECS -> Cluster -> Service -> 觸發更新任務
  2. 在 Gitlab.com 創建 Docker images 時,最後一刻發動 aws cli 通報 AWS ECS -> Cluster -> Service -> 觸發更新任務
其中 (1) 的策略是透過 Amazon EventBridge 去排程執行,原理上需要記錄 Container Registry 的狀態,像是每次執行時,把資料記錄在 DB 內,下次檢查發現有變動時,呼叫 "AWS ECS -> Cluster -> Service -> 觸發更新任務" 後,再把狀態記錄下來,此優點是不用暴露 AWS 的資訊出去,缺點是效率較差,以及需要額外資料紀錄。此例 CI/CD 實踐是用 (2) 招式,建立一個身份可以完成 "AWS ECS -> Cluster -> Service -> 觸發更新任務" 即可。

首先在 IAM 創立一個人員: gitlab.com-call-AWS-ECS-Cluster-Service-Update

https://us-east-1.console.aws.amazon.com/iam/home?region=ap-northeast-1#/users/create


接著建立政策: https://us-east-1.console.aws.amazon.com/iam/home?region=ap-northeast-1#/policies/create



給予以下權限
  • Read: DescribeServices
  • Write: UpdateService
  • Specify ARNs: ap-northeast-1, myapp-cluster
取名為: AWS-ECS-Cluster-Service-Update


如此,後續就可以改到 Gitlab.com 串 CI/CD 了,例如 .gitlab-ci.yml 和 tool/aws/ecs-update-service.sh

deploy_to_ecs:
stage: deploy
image: alpine:latest
needs:
- job: web_docker_image_build
optional: true
- job: app_docker_image_build
optional: true
before_script:
- apk add --no-cache aws-cli curl jq tree bash
- echo "AWS_ACCESS_KEY_ID = ${AWS_ACCESS_KEY_ID}"
- echo "AWS_SECRET_ACCESS_KEY = ${AWS_SECRET_ACCESS_KEY}"
- echo "AWS_DEFAULT_REGION = ${AWS_DEFAULT_REGION}"
- echo "ECS_DEPLOY_ENABLE = ${ECS_DEPLOY_ENABLE}"
- echo "ECS_CLUSTER = ${ECS_CLUSTER}"
- echo "ECS_SERVICE = ${ECS_SERVICE}"
script:
- chmod +x tool/aws/ecs-update-service.sh
- bash tool/aws/ecs-update-service.sh
rules:
- if: $CI_COMMIT_TAG =~ /^web\-v\d+\.\d+\.\d+$/
when: on_success
- if: $CI_COMMIT_TAG =~ /^app\-v\d+\.\d+\.\d+$/
when: on_success
- if: '$CI_PIPELINE_SOURCE == "web"'
when: on_success
- if: '$CI_PIPELINE_SOURCE == "api"'
when: on_success

當創建了相關命名規則的 git tag 後,將觸發 deploy stage 做事,在此規劃 Gitlab.com Project -> Settings -> CI/CD 需要定義相關參數,當參數存在時,就會透過 AWS cli 更新雲端的服務,邏輯上依序呼叫:
  1. aws ecs describe-services --cluster myapp-cluster --services myapp-service | jq '.services | .[] | .events'
  2. aws ecs wait services-stable --cluster myapp-cluster --services myapp-service && echo "Status is stable"
  3. aws ecs update-service --cluster myapp-cluster --service myapp-service --force-new-deployment
  4. aws ecs wait services-stable --cluster myapp-cluster --services myapp-service && echo "Done"

以上就是稍微完整的流程,但仍有很多有趣的地方沒有細說,包括 Docker 的建置、AWS VPC/Security Group/NAT Gateway/ECS Container 啟動類型的影響/Container 狀態檢查該怎樣設計/Zero Downtime/刪除 ECS 服務後仍要手動釋放Load Balancer等,此外,在 Gitlab.com 的 git tag 事件也有滿多有趣的,包括觸發 deploy 跟 build 的 git tag 規則是可以不一樣的,以及在 Gitlab.com 上的 Runner 運行了敏感資料 (aws cli, AWS Access Key ID, AWS Secret Access Key)、是否到底足夠信任挑選的 docker images 等等,都是值得深思的議題。

2025年2月12日 星期三

Docker 開發筆記 - 從 Gitlab 觸發 CI/CD,製作 Docker Image 並發布到 Amazon Elastic Container Service (AWS ECS)


以前常做的事是自行弄個 Jenkins 在搭配 Ansible 連續技完成 CI/CD 的任務,今年要來推廣 Docker 生態,把簡單的任務包裝成 Docker 降低人員維護的成本,在此挑選 Gitlab.com 作為出發點,接著會使用 Gitlab CI/CD 發布到 Amazon Elastic Container Service。這篇筆記不限於 Laravel framework ,但以他當作範例來進行。
首先就是弄個 gitlab project,可以是 Create blank project 也可以是 Create from template,接著就到 Settings -> CI/CD 欣賞一下有哪些設定選項

General pipelines
Auto DevOps
Runners
Artifacts
Variables
Pipeline trigger tokens
Deploy freezes
Job token permissions
Secure files

在 General pipelines 介面上,可以看到滿多設定的,但整體上起源是在 "Deploy to AWS from GitLab CI/CD" 這篇文章中,整個精髓是在 .gitlab-ci.yml 檔案上,因此可以參考 "Create from template" 的素材,當下可以按 preview 先欣賞一下,多參考幾個就有感覺了:

簡言之 .gitlab-ci.yml 就是定義想做的指令,並且可以指定要用哪個環境(docker image)來運行這些指令。例如 Laravel 範例使用的 image 是 edbizarro/gitlab-ci-pipeline-php:latest 位置,對應的就是在 https://hub.docker.com/r/edbizarro/gitlab-ci-pipeline-php 和 https://github.com/edbizarro/gitlab-ci-pipeline-php ,搞懂後就來建立自己的 .gitlab-ci.yml 檔案,目標是直接依照專案內的 Dockerfile 來編譯 Docker Images:

variables:
  DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT: "myapp-php-fpm"
  DOCKER_IMAGE_NAME_FOR_NGINX: "myapp-nginx"
  DOCKER_TLS_CERTDIR: "/certs"

stages:
  - build

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - ls -la $DOCKER_TLS_CERTDIR 
    - docker info
    - docker build -t $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest -f docker/php-fpm/Dockerfile .
    - docker build -t $DOCKER_IMAGE_NAME_FOR_NGINX:latest -f docker/nginx/Dockerfile .
    - docker images
    - pwd
    - ls -la
    - docker save $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest > $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT.image.tar
    - docker save $DOCKER_IMAGE_NAME_FOR_NGINX:latest > $DOCKER_IMAGE_NAME_FOR_NGINX.image.tar
    - ls -la
  artifacts:
    paths:
      - $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT.image.tar
      - $DOCKER_IMAGE_NAME_FOR_NGINX.image.tar
    expire_in: 1 week
  rules:
    - changes:
        - Dockerfile
        - docker/**/*
    - if: '$CI_PIPELINE_SOURCE == "web"'
    - if: '$CI_PIPELINE_SOURCE == "api"'
  #tags:
  #  - docker


如此,回到 Gitlab Project 左邊的 Build 項目,就可以在 Pipelines 去觸發 New pipeline 來觸發 Build docker images ,並且會把產出的 *.image.tar 壓縮起來供下載。此外,在 Build 內的 Pipeline editor 也是滿好用的架構,可以線上編輯 .gitlab-ci.yml 檔案內容,還可以驗證格式內容是否有錯誤。

接下來又做更多事,像是 Laravel framework 相關專案設定和初始化(包括 npm install && npm run build && php artisan optimize):

variables:
  DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT: "myapp-php-fpm"
  DOCKER_IMAGE_NAME_FOR_NGINX: "myapp-nginx"
  DOCKER_TLS_CERTDIR: "/certs"

stages:
  - prepare
  - build

composer:
  stage: prepare
  image: php:8.4-cli
  before_script:
    - apt-get update && apt-get install -y unzip libzip-dev
    - docker-php-ext-install zip
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
  script:
    # https://gitlab.com/gitlab-org/project-templates/laravel/-/blob/main/.gitlab-ci.yml?ref_type=heads
    - cd www
    - composer install --no-dev --optimize-autoloader
    #- php artisan optimize:clear
    - php artisan optimize
  artifacts:
    paths:
      - www/
    expire_in: 1 day
  rules:
    - changes:
        - www/**/*
    - if: '$CI_PIPELINE_SOURCE == "web"'
    - if: '$CI_PIPELINE_SOURCE == "api"'

npm:
  stage: prepare
  image: node:22
  dependencies:
    - composer
  script:
    - cd www
    - npm install
    - npm run build
  artifacts:
    paths:
      - www/node_modules/
      - www/public/build/
    expire_in: 1 day
  rules:
    - changes:
        - www/**/*
    - if: '$CI_PIPELINE_SOURCE == "web"'
    - if: '$CI_PIPELINE_SOURCE == "api"'

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  dependencies:
    - composer
    - npm
  script:
    - docker info
    - docker build -t $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest -f docker/php-fpm/Dockerfile .
    - docker build -t $DOCKER_IMAGE_NAME_FOR_NGINX:latest -f docker/nginx/Dockerfile .
    - docker save $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest > $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT.image.tar
    - docker save $DOCKER_IMAGE_NAME_FOR_NGINX:latest > $DOCKER_IMAGE_NAME_FOR_NGINX.image.tar
  artifacts:
    paths:
      - $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT.image.tar
      - $DOCKER_IMAGE_NAME_FOR_NGINX.image.tar
    expire_in: 1 week
  rules:
    - changes:
        - Dockerfile
        - docker/**/*
        - www/**/*  # 加入 www 目錄的變更觸發
    - if: '$CI_PIPELINE_SOURCE == "web"'
    - if: '$CI_PIPELINE_SOURCE == "api"'

最後,再來增加把 Docker images 發布到 AWS ECS ,目前應當有兩招可以發布:

先試試看 Gitlab Container Registry ,過程就是在 Build stage 中,增加登入到 Gitlab Container Registry 機制:

before_script:
  - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin

實際 Job 運行會看到這段資訊:

$ echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores
Login Succeeded

接著再把這些串起來,在 build stage 時,就把產生的 docker image 送進 Gitlab Container Registry,並且在 Gitlab Project -> Deploy -> Container Registry 看到成果:

variables:
  DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT: "myapp-php-fpm"
  DOCKER_IMAGE_NAME_FOR_NGINX: "myapp-nginx"
  DOCKER_TLS_CERTDIR: "/certs"
  PHP_IMAGE_PATH: "$CI_REGISTRY_IMAGE/$DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT"
  NGINX_IMAGE_PATH: "$CI_REGISTRY_IMAGE/$DOCKER_IMAGE_NAME_FOR_NGINX"

stages:
  - prepare
  - build

composer:
  stage: prepare
  image: php:8.4-cli
  before_script:
    - apt-get update && apt-get install -y unzip libzip-dev
    - docker-php-ext-install zip
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
  script:
    # https://gitlab.com/gitlab-org/project-templates/laravel/-/blob/main/.gitlab-ci.yml?ref_type=heads
    - cd www
    - composer install --no-dev --optimize-autoloader
    #- php artisan optimize:clear
    - php artisan optimize
  artifacts:
    paths:
      - www/
    expire_in: 1 day
  rules:
    - changes:
        - www/**/*
    - if: '$CI_PIPELINE_SOURCE == "web"'
    - if: '$CI_PIPELINE_SOURCE == "api"'

npm:
  stage: prepare
  image: node:22
  dependencies:
    - composer
  script:
    - cd www
    - npm install
    - npm run build
  artifacts:
    paths:
      - www/node_modules/
      - www/public/build/
    expire_in: 1 day
  rules:
    - changes:
        - www/**/*
    - if: '$CI_PIPELINE_SOURCE == "web"'
    - if: '$CI_PIPELINE_SOURCE == "api"'

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  dependencies:
    - composer
    - npm
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
  script:
    - docker info
    - docker build -t $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest -f docker/php-fpm/Dockerfile .
    - docker build -t $DOCKER_IMAGE_NAME_FOR_NGINX:latest -f docker/nginx/Dockerfile .
    - docker save $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest > $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT.image.tar
    - docker save $DOCKER_IMAGE_NAME_FOR_NGINX:latest > $DOCKER_IMAGE_NAME_FOR_NGINX.image.tar
    - docker tag $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest $PHP_IMAGE_PATH:$CI_COMMIT_SHA
    - docker tag $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT:latest $PHP_IMAGE_PATH:latest
    - docker tag $DOCKER_IMAGE_NAME_FOR_NGINX:latest $NGINX_IMAGE_PATH:$CI_COMMIT_SHA
    - docker tag $DOCKER_IMAGE_NAME_FOR_NGINX:latest $NGINX_IMAGE_PATH:latest
    - docker push $PHP_IMAGE_PATH:$CI_COMMIT_SHA
    - docker push $PHP_IMAGE_PATH:latest
    - docker push $NGINX_IMAGE_PATH:$CI_COMMIT_SHA
    - docker push $NGINX_IMAGE_PATH:latest
  artifacts:
    paths:
      - $DOCKER_IMAGE_NAME_FOR_LARAVEL_PROJECT.image.tar
      - $DOCKER_IMAGE_NAME_FOR_NGINX.image.tar
    expire_in: 1 week
  rules:
    - changes:
        - Dockerfile
        - docker/**/*
        - www/**/*  # 加入 www 目錄的變更觸發
    - if: '$CI_PIPELINE_SOURCE == "web"'
    - if: '$CI_PIPELINE_SOURCE == "api"'

如此就只需要到 AWS ECS 後台去建立 Task 改從 Gitlab Container Registry 取資料即可完成發佈,這個架構的優勢是避免在 Gitlab.com 關聯太多 AWS 存取方式,也可以避免 AWS 相關資訊外洩,若要更加保護的話,可以連 Gitlab CI/CD 的 Runner 都挑自己的機器會更穩。



最後,還有一點要留意的,對 Gitlab.com 免費服務有 5GB 空間的限制,假設 Docker Image 隨便就佔了幾百GB,那發佈幾次後就會沒空間了,因此,再補上一段只保留最後三版 Docker Image 的機制:

cleanup_images:
  stage: cleanup
  image: docker:latest
  variables:
    GIT_STRATEGY: none
  before_script:
    - apk add --no-cache curl jq
    - echo "CI_API_V4_URL = ${CI_API_V4_URL}"
    - echo "CI_PROJECT_ID = ${CI_PROJECT_ID}"
    # https://docs.gitlab.com/ee/api/container_registry.html
    - echo "CI_REGISTRY_PASSWORD = ${CI_REGISTRY_PASSWORD}"
    - echo "CI_JOB_TOKEN = ${CI_JOB_TOKEN}" 
    - echo "API REQUEST URL = ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/registry/repositories"
    - |
      curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/registry/repositories" | jq '.' 
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - |
      REGISTRY_TARGET=$(curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/registry/repositories" | jq -r '. | if length > 0 then .[].id else empty end')
      for REGISTRY_ID in $REGISTRY_TARGET; do
        echo "REGISTRY_ID = $REGISTRY_ID"
        curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/registry/repositories/${REGISTRY_ID}/tags"
        REGISTRY_LAST_TAG=$(curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/registry/repositories/${REGISTRY_ID}/tags" | jq -r '[ .[] | select(.name != "latest") ] | sort_by(.name) | if length > 3 then .[:-3] | .[].name else empty end')
        for REGISTRY_TAG in $REGISTRY_LAST_TAG; do
          echo "REGISTRY_ID = $REGISTRY_ID, REGISTRY_TAG = $REGISTRY_TAG"
          curl --request DELETE  --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/registry/repositories/${REGISTRY_ID}/tags/${REGISTRY_TAG}"
        done
      done
  rules:
    - when: on_success

主要是透過 CI_JOB_TOKEN 跟 Gitlab API 溝通,像是取出該專案下有幾個 Container Registry ,並且善用 docker tag 時,把 tag prefix 增加 timestamp ,方便此時 sorting 並挑選舊的 tag 且移除他們,如此就可以節省 Gitlab Project 使用空間。

至於在 AWS ECS 的設定,就主要先看 Gitlab 跟 AWS ECS 官方文件了,由於 Docker Image 我們擺在 Gitlab.com,這時 AWS ECS -> Cluster -> Task ,在定義 Container 要從哪邊 Container Registry 取出時,要寫入 registry.gitlab.com/YourGitlabGroup/YourGitlabProject/YourContainerRegistryProject:latest 等類似的位置,下一刻則是 AWS ECS 運行時,此時 ecsTaskExecutionRole 怎麼存取的議題

流程:
  1. 在 Gitlab Project 上,先建立一個 Deploy Token,會拿到一個帳號跟密碼
    • Settings -> Repository -> Deploy tokens -> Add token
  2. 在 AWS Secrets Manager 上,添加一組,選擇其他類型項目即可


    • 假設產出是 arn:aws:secretsmanager:ap-northeast-1:#####:secret:gitlab.com-container-registry
  3. 在 AWS Identity and Access Management (IAM) 上,找到 ecsTaskExecutionRole ,幫它添加可以存取 Secrets Manager 的權限
    • 開放權限可以只提供 READOnly ,甚至細微到只能對 arn:aws:secretsmanager:ap-northeast-1:#####:secret:gitlab.com-container-registry 操作
    • 沒設定好的錯誤訊息:
      • ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to get registry auth from asm: service call has been retried 1 time(s): failed to fetch secret arn:aws:secretsmanager:ap-northeast-1:#####:secret:gitlab.com-container-registry from secrets manager: AccessDeniedException: User: arn:aws:sts::#####:assumed-role/ecsTaskExecutionRole/##### is not authorized to perform: secretsmanager:GetSecretValue on resource: arn:aws:secretsmanager:ap-northeast-1:#####:secret:gitlab.com-container-registry because no identity-based policy allows the secretsmanager:GetSecretValue action status code: 400
    • 設定範例 JSON
      • {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": [
                        "secretsmanager:GetRandomPassword",
                        "secretsmanager:GetResourcePolicy",
                        "secretsmanager:GetSecretValue",
                        "secretsmanager:DescribeSecret",
                        "secretsmanager:ListSecretVersionIds",
                        "secretsmanager:ListSecrets",
                        "secretsmanager:BatchGetSecretValue"
                    ],
                    "Resource": "*"
                }
            ]
        }
  4. 在 AWS ECS 的 Task 就可以好好設定 Container 的存取機制
    • 私有登錄檔 -> Secrets Manager ARN 或名稱 -> arn:aws:secretsmanager:ap-northeast-1:#####:secret:gitlab.com-container-registry
如此在 AWS ECS -> Cluster -> Create Service 時,就會觸發 AWS ECS Task 運行,就可以看到 Docker Container 被創立出來

目前研究先告一個段落,在 AWS ECS 的設置規劃其實也要不小的篇幅的。

再碎碎念一下,直接實驗 AWS ECS 直接去跟 Gitlab.com Container Registry 要資料時,有時會出現彷彿無法正常取出的問題,這個我還不是很清楚排除的方式,目前還有偷懶隔天再弄一次,一樣的方式又正常可通過 Orz 或許最佳還是改用 AWS ECS Private Container Registry 維護,這樣服務發布架構可以省去一些不穩定的額外煩惱。

類似錯誤訊息:
  • esourceInitializationError: unable to pull secrets or registry auth: unable to get registry auth from asm: There is a connection issue between the task and AWS Secrets Manager. 
數個資安提問:
  • 假設你的 Docker Images 是在 Gitlab.com 產出,要 docker push 到 AWS ECS Container Registry 時,就會要面對在 Gitlab.com 上暴露 AWS 存取的資訊
  • 假設 Build docker images 是用 Gitlab.com 機器(Runner),對應的也是有敏感資訊會在這些機器上,你是否信任他們?
  • 假設 Build docker images 時,用了其他人製作的 Docker images ,你的敏感資訊是否有妥善保護?惡意的 docker images 可以在啟動或結束時,把用戶的敏感資訊傳遞出去
ref: