顯示具有 docker 標籤的文章。 顯示所有文章
顯示具有 docker 標籤的文章。 顯示所有文章

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月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: 

2024年10月14日 星期一

Linux 開發筆記 - 在 Ubuntu 16.04 使用 Docker Ubuntu 24.04 無法正常更新套件 (apt update)@ Ubuntu 16.04

追蹤到最後,解法應當是把 Host 端的 docker 更新來修正,但一開始很懶,只好遮住雙眼摸魚一下 XD

狀態:

```
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 16.04 LTS
Release: 16.04
Codename: xenial

$ docker run -it ubuntu:24.04 /bin/bash
root@a0f5c64ed667:/# apt update
Get:1 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
Get:2 http://archive.ubuntu.com/ubuntu noble InRelease [256 kB]
Err:1 http://security.ubuntu.com/ubuntu noble-security InRelease    
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
Err:2 http://archive.ubuntu.com/ubuntu noble InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
Get:3 http://archive.ubuntu.com/ubuntu noble-updates InRelease [126 kB]
Err:3 http://archive.ubuntu.com/ubuntu noble-updates InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
Get:4 http://archive.ubuntu.com/ubuntu noble-backports InRelease [126 kB]
Err:4 http://archive.ubuntu.com/ubuntu noble-backports InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
Reading package lists... Done
W: GPG error: http://security.ubuntu.com/ubuntu noble-security InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
E: The repository 'http://security.ubuntu.com/ubuntu noble-security InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
W: GPG error: http://archive.ubuntu.com/ubuntu noble InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
E: The repository 'http://archive.ubuntu.com/ubuntu noble InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
W: GPG error: http://archive.ubuntu.com/ubuntu noble-updates InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
E: The repository 'http://archive.ubuntu.com/ubuntu noble-updates InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
W: GPG error: http://archive.ubuntu.com/ubuntu noble-backports InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C
E: The repository 'http://archive.ubuntu.com/ubuntu noble-backports InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
E: Problem executing scripts APT::Update::Post-Invoke 'rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true'
E: Sub-process returned an error code
```

硬解:

```
root@a0f5c64ed667:/# apt update --allow-insecure-repositories
...
```

安裝軟體:

```
root@a0f5c64ed667:/# apt install -y --allow-unauthenticated vim wget curl git tree python3-virtualenv 
...
```

結果越裝越痛苦 XD 還是回去看 Docker 官網安裝教學:docs.docker.com/engine/install/ubuntu/#install-using-the-repository 

如此就可以在 Host Ubuntu 16.04 ,正確呼喚出 docker Ubuntu 24.04 來運行,此外,也可以留意 Docker 官方文件建議的 Ubuntu OS 環境已經是 20.04 以上了

2024年9月23日 星期一

PHP 開發筆記 - 在 macOS 15 安裝 PHP 7.4 開發環境 @ Apple M2 / arm64

原本是習慣用 MacPorts 管理套件,然後升上 macOS 15 後,跑了 MacPorts migration 後,發現 php74 不見了,細追才發現沒裝起來 XD 努力研究了一下:

--->  Configuring php74
Warning: Configuration logfiles contain indications of -Wimplicit-function-declaration; check that features were not accidentally disabled:
  gethostbyname_r: found in php-7.4.33/config.log
  _controlfp: found in php-7.4.33/config.log
  _controlfp_s: found in php-7.4.33/config.log
Warning: Configuration logfiles contain indications of -Wimplicit-int; check that features were not accidentally disabled:
  found in php-7.4.33/config.log
--->  Building php74
Error: Failed to build php74: command execution failed   
Error: See /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/main.log for details.
Error: Follow https://guide.macports.org/#project.tickets if you believe there is a bug.
Error: Processing of port php74 failed
--->  Some of the ports you installed have notes:
  db48 has the following notes:
    The Java and Tcl bindings are now provided by the db48-java and
    db48-tcl subports.

===

