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

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: 

2015年11月27日 星期五

DevOps 筆記 - 使用 Jenkins、Git、RPM、Ansible、Slack 建置持續整合架構(Continuous integration)

今天終於端出一盤比較好吃的菜了,用料有點雜筆記一下吧!之前已經用 Jenkins + Git + Slack 可以做到持續發布跟通知開發者了,但面對 AWS Auto Scaling 議題還是頗抖的,因為建立 AMI (Amazon Machine Image) 後,機器的狀態被保留在某一刻,這時服務量一大自動開啟機器時,就會出現服務更新的需求。原先同事採用 Puppet 方案,但仍不適合處理 Auto Scaling 的問題,因為 Puppet 需要比較強的關係式(Server/Client認證),雖然硬做也是 ok 的,但要一直維護 Server/Client 架構還是頗累,所幸有了 Ansible 的出現!這提供比較分散式的管理架構,Server 跟 Client 都是透過 keypair + ssh remote command 溝通的。

粗略配置作法:
  • Git
    • 主要特色還是 branch 的規劃,跟公司文化有關
      • master:正式網站的狀態
      • alpha:內部最新網站狀態
      • develop:最新程式碼
    • 開發者都是從 develop branch 開發服務,當有版本要釋出時,merge 進 alpha branch 後,發佈到 alpha site 測試,妥當後再把 alpha branch merge 進 master branch
      • 概念上有點從 develop -> alpha -> master 角度
      • 若有 hotfix 時,從 master 產生 branch 修正後,在 merge 回 alpha 跟 develop
      • 在 alpha site 測試出問題時,不斷地在 alpha branch 處理,到最後才 merge 回 develop branch,以及發佈到 master branch
    • 透過 Jenkins 每一次發版時,會下 git tag 標記狀態
      • develop-version-number
      • alpha-version-number
      • production-version-number
    • 相關資料:A successful Git branching model
  • Jenkins 
    • 新增 Slack Notification Plugin/Version Number Plug-In
    • 權限設定,為了不要暴露太多專案給開發者,做了簡易的權限管控
      • 設定全域安全性
        • 安全性領域:採用 Jenkins 內建使用者資料庫 且不允許註冊
        • 授權:採用專案型矩陣授權策略
          • 每一個 task 都可以自訂可以觀看的使用者
    • 管理方面,新增工作分成 build 跟 deploy 兩種
      • build 就是把 source code 包成 RPM ,方便作業系統管理和除錯追蹤,另外,對於開發者測試機的準備也有幫助
        • 取得最新程式碼後,透過 RPMBuild 包裝起來,擺至指定位置
        • 每一次 build 都會下 git tag 標記版本狀態,方便之後除錯
        • 若封裝過程可以接觸到 git 資訊,則把 source code 最後 3 筆 commit log 拉出來製作版本資訊
          • git log -3 --pretty=format:"{\"commit\":\"%H\",\"update\":\"%ad\",\"author\":\"%an\"}"
      • deploy 就是透過 ansible 進行發佈,因此也可以把 ansible 操作用 git 管理
        • 發布過程中,第一步先到指定位置找出最新的 RPM files
        • 第二部透過 AWS EC2 API 得知待更新機器列表
        • 透過 ssh remote command 進行機器更新
        • 盡量把所有動作濃縮至 Ansible command,而透過 Jenkins Web UI 撰寫幾個指令就好 
      • 工作通知
        • 每一次 build 或 deploy 都可以透過 slack plugin 將更新訊息發佈至指定頻道通知相關開發者
      • 採用輪詢 SCM 方式,由 Jenkins 主動確認程式碼狀態,以此決定是否出 build ,不採用 git hook 等被動方式進行工作任務
        • 無法規範開發者 git push 行為,擔心 push 太多造成大量 build event
      • build 完畢後,可以設立相關專案,自動進行 deploy 任務
  • AWS
    • 透過 AIM 建立一個只有 AmazonEC2ReadOnlyAccess 權限的帳號
    • 對於需被管理的機器,都透過 Tag 標記管理
      • 使用 AWS EC2 API 來動態查詢指定的機器,以此取得機器列表,例如指定 ELB 裡所有的機器
    • 關於 Auto Scaling
      • 由於機器 IP 非固定,在不同 data center 溝通容易踩到 Security Group 限制,建議還是把服務以 data center 切開
        • 例如有美西跟日本時,若 db server 開在美西,那需要存取此 db server 的機器就擺在美西就好,這樣 Security Group 比較好做(類似限制 LAN 即可)
  • Ansible
    • 將 RPM 複製到機器上安裝,在 Ubuntu server 可偷懶用 alien :P
    • 依照腳本,進行機器的環境配置,如 limits、web server 等
    • 埋入一個 boot script ,當開機時透過 ssh remote command 向 deploy server 要求更新自己
