2021年11月27日 星期六

Terraform, Ansible, AWS CodePipeline 的 EC2 單體應用程式佈署策略

EC2 單體應用程式佈署策略,使用 AWS Code Pipeline、 Code Build 以及 Terraform、 Ansible 建立 staging、production 伺服器環境,為單體應用程式 Deploy 建立一套 Code Pipeline 流程,從建置到佈署。


 整體架構

  1. 使用 Terraform 開啟 EC2, Pipeline 服務 (terraform 開出 s3 artifacts)
  2. 使用 Ansible service role 自動化安裝服務
  3. 安裝 Codedeploy-agent
  4. 寫單體應用程式佈署腳本, scripts, service 檔案
  5. 寫 CodeBuild, Deploy 使用的腳本 appspec.yml, buildspec.yml

這一篇是想要利用 CodePipeline 服務達到: 把 Code 推到 staging or production 分支,就自動跑 Build,然後自動 Deploy 上線服務。

除此之外,EC2 的啟動、環境安裝都會透過 Terraform, Anaible 進行。


準備 Terraform - 模組


這裡使用的模組是本篇文章自己寫的 Terraform Module,模組請參考這個 Repo: 

https://github.com/hpcslag/infrastructure_boilerplate/tree/main/terraform/modules/server

locals {
  env          = var.env
  project_name = var.project_name
}

resource "aws_codedeploy_app" "app" {
  compute_platform = "Server"
  name             = var.project_name
}


module "my_deployment_group" {
  source                = "./modules/deployment_group"
  deployment_group_name = "my_deployment_group"
  app_name              = aws_codedeploy_app.app.name
}

module "my_server" {
  source = "./modules/server"
  count = length(var.env) # ["staging", "production"]

  aws_region = var.aws_region

  key_name      = aws_key_pair.deployer.key_name
  instance_type = length(regexall(".*production.*", var.env[count.index])) > 0 ? "t2.medium" : "t2.small"
  volume_size = 20 # 20 GB

  namespace = "my_server_${var.official_api_env[count.index]}"
  deployment_group_name = "my_deployment_group"
}


要稍微注意,這裡啟動機器是用 list, ["staging", "production"],千萬不要莫名其妙減少一個值,這裡都是按照順序建立 server,很可能會因為增減搞壞,如果有個別處理需求,建議分開寫,不要加到 list 中。


服務寫好之後,直接啟動就會得到 CodeBuild 和 Pipeline,但還沒有辦法直接使用,要使用 Ansible 去機器安裝,但在 Ansible 安裝之前,要把機器資訊導出給 Ansible ,也就是要導出 ssh.config 和 hosts 檔案,而且要自動產生,避免太多人工。


寫一個 output.sh 方便處理


在寫一個 output.sh 之前,需要有一個 output.tf 的 output 定義,才能撈出資料: 

https://github.com/hpcslag/infrastructure_boilerplate/tree/main/terraform/modules/server

output "my_server" {
    value = flatten([
        for data in module.my_server : {
            public_ip = data.public_ip
            namespace = data.namespace
        }
    ])
}

這個寫法的意思是 my_server 是一個 counting 的 terraform modules,它就會把每一個建立出來的 EC2 自動填到這個 list 裡面。


因此你就可以建立一個腳本 output.sh 處理它,自動放到 ansible 目錄底下:

terraform output -json my_server | jq -r '.[] | "Host \(.namespace)
    Hostname \(.public_ip)
    User ubuntu
    IdentityFile ~/.ssh/id_rsa"' >> ssh.config

echo "[my_server]" >> hosts
terraform output -json my_server | jq -r '.[] | "\(.namespace)"' >> hosts


執行之前,可能需要改一下權限,使用 sudo chmod +x output.sh 可以得到執行它的權限。


準備 Ansible 模組


在 ansible 目錄下,建立一個 roles 的資料夾,可以放入各種模組,在那之前我們需要先把一個模組拉回來,就是 codedeploy-agent,ec2 機器上一定要安裝這個模組, CodeDeploy 才會動,否則就無法佈署。

