AWS CodeDeploy
AWS CodeDeploy是AWS提供的服务之一,用于发布伺服器端的软体。
简介
传统在CI/CD上面(像是Travis CI或是GitHub Actions)透过SSH或是类似的方式,将新版的程式码推到伺服器上的作法会是综合性的:
- 开机时取得最新版的程式码。
- 在CI/CD服务上先抓取目前的机器列表,然后连进去更新。
但这个作法会产生几个问题,会需要更复杂的方式解决:
- 有两种不同的触发路径,一种是开机时去抓,另外一种是外部连进来机器里面抓,要注意开机时同时发生的冲突问题。
- 这点不算难解,
lockf
通常都可以解决。
- 这点不算难解,
- 需要对CI/CD服务的机器网段开启SSH,代表其他人有机会透过同样的CI/CD服务接触到你伺服器的SSH。
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)