AWS 的 CodeDeploy 是如何做 「部署」的

AWS 今天发布了 CodeDeploy ,第一次有了 Service 的概念,我表示很惊喜,试想有了发布服务,开发自己可以搞定服务的发布,需要运维的需求进一步减小了,我感到 NOSA 的那一天就近了一步。

好吧,来看看 CodeDepoly 是如何做 部署 的,对我们自己做发布系统有什么启示,不过我最后发现自己做一套类似的系统也很简单。

 

先看几个关键字:

  1. Application

  2. Deployment Group

  3. Auto Scaling Group

  4. Security Group

  5. Service Role

Application ,就是应用的意思,一个 Application 可以有多个 Deployment Group ,我理解 Application 是服务的集合,而下面的每一个 Deployment Group 都代表着每一个「子服务」。

一个 Deployment Group 是一组被部署的 instance ,它可以包括 由 key 来指定的数个instance,也可以是 Auto Scaling Group;

Deployment Group 里的 instance 最好在一个 Security Group ;

Service Role 则是用来控制权限,CodeDepoly 需要访问EC2 的instance,所以定义一个Service Role 来界定。

 

部署的 instance 事实上要满足几个条件:

  1. instance 要绑定一个IMA role (就是上面说的Service Role ),而且IMA role 要有「正确」的权限。

  2. instance 要由 Tag,以供 Deployment Group 使用。

  3. 要在每台 instance 安装 CodeDeploy Agent 。

这几个条件具体可以参考官方文档

 

部署过程如下图:

D7726FB4-134D-44B3-89D7-90FDA2BC91D7

灰色的部分由 CodeDeploy Agent 自动完成,比如下载包,包的地址可以选择 「 S3 」或者 「Github」 ;
黄色(是黄色吗?)则由我们自己定义,分别是 「停止应用」、「安装前准备」、「安装之后动作」、「启动应用」、「确认服务」,这些规则写在一个 APPSpec 文件里,而且名字必须为 appspec.yml ,并且它要在 「包」 的 「根目录」。

appspec.yml 文件示例:


version: 0.0 os: linux files: - source: /index.html destination: /var/www/html/ hooks: BeforeInstall: - location: scripts/install_dependencies timeout: 300 runas: root - location: scripts/start_server timeout: 300 runas: root ApplicationStop: - location: scripts/stop_server timeout: 300 runas: root

 

 

另外 AWS 提供了 Deployment Configuration,默认有三种:

  1. One at a Time ,也就是 一个一个部署,一台失败就失败;

  2. Half at a Time,一次部署一半的 instance,只要超过一半成功就算成功;

  3. All at Once,一次部署所有 instance,只要有一台成功就算成功。

当然,AWS 也提供自定义,你可以自定义 发布过程至少有 多少个 instance 或者 至少 百分之多少 的 instance 成功,发布就算成功。

 

 

根据一个实例生成具有相同数据的实例

这个需求一开始是开发跟我提的,比如说他们部署好一台机器(也许很难部署)之后,不想重复部署其他机器,只想根据部署好的机器去生成一个或多个一模一样的机器。服务扩容的时候可以使用这个功能,比如每次线上服务发布之后对其中一台机器做镜像,等服务需要扩容的时候根据此镜像生成数个相同数据的实例;还有一种扩容方式是先生成数台”裸”实例,然后部署这数个实例的应用,部署之后和线上的环境一模一样。说实话,我更喜欢用第二种方式,因为它更轻,只需要把部署做好。

so,我更倾向如果一个实例非常难部署,才用这个功能(线上服务是不能非常难部署的)。

 

这里有几个坑,我们的每台实例主机名不一样;主机名要注册到DNS;我们的实例第二块盘是用UUID挂载的,而新生成的盘UUID可能不同,所以要修改/etc/fstab;同时主机名变了,Puppet证书会有问题;应用程序的日志也会在新实例中,最好置空。先列下坑,最后解决:

坑一:主机名设置问题
坑二:DNS设置问题
坑三:硬盘挂载问题
坑四:Puppet证书问题(我们的所有实例上都跑着Puppet)
坑五:应用程序日志问题(暂时不考虑这个问题)

 