在這裡使用的是:


這個 role,安裝方式是直接把 git 目錄拉到 roles 底下,如果是用 ansible-galaxy 安裝,安裝後會拿到一個安裝位置,把那個安裝位置的資料夾直接 cp 或是 mv 過去。

我的 ansible roles 目錄就會像這樣:

roles
├── andrewrothstein.ipfs
│   ├── defaults
│   ├── handlers
│   ├── meta
│   ├── tasks
│   ├── tests
│   └── vars
├── andrewrothstein.unarchive-deps
│   ├── defaults
│   ├── meta
│   ├── tasks
│   └── vars
├── common
│   ├── defaults
│   └── tasks
├── diodonfrost.amazon_codedeploy
│   ├── defaults
│   ├── handlers
│   ├── meta
│   ├── molecule
│   │   ├── default
│   │   └── windows
│   ├── tasks
│   ├── tests
│   └── vars
├── fubarhouse.rust
...

主要目錄裡面有一個 ansible.cfg:

[defaults]
inventory = ./hosts
#vault_password_file = vault.key
ansible_managed = Ansible managed, any changes you make here will be overwritten

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=15m -F ssh.config -q
scp_if_ssh = True


還有一個 hosts, ssh.config ,這兩個都是剛才 outputs.sh 產生出來的。

接著要寫一個 site.yml 當作 ansible playbook:

---
# This playbook is intended to install all the necessary dependencies of the
# application and set the remote server up for development

- name: Create user accounts for deployment and execution
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags: 
    - users
    - install

  roles:
    - users

- name: Install CodeDeploy agent
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags: 
    - codedeploy
    - install

  roles:
    - diodonfrost.amazon_codedeploy


- name: Tune journald settings
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags: 
    - journal
    - install

  roles:
    - stuvusit.systemd-journald


- name: Install software
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags: 
    - deps # ansible-playbook -v site.yml --tags deps can make install to target machine
    - install

  roles:
    - common

- name: Prepare Golang Service
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags: 
    - deps # ansible-playbook -v site.yml --tags deps can make install to target machine
    - install

  roles: 
    - role: gantsign.golang
      golang_gopath: '$HOME/workspace-go'
      golang_version: '1.17'


完成後,就可以直接執行安裝腳本指令:
ansible-playbook -v site.yml --tags install

就可以完成安裝。


以上所使用的 roles 腳本,都可以在:

https://github.com/hpcslag/infrastructure_boilerplate/tree/main/ansible

這個專案中看到範例。


準備 Service 模板和 Nginx 設定等


上一節執行後,機器已經安裝好基本應用程式,但還需要安裝 Infrastructure 的部分,總共有幾個部分需要設定,Nginx 和自訂應用程式的 Service 檔案,以下 yml 是完整的設定:


- name: Setup Nginx
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags: 
    - install
    - Nginx
  roles:
    - role: geerlingguy.nginx
      nginx_service_state: started
      nginx_service_enabled: true
      nginx_vhosts:
        - listen: "80 default_server"
          filename: "my_project.vhost.conf"
          server_name: "YOUR_DOMAIN.com"
          state: present
          extra_parameters: |
            location / {
              proxy_pass http://localhost:8080;
            }

- name: Setup My Application
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags:
    - services
    - install
  roles:
    - role: services # 使用自訂服務,在 roles/services 下
      vars:
        app_type: MyApplication


# this requires aws configure to same region
- name: Localhost trigger code pipeline
  hosts: all
  tags:
    - install
    - pipeline
  tasks:
    - name: Trigger AWS Build
      local_action: raw aws codepipeline start-pipeline-execution --name my-app-build-pipeline


上面的腳本都還無法執行,在這裡缺少了第二段 Setup My Application 的自訂服務建立,這個服務會自動幫我們安裝好應用程式的 Systemctl 模板。

此段請參考這個專案的幾個目錄: 

https://github.com/hpcslag/infrastructure_boilerplate/tree/main/ansible

  • templates
    • my-application.j2
  • services
