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

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: 

2018年5月9日 星期三

Alexa Skill 開發筆記 - 使用 AWS Lambda 與 OAUTH2 帳號連結

公司的產品是 WiFi Display 設備,結合智慧音箱後,透過音控請裝置執行動作,如播放影片。在此就順便紀錄這些開發過程。此處不會提及 OATUH2 開發項目。

首先,Alexa Skill 是個滿好玩的點子,說穿了就像 Apple TV 可以安裝 Apps 的概念,他是個市集,可以讓開發者提供更多智慧音箱的技能擴充,大約是 2015 年開始的。上頭也可以有簡單的變現機制,可以參考:Alexa開發者也可以賺錢了!看亞馬遜怎麼開啟語音應用的全新獲利模式

首先,沒有 Alexa device 也可以開發,善用模擬器即可。開發流程:

  1. 建立 Alexa Developer 帳號(過程就是建立 Amazon 帳號)
  2. 建立 AWS 帳號(綁定信用卡,有免費額度可善用)
  3. 使用模擬器服務 - https://echosim.io/


01-skill01

02-skill02

先在 Alexa Developer 先建立一個 Skill project,此例是 Video ,接著就要填寫 AWS Lambda 的位置,就切換到 AWS Lambda 創建流程,而 Alexa Skill 預設是提供 English (US) 的語系,參照 To create a Lambda function 第三步有強調 AWS Region 的部分:
Make sure you’ve selected the N.Virginia for English (US) skills or the EU (Ireland) region for English (UK) and German skills. The region is displayed in the upper right corner. Providing your Lambda function in the correct region prevents latency issues.
就要在 N.Virginia 創建 AWS Lambda function,不然亂建立根本無法互動。建立完 AWS Lambda function 後,替他增加 Alexa Skill Kit 和 Alexa Smart Home ,並且設定它的 Skill ID,並且把對應的程式碼上傳,且要記得發布出去!

03-aws01

04-aws02

05-aws03

如此一來,此 Skill 作者基本上已經可以測試了,而測試的條件包括要有一個 Alexa 音控裝置,這時就要靠模擬器,可以先到 https://echosim.io/ 登入 Amazon 帳號,即可獲得一台虛擬器。

15-simulator

接著,若你人在美國或是擁有美國的 iTunes / Google play 帳號可以下載得到 Amazon Alexa app ,那就直接用,不然只好跟我一樣用用網頁版: https://alexa.amazon.com/spa/index.html ,只要有先把 Amazon 帳號配對過 Alexa 音箱就可以正常切換到 Skill 頁面,點選右上角的 Your skill 即可切換到 Dev skill 來查看,並把自己開發的 skill 給 enable,過程就會觸發 OAUTH2 服務帳號的綁定並授權 Skill 服務使用等等,後續的過程先挑選哪個硬體裝置要被智慧音箱控制(AWS Lambda 要實作 device discovery 等機制),接著再挑哪個智慧音箱來搭配,整個過程用瀏覽器開發工具查看到 api response:

  • https://alexa.amazon.com/api/phoenix/discovery
    • 得知哪些硬體裝置要被控制
  • https://alexa.amazon.com/api/devices-v2/device
    • 得知你的帳號有哪些智慧音箱裝置


06-skill03

07-skill04

08-skill05

09-skill06

10-skill07

11-skill08

12-skill09

最後,自己測試完後,可以再開啟 Beta Test 機制,邀請其他人來測試。而要開啟 Beta Test 只需要把資料填一填,並處於 ready for submission 即可(送審前一步),關鍵的地方就是補一補 icon、描述、甚至一些網址等等,若只要一直內測就可以先亂填

13-skill10

14-skill11

最後,有問題真的要好好看文件 XD 而 AWS Lambda 則可以善用 Cloud Watch 看 logs ,以此確保真的有連線

2018年1月20日 星期六

AWS 筆記 - 使用 Ubuntu server 更換 Windows server 2012 帳號狀態

AWS Windows server

公司在三年前在 AWS Beijing 開了一台 Windows server 2012 運行一些套裝軟體,然而,負責的 IT 在整理系統帳號時,把 guest 關閉了。接著發生很多奇妙事件,最恐怖的是機器無法遠端登入了 Orz

