AWS CodeDeploy

来自Gea-Suan Lin's Wiki
Gslin讨论 | 贡献2018年9月16日 (日) 05:33的版本
跳到导航 跳到搜索
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

AWS CodeDeployAWS提供的服务之一,用于发布伺服器端的软体。

简介

AWS CodeDeploy是一套伺服器软体布署的服务,跟其他软体布署不同的点在于:

由伺服器端主动取得档案并且布署,非SSH由外部连入更新。
不用取得“线上有哪些机器”。
这点在云端时代特别重要。这避免了在开新机器时(如Auto Scaling)有机器可能会没布署到的问题(即race condition)。

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 install ruby2.3
$ cd /tmp
$ wget https://aws-codedeploy-us-east-1.s3.amazonaws.com/latest/install
$ chmod 755 install
$ sudo ./install auto

在Ubuntu 18.04下因为安装程式的bug[3]而需要复杂的workaround(先做一个假的Ruby 2.3套件绕过侦测,再修改程式码让他使用Ruby 2.5):

$ cd /tmp
$ sudo apt -y install equivs ruby
$ equivs-control ruby2.3.control
$ perl -pi -e 's{^Package.*}{Package: ruby2.3}' ruby2.3.control
$ equivs-build ruby2.3.control
$ sudo dpkg -i ruby2.3_1.0_all.deb
$ wget https://aws-codedeploy-us-east-1.s3.amazonaws.com/latest/install
$ perl -pi -e "s{\\['2.4', '2.3', '2.2', '2.1', '2.0'\\]}{['2.5', '2.4', '2.3', '2.2', '2.1', '2.0']}" install
$ chmod 755 install
$ sudo ./install auto

在非[[EC2]]的機器上可能會跑比較久(需要等<code>169.254.169.254</code>的timeout,在EC2上時這個IP會有HTTP服務提供資訊給Instance)。

然後看Agent是否有啟動:

<syntaxhighlight lang="shell-session">
$ sudo service codedeploy-agent status

如果没有的话可以用start启动:

$ sudo service codedeploy-agent start

发起端

通常会有两个指令:

  • 将现在的目录打包起来传到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

外部机器

当机器不在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但系统没有而烂掉,我们只是要他把设定档塞进系统。(因为只有Ubuntu 14.04有ruby2.0,在16.04因为没这个套件而需要用ruby2.3替代,但aws deploy install还是写死ruby2.0,而这个问题被提出来很久了,看起来官方懒得修...)

请照更上面提到的方式改装us-east-1install档案即可(这边的指令与上面是一样的,为了方便这边复制一份过来):

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

如果是在某些有提供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 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)

参考资料

外部连结