实现这个功能的大概步骤是这样:

  1. 传入 region、instance_id、主机名key、数量(instanc_id是要”克隆”的实例的instanc id;主机名key 用来生成主机名,我这里主机名是向资产系统获取的;数量就是生成新实例的数量)

  2. 根据instance_id 生成ami,拿到ami_id

  3. 根据instance_id 拿到它的 subnet_id、key_name、instance_type、sg_id ,这四项会被用在新实例的创建上,已保证新实例和原实例保持一样。

  4. 根据ami_id 创建新实例, 并用 user_data 来解决上面提到的四个坑(并保证新实例的Puppet正常运行)

导入需要用到的模块:

import os
import time
import requests
from multiprocessing.dummy import Pool as ThreadPool

import boto.ec2
from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
from boto.ec2.networkinterface import NetworkInterfaceSpecification, NetworkInterfaceCollection

看看根据instance_id 生成ami的代码。

def create_ami(region, instance_id, name):
    conn = boto.ec2.connect_to_region(region,
        aws_access_key_id=aws_access_key_id,
        aws_secret_access_key=aws_secret_access_key)

    ami_id= conn.create_image(instance_id, name, no_reboot=True,
        block_device_mapping=None)
    # print ami_id
    ami_ids = [ami_id]
    # print ami_ids

    time.sleep(3)
    ami_object = conn.get_all_images(image_ids=ami_ids)[0]
    # print ami_object.__dict__

    time_init = 0
    time_total = 300
    time_interval = 3
    while time_init < time_total:
        ami_object.update()
        print ami_object.state
        if ami_object.state == 'available':
            return ami_id
        else:
            time.sleep(time_interval)
            time_init += time_interval
    return False

再看下拿到原instance的  subnet_id、key_name、instance_type、sg_id 的代码。

def instance_info(region, instance_id):
    conn = boto.ec2.connect_to_region(region,
        aws_access_key_id=aws_access_key_id,
        aws_secret_access_key=aws_secret_access_key)

    instance_ids = [instance_id]
    reservations = conn.get_all_instances(instance_ids=instance_ids)
    for res in reservations:
        for instance in res.instances:
            subnet_id = instance.subnet_id
            key_name = instance.key_name
            instance_type = instance.instance_type
            sg_id = instance.interfaces[0].groups[0].id

            _dict = {
                'subnet_id': subnet_id,
                'key_name': key_name,
                'instance_type': instance_type,
                'sg_id': sg_id
            }
            return _dict

创建一台新实例的函数(hostname根据usage向资产系统获取,user_data是从http获取的,然后修改里面的hostname、dns_vip、ns_servers等,用于初始化)。

def create_instance(create_list):
    region = create_list["region"]
    subnet_id = create_list["subnet_id"]
    ami_id = create_list["ami_id"]
    key_name = create_list["key_name"]
    instance_type = create_list["instance_type"]
    sg_id = create_list["sg_id"]
    user_data = create_list["user_data"]
    usage = create_list["usage"]

    ret = libs.hostnames.get(region, usage)
    hostname = ret["hostname"].split(".")[0]
    user_data = user_data.replace("hostname=","hostname=%s" % hostname)
    user_data = user_data.replace("dns_vip=",
        "dns_vip=%s" % dns_info["dns_vip"])
    user_data = user_data.replace("ns_servers=",
        "ns_servers='%s'" % " ".join(dns_info["ns_servers"]) )

    network_interface = NetworkInterfaceSpecification(subnet_id=subnet_id,groups=[sg_id])
    network_interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(network_interface)

    conn = boto.ec2.connect_to_region(region,
        aws_access_key_id=aws_access_key_id,
        aws_secret_access_key=aws_secret_access_key)
        reservation = conn.run_instances(ami_id,
        key_name=key_name,
        network_interfaces=network_interfaces,
        instance_type=instance_type,
        min_count=1,
        max_count=1,
        user_data=user_data
    )    

    instance = reservation.instances[0]

    time_init = 0
    time_total = 300
    time_interval = 5
    while time_init < time_total:
        status = instance.update()
        if status == 'running':
            instance.add_tag("Name",hostname)
            break
        else:
            time.sleep(time_interval)
            time_init += time_interval

    create_list["instance_id"] = str(instance).split(":")[-1]
    create_list["placement"] = instance.placement
    create_list["status"] = instance.update()
    create_list["hostname"] = hostname

    return create_list