:info:build ld: warning: ignoring duplicate libraries: '-largon2', '-lxml2', '-lz'
:info:build Undefined symbols for architecture arm64:
:info:build   "_res_9_dn_expand", referenced from:
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       ...  
:info:build   "_res_9_dn_skipname", referenced from:
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_init", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_search", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build ld: symbol(s) not found for architecture arm64
:info:build clang: error: linker command failed with exit code 1 (use -v to see invocation)
:info:build make: *** [sapi/cli/php] Error 1
:info:build make: *** Waiting for unfinished jobs....
:info:build ld: warning: ignoring duplicate libraries: '-largon2', '-lxml2', '-lz'
:info:build Undefined symbols for architecture arm64:
:info:build   "_res_9_dn_expand", referenced from:
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       ...  
:info:build   "_res_9_dn_skipname", referenced from:
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_init", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_search", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build ld: symbol(s) not found for architecture arm64
:info:build clang: error: linker command failed with exit code 1 (use -v to see invocation)
:info:build make: *** [sapi/phpdbg/phpdbg] Error 1
:info:build make: Leaving directory `/opt/local/var/macports/build/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/work/php-7.4.33'
:info:build Command failed:  cd "/opt/local/var/macports/build/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/work/php-7.4.33" && /usr/bin/make -j8 -w all
:info:build Exit code: 2
:error:build Failed to build php74: command execution failed
:debug:build Error code: CHILDSTATUS 94983 2
:debug:build Backtrace: command execution failed
:debug:build     while executing
:debug:build "system {*}$notty {*}$callback {*}$nice $fullcmdstring"
:debug:build     invoked from within
:debug:build "command_exec -callback portprogress::target_progress_callback build"
:debug:build     (procedure "portbuild::build_main" line 10)
:debug:build     invoked from within
:debug:build "$procedure $targetname"
:error:build See /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/main.log for details.

因此,主要是 _res_9_dn_* 等等的問題,問 AI 說,解方若還要繼續用 MacPorts 管理,只能裝個肥滋滋的 bind9

% sudo port install bind9
% sudo port clean php74
% sudo port install php74 configure.cflags="-I/opt/local/include" configure.ldflags="-L/opt/local/lib -lbind9 -ldns -lisc"

但這樣跑也失敗了

接著又試了 

% brew install php@7.4   
Warning: No available formula with the name "php@7.4". Did you mean php@8.2, php@8.1 or php@8.0?
==> Searching for similarly named formulae and casks...
==> Formulae
php@8.2                                               php@8.1                                               php@8.0

To install php@8.2, run:
  brew install php@8.2

雖然繼續深入問 AI 可以得到要做 php-7.4 patch 檔案,但這樣搞下去維護太累了,等於自己編譯 php74 跟改 code 了

最終,取個平衡,未來需要 php74 的環境,就靠 Docker 吧 XD 就單純這樣用即可:

% docker run -it -v ~/projects/:/projects -p 8080:8080 -p 8000:8000 -p 8443:8443 --rm php:7.4-cli bash

如此在 Docker 內部用 `php -S 0.0.0.0:8000` 就可以在 Host 端存取查看運行結果了

若還是想直接裝在系統,可以參考 github.com/shivammathur/homebrew-php

2024年5月24日 星期五

Docker 開發筆記 - 使用 Dockerfile 透過 Ubuntu 18.04 建置 PHP7 + MySQL 5.7 + PHPMyAdmin 開發環境 @ macOS arm64

Ubuntu:18.04 + php7.2 + mysql5.7 + phpinfo();

此需求源自於舊網站的開發環境的建置,一般人不會碰到。

同事回報之前弄的 Docker 環境,因為從 Macbook Intel x86 架構,換到 Mac M2 ARM 環境後,出現問題。主因是網站使用很舊的 MySQL 5.6 環境,在 macos arm64 的 Docker 環境中,無法很便利的只用官方環境建置出來,預設只能直接升級到 MySQL 8 跑,接著還要面對 MySQL 5.6 跟 MySQL 5.7 以上的 SQL 語法變化。

在此的解法是放棄 MySQL 5.6 ,頂多弄出 MySQL 5.7 環境,並追蹤有哪些 SQL 語法需要修改,SQL語法方面,主要是面對 ONLY_FULL_GROUP_BY 模式的處理。此紀錄透過 Docker 弄個 MySQL 5.7 環境出來過度,且最終只需用 ubuntu:18.04 即可搞定大部分,以下是流水帳:

原先要用 Docker - Ubuntu 24.04 arm64 環境來使用,但追蹤後發現 Ubuntu 20.04, Ubuntu 22.04, Ubuntu 24.04 預設都只提供 MySQL 8 的環境,並且依照 MySQL APT Repository 的方式安裝 mysql-apt-config_0.8.30-1_all.deb 後,確認都沒 mysql-5.7 可選,而坊間其他教學文,主要是透過舊版 mysql-apt-config_0.8.12-1_all.deb 套件,在 Ubuntu 20.04 安裝了 mysql-5.7 ,但其過程是 mysql-apt-config 套件無法偵測出 Ubuntu 環境,透過人工選擇 Ubuntu 18.04 的環境裝到 mysql-5.7 而已,以下是個例子:

% docker run -it ubuntu:24.04 /bin/bash
root@2560222cf1ee:/# apt update && apt install -y wget lsb-release gnupg dialog
root@2560222cf1ee:/# wget https://dev.mysql.com/get/mysql-apt-config_0.8.30-1_all.deb
root@2560222cf1ee:/# dpkg -i ./mysql-apt-config_0.8.30-1_all.deb

MySQL APT Repo features MySQL Server along with a variety of MySQL components. You may select the appropriate product to choose the version that you
wish to receive.

Once you are satisfied with the configuration then select last option 'Ok' to save the configuration, then run 'apt-get update' to load package list.
Advanced users can always change the configurations later, depending on their own needs.

  1. MySQL Server & Cluster (Currently selected: mysql-8.4-lts)  3. MySQL Preview Packages (Currently selected: Disabled)
  2. MySQL Tools & Connectors (Currently selected: Enabled)      4. Ok

Which MySQL product do you wish to configure?
Which server version do you wish to receive?

來試試看裝舊版 mysql-apt-config_0.8.12-1_all.deb:

root@2560222cf1ee:/# dpkg -i ./mysql-apt-config_0.8.12-1_all.deb
root@bc33821f13de:/# apt update
W: GPG error: http://repo.mysql.com/apt/ubuntu cosmic InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY B7B3B788A8D3785C
E: The repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
root@bc33821f13de:/# apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
Executing: /tmp/apt-key-gpghome.eheHF2erjp/gpg.1.sh --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
gpg: key B7B3B788A8D3785C: public key "MySQL Release Engineering <mysql-build@oss.oracle.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1
root@bc33821f13de:/# apt update
W: http://repo.mysql.com/apt/ubuntu/dists/cosmic/InRelease: Key is stored in legacy trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in apt-key(8) for details.
N: Skipping acquire of configured file 'mysql-tools/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'
N: Skipping acquire of configured file 'mysql-5.7/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'
N: Skipping acquire of configured file 'mysql-apt-config/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'

最後就輕鬆改用 ubuntu:18.04 ,再把 phpmyadmin 擺好即可,收工。

連續動作:

# Use the official Ubuntu 18.04 as the base image
FROM ubuntu:18.04

# Set non-interactive mode for APT
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Taipei
ENV PHP_VERSION=7.2

# Update the package list and install necessary packages
RUN apt-get update && \
    apt-get install -y tzdata && \
    ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && dpkg-reconfigure --frontend noninteractive tzdata && \
    apt-get install -y \
    htop \
    jq \
    vim \
    curl \
    wget \
    telnet \
    php7.2-cli \
    php7.2-curl \
    php7.2-fpm \
    php7.2-ldap \
    php7.2-mysql \
    php7.2-zip \
    phpunit \
    mysql-server-5.7 \
    ssl-cert \
    nginx \
    openssh-server && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

EXPOSE 22 80 443 8080 8443 3306

# Set up MySQL
RUN service mysql start && \
    mysql -e "CREATE USER 'developer'@'%' IDENTIFIED BY 'developer'; GRANT ALL PRIVILEGES ON *.* TO 'developer'@'%'; CREATE DATABASE developer; FLUSH PRIVILEGES;"

# Set up PHP-FPM Short Open Tag
RUN test -f /etc/php/$PHP_VERSION/fpm/php.ini && \ 
    sed -i 's/short_open_tag = Off/short_open_tag = On/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/max_execution_time = 30/max_execution_time = 300/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 100M/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/post_max_size = 8M/post_max_size = 100M/' /etc/php/$PHP_VERSION/fpm/php.ini 

# Set up phpmyadmin
RUN cd /tmp && \
    wget https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.gz && \
    tar xvzf phpMyAdmin-latest-all-languages.tar.gz && \
    mv phpMyAdmin-*-all-languages /usr/share/phpmyadmin && \
    mkdir -p /usr/share/phpmyadmin/tmp && \
    chown -R www-data:www-data /usr/share/phpmyadmin && \
    chmod 777 /usr/share/phpmyadmin/tmp

# Copy the entrypoint script
COPY nginx-php7.2-fpm-phpmyadmin.conf /etc/nginx/sites-enabled/phpmyadmin
COPY nginx-php7.2-fpm-website.conf /etc/nginx/sites-enabled/default 
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

# Set the entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

# Start services in the foreground
CMD ["bash"]

# Define a volume for mounting the local directory
VOLUME ["/var/www/my-website"]

運行:

% ls
Dockerfile nginx-php7.2-fpm-phpmyadmin.conf
README.md nginx-php7.2-fpm-website.conf
entrypoint.sh
% docker build -t test .
% mkdir /tmp/php
% echo "<?php phpinfo();" > /tmp/php/index.php
% cat /tmp/php/index.php 
<?php phpinfo();
% docker run -p 80:80 -p 443:443 -p 8080:8080 -p 8443:8443 -v /tmp/php:/var/www/my-website -t test
 * Starting MySQL database server mysqld                                                           No directory, logging in with HOME=/
                                                                                            [ OK ]
 * Starting OpenBSD Secure Shell server sshd                                                [ OK ] 
 * Starting nginx nginx                                                                     [ OK ] 
Fri May 24 21:42:43 CST 2024

如此,就可以用 http://localhost, https://localhost/ 看到 phpinfo() 的資料,而用 http://localhost:8080, https://localhost:8443/ 則會看到 phpmyadmin 網頁服務

要關閉時,記得找一下 container id:

% docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED              STATUS              PORTS                                                                                                        NAMES
a1631b90d924   test      "/usr/local/bin/entr…"   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp, 22/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:8080->8080/tcp, 3306/tcp, 0.0.0.0:8443->8443/tcp   thirsty_booth
% docker stop a1631b90d924
a1631b90d924

 其他資訊:changyy/docker-study/ubuntu18.04-php7.2-mysql5.7-phpmyadmin

2024年3月27日 星期三

PHP 開發筆記 - 使用 Docker 在 Ubuntu 22.04 和 PHP 8.1 架設 osTicket v1.18.1 環境,研究 Mail Parser 流程

接續上篇追蹤 osTicket 信件處理流程的筆記,這次用 Docker 包裝可運行和測試的獨立環境,主要採用 Ubuntu 22.04 與 PHP 8.1,並且規劃方式,建構出備份原始信件的架構,以及可反覆解析信件的流程。

首先是 Dockerfile ,主要設計從 GitHub.com 取出 v1.18.1.zip 並解壓縮 /var/www/osticket-v1.18.1,其餘是安裝相關套件和資料庫的帳號建立等等,最後再把 MySQL, PHP-FPM 和 nginx 運行起來,並預設把 nginx logs 寫到 stdout 觀看:

RUN wget https://github.com/osTicket/osTicket/releases/download/v1.18.1/osTicket-v1.18.1.zip -O /tmp/osticket.zip \
    && unzip /tmp/osticket.zip -d /var/www/ \
    && mv /var/www/upload /var/www/osticket-v1.18.1 \
    && cp /var/www/osticket-v1.18.1/include/ost-sampleconfig.php /var/www/osticket-v1.18.1/include/ost-config.php \
    && chown -R www-data:www-data /var/www/osticket-v1.18.1 \
    && rm /tmp/osticket.zip

...

RUN service mysql start && \
    mysql -e "CREATE USER 'developer'@'%' IDENTIFIED BY '12345678';" && \
    mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'developer'@'%' WITH GRANT OPTION;" && \
    mysql -e "CREATE DATABASE osticket_dev CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" && \
    mysql -e "GRANT ALL PRIVILEGES ON osticket_dev.* TO 'developer'@'%';" && \
    mysql -e "FLUSH PRIVILEGES;" 

...

CMD service mysql start && service php8.1-fpm start && service nginx start && tail -f /var/log/nginx/access.log /var/log/nginx/error.log;

接下來更動 include/class.mailfetch.php 檔案,讓他下載信件時,可以存一份在 /tmp 方便後續使用:

@file_put_contents("/tmp/debug-mail.$i", $this->mbox->getRawEmail($i));

最後,弄一隻 api/cron-dev.php 檔案,可以指定 RawMail 的格式路徑,從指定位置讀進來解析信件,如此靠 api/cron-dev.php 就可以輕鬆不斷實驗 MIMEDecode 流程:

# php api/cron-dev.php 
[INFO] Input: /tmp/debug-mail
[INFO] Input File not found

# php api/cron-dev.php /tmp/debug-mail.1
[INFO] Input: /tmp/debug-mail.1
Ticket Object
(
    [ht] => Array
        (
            [ticket_id] => 2
            [ticket_pid] => 
...
            [lastupdate] => 2024-03-27 21:35:52
            [created] => 2024-03-27 21:35:52
            [updated] => 2024-03-27 21:35:52
            [topic] => 
            [staff] => 
            [user] => User Object
                (
                    [ht] => Array
                        (
                            [id] => 2
                            [org_id] => 0
                            [default_email_id] => 2
                            [status] => 0
                            [name] => UserName
                            [created] => 2024-03-27 21:35:52
                            [updated] => 2024-03-27 21:35:52
                            [default_email] => UserEmailModel Object
                                (
                                    [ht] => Array
                                        (
                                            [id] => 2
                                            [user_id] => 2
                                            [flags] => 0
                                            [address] => user@example.com
...