一開始想朝更換 Administrator 密碼試試,結果一看過程步驟也不少:
使用 EC2Config 重置 Windows 管理员密码 -
https://docs.aws.amazon.com/zh_cn/AWSEC2/latest/WindowsGuide/ResettingAdminPassword_EC2Config.html
接著,反而朝惡搞的方式進去研究,例如:如何修改機碼啟用 guest 帳號


最後,使用了前一份(一年前版本)的全系統備份,開了一台機器,把他的 C:/Windows/System32/config/SAM 複製出來,覆蓋掉無法登入的機器上,如此一來就搞定了!

情況簡介:
A server: 壞掉的 Windows server 2012
B server: 從 A server 現狀建置 AMI 而開啟的機器
C server: 前一版次備份的 Windows server
D server: Ubuntu 14.04 server
A 計畫:
將 B server 的 C 槽卸下,並掛到 C server 上進行 B server 的 C槽 資料修改,改完後再卸下,重裝到 B server,把 B server 重新啟動,看看是否正常

然而,A 計劃失敗了,在 AWS console 可以看到系統無法正常通過檢驗,於是啟動了 B 計畫

B 計畫:
再次建置乾淨的 B server,將 B server 的 C 槽卸下,也把 C server - C 槽卸下,並一起掛到 D server 上,拿著 C server - C 槽關鍵資訊,直接覆蓋掉 B server - C 槽 資料,修改完後再卸下,重裝到 B server,再把 B server 重新啟動
果真 B 計劃成功了 :p 猜測 A方案 可能因為修改的是 Windows OS level 資訊,修改完可能也改變了 B server - C 槽狀態,以至於重新啟用時反而資訊對不上而無法開機。

AWS Windows server

其他筆記:

  1. Windows 需要 stop 機器,才能卸下指定硬碟,請開啟 “終止保護“ 避免不小心做錯事
  2. Windows 的系統碟(C:)根设备用 /dev/sda1 資訊,卸下後要重新裝上時,記得要用原本的 /dev/sda1 才行
  3. 在 EC2 - EBS 管理介面要卸下某硬碟時,建議把 Name 帶點原本的機器資訊,方便找尋
  4. 在 Ubuntu 要 mount NTFS 都還算簡單,sudo mount /dev/xsgf1 /mnt/Win2012CDisk 
  5. Windows 的帳號資訊擺在 C:\Windows\System32\config\SAM 檔案,可以單純做 cp /mnt/C_server_c_diskt/Windows/System32/config/SAM /mnt/B_server_c_diskt/Windows/System32/config/SAM 
  6. 驗證完畢後,就可以安心地把 A server 先暫停 -> 把系統槽卸下 -> 掛到 Ubuntu server -> mount 起來 -> 覆蓋掉 SAM -> 卸下 -> 掛回 A server -> A server 重新開機

2017年6月14日 星期三

AWS 筆記 - AWS ELB 與 Nginx 之 DOS 惡意攻擊者的處理方式

這個緣由是這樣的,有一台 Server 被狂打,但架構是:

Remote client <-> AWS ELB <-> Web server (Nginx) <-> Application

當有用戶非常好心地發大量 request 來檢查機器漏洞,若 Nginx 預設都沒多做設定,那 access.log 都會只記錄到 AWS ELB IP ,也就是不能把 access.log 紀錄的 IP 拿來 ban ,會變成 ban 掉 ELB。

而 AWS 提供的 Security group 預設是從 Allow 的角度,若要特意 ban 掉某個 IP 會有點難搞,例如要改寫防火牆規則,因此,最後就改成從實體 server 去阻擋遠端 client request 了。

步驟一:先讓 Nginx 可以記錄到真實的 remote ip

/etc/nginx/nginx.conf

http {
    # ...
    real_ip_header X-Forwarded-For;
    set_real_ip_from 0.0.0.0/0;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    # ...
}


如此一來,access.log 的 remote_addr 就可以不是 AWS ELB IP 了。

步驟二:用 Nginx 去 deny ip

/etc/nginx/conf.d/your-service.conf

server {
  # ...
  deny RemoteIP;
  # ...
}


這樣設定後,在用 sudo nginx -t 檢查語法後,就可以啟用了

在此不用 iptables 去阻擋的主因是封包進來的 IP 都是 AWS ELB ,所以才改從 Nginx/App 這層取阻擋。缺點就是 access.log 還是一直肥,好處是可以觀察 access.log 看看對方是不是放棄攻擊了?XD

未來若碰到 DDOS 就...