现在看下总的入口了(用到了multiprocessing来并发创建实例)。

def create_instances(region, instance_id, num, usage):
    ret = requests.get(clone_install_script)
    user_data = ret.text

    _time = time.strftime("%Y%m%d%H%M%S", time.localtime())
    name = "{0}-{1}".format(instance_id, _time)
    ami_id = create_ami(region, instance_id, name)
    if not ami_id:
       return False

    _instance_info = instance_info(region, instance_id)
    create_list = {
        "region": region,
        "subnet_id": _instance_info["subnet_id"],
        "instance_type": _instance_info["instance_type"],
        "key_name": _instance_info["key_name"],
        "sg_id": _instance_info["sg_id"],
        "ami_id": ami_id,
        "user_data": user_data,
        "usage": usage
    }

    create_lists = list()
    for i in xrange(num):
        create_lists.append(create_list)

    pool = ThreadPool(100)

    create_results = pool.map(create_instances, create_lists)

    pool.close()
    pool.join()

    return create_results

最后再看下user_data的脚本。

#!/bin/bash

hostname=
hostname $hostname
sed -i "s/^HOSTNAME=.*/HOSTNAME=$hostname/g" /etc/sysconfig/network

sed -i "#/home/#d" /etc/fstab
/sbin/blkid |egrep -v "vda" |sort -u -k1 |awk '{print $2" /home/ ext4 nosuid,noatime 1 2"}' >>/etc/fstab

/bin/rm -rf /var/lib/puppet/

dns_vip=
sed -i "/nameserver/s/.*/nameserver ${dns_vip}/g" /etc/resolv.conf

ns_servers=
#################
##增加DNS解析代码##
#################

####################
##配置Puppet代码#####
####################

reboot

 

代码这东西,参考下啦,Goodbye …

 

aws volume 的查询、创建、挂载 和 卸载

做个集合,方面查阅,欢迎参考,哈哈哈。

 

查看  volume-id 

import boto.ec2

region = “ap-southeast-1”
aws_access_key_id = “”
aws_secret_access_key = “”

conn = boto.ec2.connect_to_region(region, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key)
reservations = conn.get_all_instances()
for res in reservations:
    for instance in res.instances:
        if ‘Name’ in instance.tags:
           volume_list = []
           for i in instance.block_device_mapping:
               volume_dict = {}
               volume_dict[“device”] = i
               volume_dict[“volume_id”] = instance.block_device_mapping.get(i).volume_id
               volume_list.append(volume_dict)

           print “%s (%s) [%s]:\n %s” % (instance.tags[‘Name’], instance.id, instance.state, volume_list)
        else:
           volume_list = []
           for i in instance.block_device_mapping:
               volume_dict = {}
               volume_dict[“device”] = i
               volume_dict[“volume_id”] = instance.block_device_mapping.get(i).volume_id
               volume_list.append(volume_dict)

           print “%s [%s]:\n %s” % (instance.id, instance.state, volume_list)
        print

 

创建volume

import boto.ec2

region = “ap-southeast-1”
aws_access_key_id = “”
aws_secret_access_key = “”

conn = boto.ec2.connect_to_region(region, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key)

size = “126”
zone = “ap-southeast-1b”

volume_info = conn.create_volume(size, zone)
volume_id = volume_info.id

time_init = 0
time_total = 20
time_interval = 1
while time_init < time_total:
    volume_info.update()
    status = volume_info.status
    if status == ‘available’:
        print “{0}, {1}”.format(volume_id, status)
        break
    else:
        time.sleep(time_interval)
        time_init += time_interval

 

把一个 volume-id 挂载到 一个实例上