基本上 services/task/service-my-application.yml 這個檔案就定義要使用 templates 資料夾中的 my-application.j2 當作 systemctl 模板,它將會把這個檔案複製過去。

裡面 j2 有許多模板變數可以被替換,只要根據需求更改 service-my-application.yml 就可以了。

第三段 yml 執行是 aws codepipeline,這是為了觸發將你的應用程式開始進行編譯,預期編譯完可以把程式放到你的機器上,這些在 terraform 中早已經有定義。

- name: Setup Nginx
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags: 
    - install
    - Nginx
  roles:
    - role: geerlingguy.nginx
      nginx_service_state: started
      nginx_service_enabled: true
      nginx_vhosts:
        - listen: "80 default_server"
          filename: "my_project.vhost.conf"
          server_name: "YOUR_DOMAIN.com"
          state: present
          extra_parameters: |
            location / {
              proxy_pass http://localhost:8080;
            }

- name: Setup My Application
  hosts: all
  remote_user: "{{ user_name }}"
  become: true
  tags:
    - services
    - install
  roles:
    - role: services # 使用自訂服務,在 roles/services 下
      vars:
        app_type: MyApplication


# this requires aws configure to same region
- name: Localhost trigger code pipeline
  hosts: all
  tags:
    - install
    - pipeline
  tasks:
    - name: Trigger AWS Build
      local_action: raw aws codepipeline start-pipeline-execution --name my-app-build-pipeline



撰寫 CodeBuild 的 buildspec.yml


預設 CodeBuild 就會吃你 Repo 專案底下的 buildspec.yml 檔案進行處理,這裡的範例是:

version: 0.2

phases:
  install:
    commands:
      - go mod download

  build:
    commands:
      - go build

artifacts:
  files:
    - [YOUR APPLICATION BINARY FILE NAME]
    - appspec.yml
    - scripts/deploy-clean.sh
    - scripts/deploy-install.sh
    - .env
  name: "go-server-$(date +%Y-%m-%d)"
  discard-paths: yes

cache:
  paths:
    - /go/pkg/**/*


可以注意到上方 artifacts 是把某些檔案帶到下一個 Pipeline: CodeDeploy, scripts 目錄下的內容在這個資料夾中,需要把它放進專案 

https://github.com/hpcslag/infrastructure_boilerplate/tree/main/scripts

它會依照 systemctl 的名稱進行應用程式重啟跟安裝,而且會保留 5 個版本。 這個腳本基本上就代表著更新應用程式。

撰寫 CodeDeploy 腳本 appspec.yml


當前面 buildspec.yml 完成建置之後,就會把剛才那些 artifacts 檔案帶到 appspec.yml,這裡就可以直接執行剛才帶來的檔案,完成最後佈署 + 更新。

範例指令檔案如下:

version: 0.0
os: linux

runas: deploy

files:
  - source: /
    destination: /tmp/go-deploy

hooks:
  BeforeInstall:
    - location: deploy-clean.sh

  AfterInstall:
    - location: deploy-install.sh


如此,這樣就算完成整個單體應用程式佈署流程了。


偵錯 CodeDeploy, CodeBuild


以下是一些我碰過的 CodeDeploy 錯誤問題跟可能找出問題的方式:

  1.  CodeDeploy 連 step 1 都進不去就掛掉,而且錯誤訊息是空的
    1. 這表示你可能沒有在你的機器上安裝 CodeDeploy Agent
    2. 查看 journalctl -xefu codedeploy-agent 的 log
    3. 查看佈署錯誤紀錄: tail /var/log/aws/codedeploy-agent/codedeploy-agent.log
  2. 執行到下載階段出錯
    1. 你沿用之前的 build,而且過很久 artifacts 都消失了,需要重新 build
    2. 可能沒有 IAM 權限,試著在 buildspec, appspec 中打印 aws sts get-caller-identity
  3. 檢查權限的方式
    1. aws sts get-caller-identity
    2. curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<相關 Role Name> 這段可以檢查目前的 Role 有沒有你指定的權限

References:

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
https://github.com/hashicorp/packer/issues/7142

沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014