2017年5月21日 星期日

[PHP] 使用 AWS SDK 進行 S3 檔案上傳

寫過又忘了,筆記一下,環境是 PHP 5.5.9:

<?php
require 'aws.phar'; // http://docs.aws.amazon.com/aws-sdk-php/v3/download/aws.phar //aws-3.27.5.phar
$s3 = Aws\S3\S3Client::factory(array(
'region' => 'us-west-2',
// http://docs.aws.amazon.com/aws-sdk-php/v3/api/index.html
'version' => '2006-03-01',
'credentials' => array(
'key' => 'key',
'secret' => 'secret',
),
        ));

try {
$result = $s3->putObject(array(
'Bucket' => 'my-bucket',
'Key' => 'my-object-key'
'SourceFile' => $_FILES['userfile']['tmp_name'],
        ));
} catch (Exception $e) {
             
echo $e->getMessage() . "\n";
}

print_r($result);
// $result['ObjectURL'];


Aws\Result Object
(
    [data:Aws\Result:private] => Array
        (
            [Expiration] =>
            [ETag] =>
            [ServerSideEncryption] =>
            [VersionId] =>
            [SSECustomerAlgorithm] =>
            [SSECustomerKeyMD5] =>
            [SSEKMSKeyId] =>
            [RequestCharged] =>
            [@metadata] => Array
                (
                    [statusCode] => 200
                    [effectiveUri] => https://s3-us-west-2.amazonaws.com/my-bucket/my-object-key
                    [headers] => Array
                        (
                            [x-amz-id-2] =>
                            [x-amz-request-id] =>
                            [date] =>
                            [etag] =>
                            [content-length] => 0
                            [server] => AmazonS3
                        )

                    [transferStats] => Array
                        (
                            [http] => Array
                                (
                                    [0] => Array
                                        (
                                        )

                                )

                        )

                )

            [ObjectURL] => https://s3-us-west-2.amazonaws.com/my-bucket/my-object-key
        )

)

2016年8月19日 星期五

AWS 筆記 - 透過 VPN/IPSEC 讓 AWS VPC 與 Microsoft Azure VirtualNetwork 互通

對 Azure 來說,預設建置的 VirtualNetwork 就是 10.0.0.0 開頭的網路環境,大多可以繼續延用下去。在這延續上一篇 AWS 筆記 - 建置自己的 VPC 且透過 VPN/IPSec 讓各個 Region 的 VPC 互通 ,先簡略調整 Azure VirtualNetwork 環境,接著建置 Azure VPC 與 AWS VPC 連線機制。

假設 AWS VPC 為 10.100.0.0/16 網域,而 Azure VirtualNetwork 為 10.50.0.0/16 網域。

設置 Azure VPC:

Azure Portal -> New
-> Resource -> Create -> name=StudyVPN, Location=WestUS
-> Networking -> VirtualNetwork
-> Name=US
-> Address Space=10.50.0.0/16
-> Subnet Name=default
-> Subnet address range=10.50.0.0/24
-> Location = WestUS
-> Virtual Machines -> Ubuntu Server 14.04
-> Name=VPN
-> Location = WestUS
-> DS1_V2 Standard

請先將 AWS EC2 Security Group 跟 Azure Network security group 都設置好、開放對方 VPN IP。此例為 AWS EU-West-1 機器跟 Azure WestUS 機器,設置完防火牆後,先 ping 一下彼此吧。

在 EU-West-1 的 VPN Server 機器,其 public ip = 52.209.16.24:

$ ping 13.88.186.107

在 Azure WestUS 的 VPN Server 機器,其 public ip = 13.88.186.107:

$ ping 52.209.16.24

先設置 Azure VPN server:

$ sudo apt-get update && sudo apt-get -y upgrade && sudo apt-get -y dist-upgrade && sudo apt-get autoremove && sudo apt-get autoclean

$ sudo apt-get install openswan
$ ls /etc/ipsec*
/etc/ipsec.conf  /etc/ipsec.secrets

