AWS CodeDeploy

来自Gea-Suan Lin's Wiki
跳到导航 跳到搜索

AWS CodeDeployAWS提供的服務之一,用於發佈伺服器端的軟體。

簡介

傳統在CI/CD上面(像是Travis CI或是GitHub Actions)透過SSH或是類似的方式,將新版的程式碼推到伺服器上的作法會是綜合性的:

  • 開機時取得最新版的程式碼。
  • 在CI/CD服務上先抓取目前的機器列表,然後連進去更新。

但這個作法會產生幾個問題,會需要更複雜的方式解決:

  • 有兩種不同的觸發路徑,一種是開機時去抓,另外一種是外部連進來機器裡面抓,要注意開機時同時發生的衝突問題。
    • 這點不算難解,flock可以拿來用。
  • 需要對CI/CD服務的機器網段開啟SSH,代表其他人有機會透過同樣的CI/CD服務接觸到你伺服器的SSH。
    • 這個部分可以在執行CI/CD時抓取本身的IP address,動態設定Firewall(像是AWS的Security Group)以降低風險(但不是完全阻隔),等到任務結束後再從Firewall上移除。但用這個方式又會衍生出其他問題:
      • 大多數的CI/CD服務是透過NAT的方式連外,但不能保證每次連外的IP是一樣的(雖然目前的應該都會一樣,但這沒有保證,屬於side effect)。
      • 另外必須考慮到多隻程式操作Firewall設定時可能會有衝突的問題,這點如果Firewall沒有支援CAS類的atomic操作,會透過外部的distributed lock避免。

AWS CodeDeploy是一套伺服器軟體佈署的服務,跟傳統CI/CD佈署不同的點在於:

  • 都是由伺服器端主動取得檔案並且佈署。
  • 不需要額外開SSH讓CI/CD服務連入操作。

AWS端設定

除了CodeDeploy設定外,還會需要建立S3 bucket:

aws --profile default --region us-east-1 deploy create-application --application-name my-test
aws --profile default --region us-east-1 s3 mb "s3://gslin-codedeploy-us-east-1-my-test/"

伺服器端安裝

在伺服器端需要安裝Agent,這隻Agent會負責取得檔案並且執行設定的步驟[1]

Ubuntu的環境為例子來說[2],會需要先安裝Ruby 2.0(在14.04下)或Ruby 2.3(在16.04下)後,取得安裝設定檔執行:

sudo apt -y install ruby
cd /tmp
wget https://aws-codedeploy-us-east-1.s3.amazonaws.com/latest/install
chmod 755 install
sudo ./install auto

在非EC2的機器上可能會跑比較久(需要等169.254.169.254的timeout,在EC2的環境裡,這個IP會有HTTP服務提供資訊給Instance使用)。

然後看Agent是否有啟動:

sudo service codedeploy-agent status

如果沒有的話可以用start啟動:

sudo service codedeploy-agent start

外部機器的額外步驟

當機器不在EC2上時,有幾種方法可以註冊到CodeDeploy的系統上,會被稱為On-Premises Instance。這邊我們介紹的方法是一台機器給一個IAM user的方式。

首先先在一般的機器上產生出對應的權限與設定檔(不需要在需要註冊的機器上),因為要建立IAM權限,通常會是由管理員建立(有AdministratorAccess的人):

aws deploy register --instance-name api-example-1 --region us-east-1

然後把生出的.yml檔案傳到要註冊的機器上:

scp codedeploy.onpremises.yml api-example-1:/tmp/

然後在要註冊的機器上執行aws deploy install

cd /tmp
sudo aws deploy install --config-file codedeploy.onpremises.yml --region us-east-1

上面的指令目前會因為他想裝ruby2.0但系統沒有而顯示錯誤訊息,但我們已經裝好CodeDeploy的檔案了,這個指令的目的只是要他設定檔塞進系統。

如果是在某些有提供http://169.254.169.254/服務的VPS上執行(像是Vultr),會有Amazon EC2 instances are not supported.這類的錯誤訊息,這時候就需要用iptables暫時性擋掉對169.254.169.254的Port 80連線了。理論上用這個指令在重開後就會失效,對其他應用程式比較不會有副作用:

sudo iptables -I OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j DROP

都跑起來後(建議直接重開機測試)要記得加上Tag讓後續設定可以抓到機器:

aws deploy add-tags-to-on-premises-instances --instance-name api-example-1 --tags Key=Name,Value=api-example-1

發佈

通常會有兩個指令:

  • 將現在的目錄打包起來傳到S3上。可能會使用--ignore-hidden-files避免.git或是.svn被包進去,但這個方式會使得.htaccess不會被包進去,對於使用Apache的使用者來說要注意。
  • 要求CodeDeploy送指令到各機器上抓檔案。
export NOW=$(date -u +%Y%m%d-%H%M%S)
export S3_KEY=${APPLICATION_NAME}/${GIT_BRANCH}-${NOW}-${GIT_HASH}
aws deploy push \
  --application-name "${APPLICATION_NAME}" \
  --profile "${AWS_PROFILE}" \
  --region "${AWS_REGION}" \
  --s3-location "s3://${S3_BUCKET}/${S3_KEY}"
aws deploy create-deployment \
  --application-name "${APPLICATION_NAME}" \
  --deployment-group-name "${GIT_BRANCH}" \
  --profile "${AWS_PROFILE}" \
  --region "${AWS_REGION}" \
  --s3-location bucket="${S3_BUCKET},key=${S3_KEY},bundleType=zip"

這邊可以看到故意放一些資訊到檔案名稱上,讓後續維護起來(找問題時)比較輕鬆。

伺服器端

在伺服器端要進行的行為是被定義在appspec.xml內。最簡單的設定就是指定要將這包檔案解到哪邊:

version: 0.0
os: linux
files:
  - source: /
    destination: /srv/www.example.com

其他

官方有提供把AWS CodeDeploy的Alarm轉到Slack上的Blueprint,而這個AWS Lambda程式可以把AWS CodeDeploy的Trigger(而非Alarm)轉到Slack上。有兩個環境變數要設定:

  • kmsEncryptedHookUrl
  • slackChannel
import boto3
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError


# The base-64 encoded, encrypted key (CiphertextBlob) stored in the kmsEncryptedHookUrl environment variable
ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
# The Slack channel to send a message to stored in the slackChannel environment variable
SLACK_CHANNEL = os.environ['slackChannel']

HOOK_URL = boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext'].decode('utf-8')

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    region = message['region']
    account_id = message['accountId']
    event_trigger_name = message['eventTriggerName']
    application_name = message['applicationName']
    deployment_id = message['deploymentId']
    deployment_group_name = message['deploymentGroupName']
    create_time = message['createTime']
    complete_time = message['completeTime']
    status = message['status']

    if status == 'FAILED':
        slackColor = 'danger'
    else:
        slackColor = 'good'

    slack_message = {
        'channel': SLACK_CHANNEL,
        'attachments': [
            {
                'author': application_name,
                'fallback': ', '.join([status, deployment_group_name, deployment_id, create_time]) + '.',
                'color': slackColor,
                'title': 'Execute AWS CodeDeploy',
                'fields': [
                    {
                        'title': 'status',
                        'value': status
                    },
                    {
                        'title': 'deploymentGroupName',
                        'value': deployment_group_name
                    },
                    {
                        'title': 'deploymentId',
                        'value': deployment_id
                    },
                    {
                        'title': 'createTime',
                        'value': create_time
                    }
                ]
            }
        ]
    }

    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

參考資料

相關連結

外部連結