import boto.ec2

region = “ap-southeast-1”
aws_access_key_id = “”
aws_secret_access_key = “”

volume_id = “”
instance_id = “”
device = “”

conn = boto.ec2.connect_to_region(region, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key)
reservations = conn.get_all_instances()
curr_vol = conn.get_all_volumes([volume_id])[0]
#print curr_vol
#print curr_vol.zone
if curr_vol.status == ‘available’:
    conn.attach_volume(volume_id, instance_id, device)

 

把一个 volume-id 从一个实例上卸掉

import boto.ec2

region = “”
aws_access_key_id = “”
aws_secret_access_key = “”

instance_id = “”
volume_id = “”
device = “”

conn = boto.ec2.connect_to_region(region, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key)
reservations = conn.get_all_instances()
curr_vol = conn.get_all_volumes([volume_id])[0]
#print curr_vol
#print curr_vol.zone
if curr_vol.status == ‘in-use’:
    conn.detach_volume(volume_id, instance_id, device)

使用aws boto 创建 自定义 的实例

之前的笔记,觉得有价值,还是拿出来分享下。

 

参考下面的python 代码:

     import requests
     import time
     import boto.ec2
     from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
     from boto.ec2.networkinterface import NetworkInterfaceSpecification, NetworkInterfaceCollection

    block_device_map = BlockDeviceMapping()
    block_dev_type = BlockDeviceType()
    block_dev_type.snapshot_id = snap_id
    block_dev_type.delete_on_termination = False
    block_dev_type.size = volume_capacity
    block_device_map[‘/dev/sdb‘] = block_dev_type

    hostname = “”
    user_data= user_data.replace(“hostname=”,”hostname=%s” % hostname)

    network_interface = NetworkInterfaceSpecification(subnet_id=subnet_id, groups=[sg_id])
    network_interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(network_interface)

    conn = boto.ec2.connect_to_region(region, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key)
    reservation = conn.run_instances(ami_id,
        key_name=key_name,
        network_interfaces=network_interfaces,
        instance_type=instance_type,
        block_device_map=block_device_map,
        # security_groups=[security_groups],
        min_count=1,
        max_count=1,
        user_data=user_data,
        )

    instance = reservation.instances[0]

    time_init = 0
    time_total = 300
    time_interval = 5       
    while time_init < time_total:
        status = instance.update()   
        if status == ‘running’:
            instance.add_tag(“Name“,hostname)
            break
        else:
            time.sleep(time_interval)
            time_init += time_interval

    install_list[“instance_id”] = str(instance).split(“:”)[-1]
    install_list[“placement”] = instance.placement
    install_list[“status”] = instance.update()
    install_list[“hostname”] = hostname

1. ami_id 是制作的镜像,系统的基础配置已经在镜像里做了。

2. snap_id 是10G的一个snapshot ,我们把它挂载/home,用作数据盘,注意,block_dev_type.size 指定的值如果小于10,依然会创建10G的盘。

3. user_data 是通过http获取的脚本内容,我们把脚本里面的hostname值替换掉,实现不同机器不同主机名的目的(hostname 通过API从资产系统获得)。

4. 这里我把 “Name” 这个 tag 改成 主机名。

5. user_data 的内容如下:

#!/bin/bash

hostname=
hostname $hostname
sed -i “s/^HOSTNAME=.*/HOSTNAME=$hostname/g” /etc/sysconfig/network

mount /dev/sdb /home/
/sbin/blkid |egrep -v “vda” |sort -u -k1 |awk ‘{print $2″ /home/ ext4    noexec,nosuid,noatime   1 2″}’ >>/etc/fstab

echo “[`date`] personal post config script exec finished.” >>/var/log/message

 

问题:

1. 装完机器发现 /dev/sdb 或者 /dev/xvdb 只有 快照的大小,而不是指定的 volume_capacity
可以在装机之后执行下 resize2fs ,类似:
echo “resize2fs /dev/xvdb ;sed -i ‘/resize2fs/d’ /etc/rc.d/rc.local ” >>/etc/rc.d/rc.local
    reboot