/etc/ipsec.d:
aacerts  cacerts  certs  crls  examples  ocspcerts  policies  private
$ sudo ipsec setup version
Linux Openswan U2.6.38/K3.19.0-65-generic (netkey)
See `ipsec --copyright' for copyright information.
$ sudo vim /etc/ipsec.d/AWS-EU-WEST-1.conf
conn Connect-to-AWS-EU-WEST-1
 type=tunnel
 authby=secret

 leftid=13.88.186.107
 left=%defaultroute
 leftsubnet=10.50.0.0/16
 leftnexthop=%defaultroute

 right=52.209.16.24
 rightsubnet=10.100.0.0/16

 pfs=yes
 auto=start
$ sudo vim /etc/ipsec.d/AWS-EU-WEST-1.secrets
13.88.186.107 52.209.16.24 : PSK "IPSEC_PASSWORD"

$ sudo vim /etc/ipsec.conf
#protostack=auto
protostack=netkey

#...
include /etc/ipsec.d/AWS-EU-WEST-1.conf

$ sudo vim /etc/ipsec.secrets
include /etc/ipsec.d/AWS-EU-WEST-1.secrets

$ sudo ipsec setup restart
ipsec_setup: Stopping Openswan IPsec...
ipsec_setup: Starting Openswan IPsec U2.6.38/K3.19.0-65-generic...


更新 AWS VPN server:

$ sudo vim /etc/ipsec.d/AZURE-US-WEST.conf
conn Connect-to-AZURE-EU-WEST
 type=tunnel
 authby=secret

 leftid=52.209.16.24

 left=%defaultroute
 leftsubnet=10.100.0.0/16
 leftnexthop=%defaultroute

 right=13.88.186.107
 rightsubnet=10.50.0.0/16

 pfs=yes
 auto=start
$ sudo vim /etc/ipsec.d/AZURE-US-WEST.secrets
$ sudo ipsec setup restart
ipsec_setup: Stopping Openswan IPsec...
ipsec_setup: Starting Openswan IPsec U2.6.37/K4.4.11-23.53.amzn1.x86_64...
ipsec_setup: /usr/libexec/ipsec/addconn Non-fips mode set in /proc/sys/crypto/fips_enabled


當兩邊的設定好後,並且用 ipsec setup restart 後,另一方才可以用指令連上對方:

Azure VPN server:

$ sudo ipsec auto --up Connect-to-AWS-EU-WEST-1
117 "Connect-to-AWS-EU-WEST-1" #4: STATE_QUICK_I1: initiate
004 "Connect-to-AWS-EU-WEST-1" #4: STATE_QUICK_I2: sent QI2, IPsec SA established tunnel mode {ESP=>0x29ba22d7 <0x68b944dd xfrm=AES_128-HMAC_SHA1 NATOA=none NATD=52.209.16.24:4500 DPD=none}


AWS VPN server:

$ sudo ipsec auto --up Connect-to-AZURE-EU-WEST
117 "Connect-to-AZURE-EU-WEST" #6: STATE_QUICK_I1: initiate
004 "Connect-to-AZURE-EU-WEST" #6: STATE_QUICK_I2: sent QI2, IPsec SA established tunnel mode {ESP=>0x74246b5f <0x7156f72b xfrm=AES_128-HMAC_SHA1 NATOA=none NATD=13.88.186.107:4500 DPD=none}


並且兩邊就可以 ping 彼此的 private ip 囉!

接著還需設定 routing table 以及對 AWS VPN Server機器打開 Change Source/Dest. Check 、對 Azure VPN Server 機器的網卡打開 IP forwarding 後,兩邊機器就可以互相 ping  到啦:

對 AWS 而言,在 VPC Dashboard -> Route 中,將相關的子網域都添加一則 10.50.0.0/16 導向至 AWS VPN Server (別忘了,若第一次設置 EC2 上的 VPN Server,還要從 EC2 Dashboard 修改該機器,取消 Change Source/Dest. Check)

對 Azure 而言,Azure portal -> New -> Networking -> Route table ,建立後,再去設定

Resource -> Route table -> Settings
-> Routes -> Add -> Route name=ToAWS, Address prefix=10.100.0.0/16, Next hop type=Virtual appliance -> Next hop address=10.50.0.4 (Azure VPN Server Private IP)
-> Subnet -> Associate subnet -> 挑選 VirtualNetwork、挑選 Subnet

最後,記得 NAT 相關要設定好,像在 Azure 上的 VPN server 是從一台乾淨的 Ubuntu 14.04 裝後,還必須設定以下:

$ echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
$ echo "net.ipv4.conf.all.accept_redirects = 0" | sudo tee -a /etc/sysctl.conf
$ echo "net.ipv4.conf.all.send_redirects = 0" | sudo tee -a /etc/sysctl.conf
$ for vpn in /proc/sys/net/ipv4/conf/*; do echo 0 | sudo tee $vpn/accept_redirects; echo 0 | sudo tee $vpn/send_redirects; done
$ sudo sysctl -p


其他備註:

$ sudo ipsec setup restart
ipsec_setup: Stopping Openswan IPsec...
ipsec_setup: Starting Openswan IPsec 2.6.38...
ipsec_setup: No KLIPS support found while requested, desperately falling back to netkey
ipsec_setup: NETKEY support found. Use protostack=netkey in /etc/ipsec.conf to avoid attempts to use KLIPS. Attempting to continue with NETKEY


解法 => protostack=netkey

$ sudo ipsec auto --up Connect-to-AWS-EU-WEST-1
022 "Connect-to-AWS-EU-WEST-1": We cannot identify ourselves with either end of this connection.


解法 => 先把一些錯誤訊息修掉,如 sudo ipsec setup restart 看到的問題,接著再看 conn 的設定,當設定錯誤也會看到這段訊息。

2016年8月17日 星期三

AWS 筆記 - 建置自己的 VPC 且透過 VPN/IPSec 讓各個 Region 的 VPC 互通

Amazon VPC 的全名為 Amazon Virtual Private Cloud,就是所謂的內網架構,在此網路架構內的機器可以互通有無。https://aws.amazon.com/tw/vpc/

假設使用情境:
  1. 建立一個 VPC 網段為 10.100.0.0/16
  2. 建立兩個 Subnet 網段 A 跟 B 分別為 10.100.1.0/24, 10.100.2.0/24 且為不同 Availability Zone
  3. 其中 A 網段不給予 Public IP ,而 B 網段會有 Public IP
  4. 其中 A 網段需建立 NAT 運作方式,可以連外,也可以被 B 網段連到
建置方式:

VPC dashboard
-> Your VPCs -> Create VPC
-> 給定 CIDR block,如 10.100.0.0/16
-> Subnets -> Create Subnet
-> 挑選剛剛建立 VPC 以及 Availability Zone (兩個 Subnet 挑不同區),以及給定 CIDR block
-> A 網段 10.100.1.0/24
-> B 網段 10.100.2.0/24 並打開 Auto-Assign Public IP 屬性
-> Internet Gateways -> Create Internet Gateway
-> 勾選後,點擊 Attach to VPC -> 挑剛剛的 10.100.0.0/16
-> Nat Gateways -> Create NAT Gateway
-> 在此只需建立 A 網段的 NAT Gateway,但要建立在 B 網段,才能讓 A 網段的機器連出去 -> 給予 EIP/Create New EIP
-> Route Tables -> Create Route Table
-> 建立 A 網段 10.100.1.0/24 專屬表 -> 並設置 0.0.0.0/0 給予剛剛建立的 NAT   -> 建立 B 網段 10.100.2.0/24 專屬表 -> 並設置 0.0.0.0/0 給予剛剛建立的 Internet Gateway
-> 回到 Subnets 分別挑選 A/B 網段並更新他們的 route table

如此一來,就有一個 VPC = 10.100.0.0/16 ,內有兩個 subnet = A網段 10.100.1.0/24, B網段 10.100.2.0/24,其中 A 網段聯外方式就是透過 NAT 出去(對外會看到NAT IP),而 B 網段則是用 public ip 透過 Internet Gateway 出去。機器們可以用 curl ipino.io 去驗證 IP ,以及使用 ping 來檢驗是不是可以連到彼此。以上就是自建 VPC 且擁有 public ip 跟 private ip 網段的部分。

接著進行不同 Region VPC 溝通方式,不同 Region VPC 要溝通的方式,就只是單純在各個 Region 架設 VPN/IPSEC Server 互連,除了彼此更新 Route Table 外,還需在 VPN 機上打開防火牆、取消封包檢查等,如此一來即可處理封包移動流程。

假設有 10.100.0.0/16 跟 10.200.0.0/16 兩個網段,一處在 EU-West-1 另一處在 SA-EAST-1 ,一樣透過上述先建立完 VPC-EU-West-1 跟 VPC-SA-EAST-1,並且分別建立 Subnet EU-WEST-1 10.100.1.0/24 跟 SA-EAST-1 10.200.1.0/24 且兩者都有 Public IP(在此不做 private subnet),接著分別在這兩各區域,各開一台機器安裝 IPSEC,在此直接挑 Community AMIs -> amzn-ami-vpc-nat -> amzn-ami-vpc-nat-hvm-2016.03.3.x86_64-ebs CentOS ,並且使用 ec2-user 帳號登入。建議先在兩台 VPN server 彼此 ping 一下對方(在 Security Group 可以偷懶開放對方 IP 所有網路流量),接著就設定 IPSEC 的部分:

兩台機器都安裝 openswan 軟體:

$ sudo yum install openswan
$ sudo ls /etc/ipsec*
/etc/ipsec.conf  /etc/ipsec.secrets

/etc/ipsec.d:
policies


在 EU-WEST-1 機器,其 public ip = 52.209.16.24:

$ sudo vim /etc/ipsec.d/SA-EAST-1.conf
conn Connect-to-SA-EAST-1
type=tunnel
authby=secret

leftid=52.209.16.24
left=%defaultroute
leftsubnet=10.100.0.0/16
leftnexthop=%defaultroute

right=52.67.138.147
rightsubnet=10.200.0.0/16

pfs=yes
auto=start
$ sudo vim /etc/ipsec.d/SA-EAST-1.secrets
52.209.16.24 52.67.138.147: PSK "IPSEC_PASSWORD"
$ sudo grep -c "^include /etc/ipsec.d/" /etc/ipsec.conf > /dev/null || (echo "include /etc/ipsec.d/*.conf" | sudo tee --append /etc/ipsec.conf)
$ sudo grep -c "^include /etc/ipsec.d/" /etc/ipsec.secrets > /dev/null || (echo "include /etc/ipsec.d/*.secrets" | sudo tee --append /etc/ipsec.secrets)
$ sudo ipsec setup restart
$ sudo ipsec auto --up Connect-to-SA-EAST-1
117 "Connect-to-SA-EAST-1" #3: STATE_QUICK_I1: initiate
004 "Connect-to-SA-EAST-1" #3: STATE_QUICK_I2: sent QI2, IPsec SA established tunnel mode {ESP=>0xe7553eef <0xa8498182 xfrm=AES_128-HMAC_SHA1 NATOA=none NATD=52.67.138.147:4500 DPD=none}


在 SA-EAST-1 機器上,其 public ip = 52.67.138.147:

$ sudo vim /etc/ipsec.d/EU-WEST-1.conf
conn Connect-to-EU-WEST-1
type=tunnel
authby=secret

leftid =52.67.138.147
left=%defaultroute
leftsubnet=10.200.0.0/16
leftnexthop=%defaultroute

right=52.209.16.24
rightsubnet=10.100.0.0/16

pfs=yes
auto=start
$ sudo vim /etc/ipsec.d/EU-WEST-1.secrets
52.67.138.147 52.209.16.24 : PSK "IPSEC_PASSWORD"
$ sudo grep -c "^include /etc/ipsec.d/" /etc/ipsec.conf > /dev/null || (echo "include /etc/ipsec.d/*.conf" | sudo tee --append /etc/ipsec.conf)
$ sudo grep -c "^include /etc/ipsec.d/" /etc/ipsec.secrets > /dev/null || (echo "include /etc/ipsec.d/*.secrets" | sudo tee --append /etc/ipsec.secrets)
$ sudo ipsec setup restart
$ sudo ipsec auto --up Connect-to-EU-WEST-1
117 "Connect-to-EU-WEST-1" #5: STATE_QUICK_I1: initiate
004 "Connect-to-EU-WEST-1" #5: STATE_QUICK_I2: sent QI2, IPsec SA established tunnel mode {ESP=>0x681a5ce6 <0xcb613471 xfrm=AES_128-HMAC_SHA1 NATOA=none NATD=52.209.16.24:4500 DPD=none}


接著,就可以在兩台機器上,分別 ping 對方的 private ip 來測試,如此一來就算打通一條連線,最後要做的就是設定 VPC 的 route table:

在 EU-WEST-1 VPC (10.100.0.0/16) 的所有 Subnet (例如10.100.1/16),多添加一條 routing rule = 10.200.0.0/16 而 Target 用 EU-WEST-1 上的 VPN Server (可以在 Target 上打 i 關鍵字,就可以看到 EC2 server);在 SA-EAST-1 VPC (10.200.0.0/16) 裡所有的 Subnet (例如 10.200.1.0/16) ,多添加一條 routing rule = 10.100.0.0/16 而 Target 用 SA-EAST-1 上的 VPN Server 。

最後,回到 EC2 Dashboard ,將各區域的 VPN Server 的設定更新一下:Actions -> Networking -> Change Source/Dest. Check -> Disable,如此一來,雙方 VPC 下的機器,就可以互相 ping 和連線了。

在此使用 IPSEC 的做法,是因為後續可以來做跨 ISP 的結合,如 Azure service 等。

2016年8月15日 星期一

AWS 筆記 - 在 AWS Billing 使用 Tag 關注各個服務的開銷

AWSBillingGroupByTag

前陣子在追一些花費時,跟同事花了不少功夫在節費,同事頗厲害,一下就看到不少可省錢的地方,只是省了一輪後,還是希望能夠查看各項服務的開銷,最好每月追蹤,然而,在 AWS Billing 預設報告都是以 AWS Service 為單位,而非依照我們自己的服務為單位,例如一個服務用到數台 EC2 跟一台 RDS 時,希望把這費用可以記在一起,特別又有流量等問題等。

結果翻一下 AWS Billing ,果然已有這類功能,頗夠用!只需在 EC2/RDS 的機器上都多一個 tag 來辨識,例如 tag=Price 且同一個服務就用一樣的數值(例如服務網址),並且在 Load Balancer 和 Auto Scaling 都可以添加 tag 。

接著在 AWS Billing 的 Cost allocation tags 上,挑選剛剛新增完的 Price 標籤並 Save 起來,等幾天後,就可以用 AWS Billing - Cost Explorer 看到漂亮的圖了(用 Group by Tag => Price),並支援以天或月顯示,並且會有明確地數字表格顯示各個服務花了多少錢。

2016年7月27日 星期三

Microsoft Azure 管理筆記 - 仿 AWS 使用 Load Balancer (Azure: Load Balancer/Availability Set)

以 AWS 來說,部署服務會想要用 Auto Scaling 搭配 Load Balancer 來達成 high availability 的架構,而 Load Balancer 扮演著將 requests 導向至一群機器。就 Azure 的服務情況,拆解後包括 Availability Set、Load Balancer 項目。

以把玩的流程來看,需要先搞懂 Availability Set,其建置流程包括 Fault domains 跟 Update domains 關鍵字(可以參考這篇完整介紹 Azure Exam Prep – Fault Domains and Update Domains )

azure_try_to_move_into_aset

接著,在 Availability Set 中添加一台機器(沒機器時,Load balancer 無法設定),這步只能在新開機器時設定,不能對已經開啟的機器設定。

透過新增機器時,設定 Availability Set:
New -> Virtual machine -> Create -> 基本設定後, 在第三步 Settings - Availability 項目中,可以指定 Availability Set。
建置完後,緊接著建立 Load Balancer,建完後在其 Load Balancer -> Settings -> Backend pools -> Add a virtual machine -> Availability Set -> 挑選剛剛建立的 Availability Set(若 Set 裡沒機器,則會無法挑選!)-> 挑選裡頭的機器!

azure_lb_probes

azure_lb_rules

接著處理 requests 導向問題,例如開放 HTTP 80/443, TCP 22 port:
Load Balancer -> Settings -> Probes: 這就像 AWS Load Balancer 檢查某端口服務是否正常
Load Balancer -> Settings -> Load balancing rules: 正式把 requests 導向給後端機器群
azure_vm_network_security_group

此外,別忘了對剛剛建立的 VM 設定好防火牆(Network security group, 如 VM -> VM Settings -> Network interface -> settings -> )
Network security group -> Settings -> Inbound security rules (有需要可以在設定 Outbound security group),例如預設有開通 22 port ,此時就只需再添加 80, 443 port
如此一來,當 Load balancer 的 probes 檢驗通常順暢後,就可以對 load balancer 送資料試試了。

檢驗流程:
  1. 先檢查 VM 是否運作正常,如有 public ip 就從外面測試 telnet vm_ip port 做檢查,可以先裝個 nginx 或用 sudo python -m SimpleHTTPServer 443 / sudo python -m SimpleHTTPServer 80 應急一下
  2. 對 Load balancer 檢驗,如 telnet load_balanacer_ip port
以上則完成 AWS Load balancer 建置。