建立 AWS 到 IDC 的VPN

由于我们的很多运维工具都部署在自己的 IDC 里面,比如 Puppet、YUM 源、NTP 服务器、Nginx 日志分析工具 等,如果不打通 AWS 到 IDC 的内网,这些工具都要在 AWS 搭建一份,太麻烦,so do it .

一开始我想使用 AWS 的「VPN连接」服务(文档在 这里 )来做,但是要求 IDC 端是 cisco 路由器等硬件设备,我们 IDC 没有这些设备,所以只能找机器来做了。

 

1.机器是 Centos6,改一下脚本,在两台机器上执行。

#!/bin/bash
#L_PUBLIC=$(curl -s ip.cn | grep -Po ‘\d+\.\d+\.\d+\.\d+’)
L_PUBLIC=$L_PUBLIC
R_PUBLIC=$R_PUBLIC
L_PRIVATE=$L_PRIVATE
R_PRIVATE=$R_PRIVATE
TUNL=tunl-aws
TTL=255

echo "Loading kernel modules…"
modprobe -av ipip
echo "Setting up tunnel…"
ip tunnel del $TUNL &>/dev/null || true # delete old tunnel first
ip tunnel add $TUNL mode ipip local $L_PUBLIC remote $R_PUBLIC ttl $TTL
ip link set $TUNL up
ip addr add $L_PRIVATE peer $R_PRIVATE dev $TUNL
ip route add $R_PRIVATE dev $TUNL
echo "Testing tunnel: pinging from $L_PRIVATE to $R_PRIVATE …"
ping -c1 $R_PRIVATE
[[ $? = 0 ]] && echo "Tunnel is set up successfully."

L_PUBLIC 是 本地机器公网IP,R_PUBLIC 是对方机器公网IP,L_PRIVATE 是 本地私有IP,R_PRIVATE 是对方私有IP,L_PRIVATE 和 R_PRIVATE 在一个网段(两个 PRIVATE IP 用于两端通信,因为是两个IP,所以划一个 /30 的网段就行了)。

要注意两点:

1). AWS 端机器的 L_PUBLIC 要写内网IP,而不是弹性IP(这点应该和 AWS 的网络结构有关),否则执行脚本之后也无法 ping 对方的 PRIVATE IP.

2). 两台机器 net.ipv4.ip_forward 要设置为 1 .

 

2.配置好隧道之后,还要在 两台机器上添加路由,比如我在 IDC 机器上增加:

route add -net 172.30.0.0/16 gw 10.0.20.34

在 AWS 机器上增加:

route add -net 10.0.0.0/8 gw 10.0.20.33

172.30.0.0/16 是 AWS 的网段,10.0.0.0/8 是 IDC 的网段。

 

3.需要在两端所有的机器上增加路由表,在 IDC 可以通过 Puppet 增加,更好的方式是通过 ospf 发布路由,还可以向公司的网络工程师帮忙。

在 AWS 可以修改 「VPC 的路由表」,增加一条到 IDC 网段的路由,我这里是把 10.0.0.0/8 网段的路由指向 AWS 机器 内网的接口上,如图:

S488C0Q)5W$QUW0@E[E`SNY

另外要注意的是,增加路由表之后,在 ec2 机器上 用 route -n 是看不到增加的路由的,但是确实已经生效;当 ec2 机器重启之后,此时用 route -n 可以看到增加的路由。

 

理论上说,完成上面的步骤之后,VPN 就已经 OK 了,但是这里是 AWS,不是自己的网络架构,所以我又遇到了坑。。。。

上面说了要把两方的机器的 net.ipv4.ip_forward 打开,但是在 ec2 机器上打开之后,还需要关闭 ec2 的 「源/目标检查」,如图:

bbbb

源/目标检查 的解释如下:

Changing the Source/Destination Checking of a Network Interface The Source/Destination Check attribute controls whether source/destination checking is enabled on the instance. Disabling this attribute enables an instance to handle network traffic that isn’t specifically destined for the instance. For example, instances running services such as network address translation, routing, or a firewall should set this value to disabled. The default value is enabled.

最后放张图,看下成果。。。。

aaaa

 

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 成功,发布就算成功。

 

 

aws boto 停止 和 删除实例

停止实例

import boto.ec2

region = ""
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)

instance_list = []

conn.stop_instances(instance_ids=instance_list)

删除实例

import boto.ec2

region = ""
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)

instance_list = []

for instance in instance_list :
 conn.modify_instance_attribute(instance, "disableApiTermination", False)

conn.terminate_instances(instance_ids=instance_list)

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

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

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 …