以上使用的相關資源:
  • RPM 打包目錄專用
    • https://github.com/changyy/rpm-builder
    • 若打包目錄有 .git 時,會抓出最後幾筆 commit log 塞進 version.json 檔案內,可協助往後的偵錯
    • 由於 RPMBuild 很吃環境變數,面對 Jenkins 同時出 build 會衝到共用資源的部分,在此已經透過修改 $HOME 目錄來避開共用資源的問題,因此不用再擔心 Jenkins 一次只能出一個 build
  • AWS EC2 機器查詢
    • http://docs.ansible.com/ansible/intro_dynamic_inventory.html#example-aws-ec2-external-inventory-script
    • Jenkins 建置範例(script),只要修改 yml 檔案名稱即可:
      • develop site
        • export ANSIBLE_HOST_KEY_CHECKING=False
          export EC2_INI_PATH=$WORKSPACE/dynamic-inventory-ec2.ini
          export AWS_ACCESS_KEY_ID=XXX
          export AWS_SECRET_ACCESS_KEY=OOO
          export DEPLOY_TARGET=site-develop.yml

          ansible-playbook $WORKSPACE/$DEPLOY_TARGET -i $WORKSPACE/bin/ec2.py --private-key=$HOME/.ssh/ansible-deploy.pem
      • alpha site
        • export ANSIBLE_HOST_KEY_CHECKING=False
          export EC2_INI_PATH=$WORKSPACE/dynamic-inventory-ec2.ini
          export AWS_ACCESS_KEY_ID=XXX
          export AWS_SECRET_ACCESS_KEY=OOO
          export DEPLOY_TARGET=site-alpha.yml

          ansible-playbook $WORKSPACE/$DEPLOY_TARGET -i $WORKSPACE/bin/ec2.py --private-key=$HOME/.ssh/ansible-deploy.pem
      • production site
        • export ANSIBLE_HOST_KEY_CHECKING=False
          export EC2_INI_PATH=$WORKSPACE/dynamic-inventory-ec2.ini
          export AWS_ACCESS_KEY_ID=XXX
          export AWS_SECRET_ACCESS_KEY=OOO
          export DEPLOY_TARGET=site-production.yml

          ansible-playbook $WORKSPACE/$DEPLOY_TARGET -i $WORKSPACE/bin/ec2.py --private-key=$HOME/.ssh/ansible-deploy.pem
  • Ansible 找尋最新 package files
  • Ansible 設置自動要求更新
    • $ ansible-galaxy install changyy.self-update 
    • https://github.com/changyy/ansible-role-self-update
    • 比較雜亂一點,原理:
      • 機器部署時,建立一個 auto update script (負責通報 deploy server 更新指定 host),將此 script 埋入開機執行區( /etc/rc.local )
        • 當機器啟動時執行該 script ,把部署此機器該有的資訊以 ssh remote command 丟給 deploy server 
      • 該 ssh remote command 包含的動作
        • 登入 deploy server 並切換至 ansible workspace
        • 透過 dynamic inventory 建立動態 host 資訊
        • 執行 ansble-playbook 批次指令
      • Deploy server workspace 範例:
        • $ cat bin/echo.sh
          #!/bin/bash
          echo "{\"$HOST\":$DATA}"

          $ HOST=MySite DATA=[\"SERVER_IP\"] bash bin/echo.sh
          {"MySite":["SERVER_IP"]}

          $ cd ansible-deploy-dir && \
          ANSIBLE_HOST_KEY_CHECKING=false \
          HOST=MySite \
          DATA=[\"SERVER_IP\"] \
          ansible-playbook MySite.yml \
          -i bin/echo.sh \
          --private-key=mysite-login-key.pem

2015年11月26日 星期四

Ansible 筆記 - 讓 Server 主動更新的方式

這陣子使用 Ansible 進行大部分的機器發佈,也進入了另一個瓶頸:如何讓 Server 自動啟動後也能更新至最新版。我把它當作 Server 主動更新的流程。

目前搭配 Jenkins + git,可以定期追蹤是否要出新版,一旦決定出新版後,就是一道道 Ansible 指令執行完成發佈。在架設 AWS Auto Scaling 時,將碰到服務量變大時,將會自動開啟機器,此時這個機器狀態是有機會是過時的,這時需要讓這些機器開啟啟動時進行軟體更新,來確保得到的資料是最新的。

透過 Dynamic Inventory 架構,仿造 AWS EC2 External Inventory Script 來執行,動態塞 host 資訊給 Ansible 即可,例如:

$ cat bin/echo.sh
#!/bin/bash
echo "{\"$HOST\":$DATA}"

$ HOST=target DATA=[\"server_ip1\",\"server_ip2\"] bash bin/echo.sh                                                
{"target":["server_ip1","server_ip2"]}

$ ANSIBLE_HOST_KEY_CHECKING=false HOST=webserver DATA=[\"localhost\",\"127.0.0.1\"] ansible webserver -i bin/echo.sh -m raw -a date -k
SSH password:
127.0.0.1 | success | rc=0 >>
Thu Nov 25 11:23:26 UTC 2015


localhost | success | rc=0 >>
Thu Nov 25 11:23:26 UTC 2015


因此,整個設計架構就是讓 servers 帶足資訊到發動 Deploy(ansible commands) server 請他更新自己即可!

2015年11月17日 星期二

Ansible 筆記 - 動態取得 EC2 機器進行發佈更新 (Dynamic Inventory) @ Ubuntu 14.04

摸了好一陣子,終於搞懂了 :P 簡單的說,採用 Dynamic Inventory 進行動態取得機器列表,此例是透過 EC2 相關工具來取得的。

$ echo `EC2_INI_PATH=ec2.ini AWS_ACCESS_KEY_ID=KEY_ID AWS_SECRET_ACCESS_KEY=ACCESS_KEY python ec2.py --refresh-cache`
{ "_meta": { "hostvars": { "IP" : { ... } } } }


其中 ec2.ini 內容:

[ec2]

#https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.ini
#regions = all
regions = ap-northeast-1
regions_exclude =

destination_variable = public_dns_name
vpc_destination_variable = ip_address
route53 = False

cache_path = /tmp
cache_max_age = 0

rds = False
elasticache = False

#instance_filters = tag:Ansible=*


整體上,執行 ansible-playbook 方式就改成:

$ ANSIBLE_HOST_KEY_CHECKING=false EC2_INI_PATH=ec2.ini AWS_ACCESS_KEY_ID=KEY_ID AWS_SECRET_ACCESS_KEY=ACCESS_KEY ansible-playbook mysite.yml -i ec2.py --private-key=ansible-deploy.pem

並且在 mysite.yml 中定義的 hosts 也需調整,例如想要透過 EC2 上的 tag 資訊,例如我對 EC2 上的機器標記 Tag 為 Ansible=deploy 機器發佈,這時可以先透過 python ec2.py 觀看想要的 tag 被轉成什麼,接著再去修改 mysite.yml 的 hosts 資訊即可。而 ANSIBLE_HOST_KEY_CHECKING=false 是用在略過 ssh key checking

例如:

$ echo `EC2_INI_PATH=ec2.ini AWS_ACCESS_KEY_ID=KEY_ID AWS_SECRET_ACCESS_KEY=ACCESS_KEY python ec2.py --refresh-cache`
{ "_meta": { "hostvars": { "IP" : { ... } } }, "tag_Ansible_deploy": [ "IP" ] }

$ cat mysite.yml
- hosts: tag_Ansible_deploy
  ...


最後一提,此例使用的 ec2.py 是從官方文件提到的,執行時需安裝 python-boto 環境,此例為了搭配 Jenkins 使用,盡可能把執行 AWS API 所需的資料都透過環境變數傳遞。

2015年10月20日 星期二

透過 Bitbucket、Jenkins、Slack 打造持續發佈打屁等自動化架構

透過 Bitbucket 作為 git 版本控制的服務托管處,而 slack 則是工程師打屁好去處,而 Jenkins 則是打造一鍵發佈的便利介面。雖說連續動作也可以化簡成一隻 script,但能夠端出 Web UI 介面,還是大勝!

首先提一下 git 版本控制,透過 Bitbucket 服務,人多的話還是花一點小錢了!在 Bitbucket 可以建立群組概念,大概可粗略分成 Administrators、Developer、Deploy 三個個群組(若要細分則可以變成 iOS Developer、Android Developer、Web developer 或是甚至以專案為單位等),這邊設計讓所以有成員都進 Developer 並且 Developer group 預設對所有專案擁有 read only 權限。而埋了一個機器人在 deploy group 中,預設沒有任何權限(機器人不需進入 developer group)。當有專案要搭建自動發佈流程時,請專案擁有者或管理者,讓專案可供 Deploy Group 有 write 權限,這是為了 Jenkins 發佈新版時,能夠打個 tag 記錄狀態。而 Deploy group 裡的機器人,就只需添加個 ssh key 即可。

在 Jenkins Web UI:
  1. 建立作業 => 填寫作業名稱 => 建置 Free-Style 軟體專案 
  2. 設定 Slack Notifications => 進階設定 Team Domain、Integration Token 和 Project Channel => Test Connection
  3. 原始碼管理 => git => git@bitbucket.org:group/project.git => Credentials 搭配 Bitbucket SSH key => Branches to build 要指定 => Additional Behaviours => Advanced sub-modules behaviours => Recursively update submodules
  4. 建置環境 => Create a formatted version number => 設定 Environment Variable Name (VERSION) => Version Number Format String (1.0.0) 
  5. 建置 => 例如拉完程式碼後想要做的事,如 /path/project-build.sh project-name  $VERSION $BUILD_NUMBER $JOB_NAME 
  6. 建置後動作 => Git Publisher => Push Only If Build Succeeds => Tag to push (develop-$VERSION-$BUILD_NUMBER) => Target remote name (origin)
如此一來,開發者可以用 bitbucket 持續開發,並且用不同 branch 管理,如 develop branch、alpha branch、release branch 等,而 Jenkins 就建立三個任務,分別從上述 branch 取資料出來使用,而在 "建置" 跟 "建置後動作" 都可以對各個 branch 操作做細部的調整。

例如"建置"中的 /path/project-build.sh 的程式,就可以包含建立 RPM 、更新 repos 等行為,同時有環境變數 $VERSION $BUILD_NUMBER $JOB_NAME 拿來輔助。

以上就是將程式碼編譯打包的流程,若要再添加上 deploy 流程時,可以再額外建置一個 Jenkins 工作,裡頭只需填寫 slack 通知和建置項目,而建置項目就可以一隻 script 處理發佈的動作即可,而在相對應工作中,把剛剛的 deploy task 建立相依性即可。

如此一來,當 Jenkins 每一次建置時,各個階段的都丟訊息至 slack 指定的頻道,等同通知相關人員。若需要做類似 git hook 來達成自動建置時,可以使用 Jenkins "建置觸發程式" 裡的輪詢  SCM 項目,例如每 10 分鐘確認一次 bitbucket 專案狀況等,達成類似 git hook 行為。

2015年5月19日 星期二

GAE 筆記 - 在 Ubuntu Server 上傳 Google App Engine 程式碼 @ Ubuntu 14.04

最近嘗試在 Ubuntu server 開發,以前常在 Windows 、 Ubuntu Desktop 和 Mac OSX 等視窗介面,最近 Google App engine 也把認證的部分換了,以前是在 Google App engine tools 上輸入帳號密碼,現在都改用 OAuth 認證方式,實在方便。

若要在 Ubuntu server 上運行 Google App Engine ,透過 Command line 即可完成測試跟發佈:

測試:

$ cd /path/gae/project
$ python -m py_compile *.py && dev_appserver.py --port=8080 .


發佈:

$ cd /path/gae/project
$ python -m py_compile *.py && appcfg.py --oauth2 --noauth_local_webserver -A YourGAEProjectID update .

過程只需再用 browser 瀏覽指定的 link 以及完成 oauth 的認證即可發布啦!

2014年5月7日 星期三

AWS 筆記 - 使用 Amazon EC2 進行 Deploying Web services

稍微把玩 AWS Elastic Load Balancing 跟 Auto Scaling 後,大概有一點粗淺的心得:
  • 開一台機器,如 micro 等級,並設定這台機器關機也不下線,可專門用於製作 AMI
  • EC2 micro 採用 EBS 管理,每一次製作 AMI 會產生新的 EBS snapshot,記得定時去清理
  • 程式碼若是透過 git 管理,可以在 /etc/rc.local 上設定開機自動更新方式,至少 AMI 不是最新程式碼時也還能堪用(但 git server 掛了就...)
  • 如果想要透過 web cgi 更新,記得把 source tree 的 owner 或 group (搭配775方式) 設定 www-data,可以用 sudo -u www-data /tmp/update.sh 進行測試
AWS Elastic Load Balancing:
  • 使用 AWS ELB 時,可以讓多台機器綁定在固定的 DNS Name,記得需要處理一下多台機器進行切換其 Session/Cookie 問題,例如單純的使用情境,可以透過設定 Stickiness: LBCookieStickinessPolicy, expirationPeriod='600' 等方式來應付等
  • 對於有使用 HTTP Authentication 的 web server 而言,因為帳密是一直隨 browser 傳給 web server 的(就像 cookie一樣),所以 ELB 再切換機器時,不會因機器不同台而再次詢問帳密
AWS Auto Scaling:
  • 對 Auto Scaling 而言,預設機器都是關機後就下線,除非改掉預設方式,不然更新系統時 reboot 的結果就是開一台新機器,此外,把 Auto Scaling Group 刪掉等同把機器都下線
  • 透過製作新的 AMI 來取代系統更新時,而原先設定好的 Auto Scaling Launch Config 不能更新採用的 AMI,但可透過新增新的 Launch Config 後,更新 Auto Scaling 設定後,再刪掉舊的 Launch config 等