docker环境搭建zk集群

对于个人开发者而言,学习分布式的好多东东,都比较费劲,因为手头的机器不够。若是单机使用虚拟机来启动一个个虚拟server话,又比较耗费资源,要求单机性能够好。幸好docker这种轻量级的东东出现了。本文主要是记录使用docker搭建zk集群的过程。

下载zk镜像

这次使用garland搭好的镜像。

docker pull garland/zookeeper

构建zk集群

docker run -d 
 --name=zk1 
 --net=host 
 -e SERVER_ID=1 
 -e ADDITIONAL_ZOOKEEPER_1=server.1=localhost:2888:3888 
 -e ADDITIONAL_ZOOKEEPER_2=server.2=localhost:2889:3889 
 -e ADDITIONAL_ZOOKEEPER_3=server.3=localhost:2890:3890 
 -e ADDITIONAL_ZOOKEEPER_4=clientPort=2181 
 garland/zookeeper

docker run -d 
 --name=zk2 
 --net=host 
 -e SERVER_ID=2 
 -e ADDITIONAL_ZOOKEEPER_1=server.1=localhost:2888:3888 
 -e ADDITIONAL_ZOOKEEPER_2=server.2=localhost:2889:3889 
 -e ADDITIONAL_ZOOKEEPER_3=server.3=localhost:2890:3890 
 -e ADDITIONAL_ZOOKEEPER_4=clientPort=2182 
 garland/zookeeper

docker run -d 
 --name=zk3 
 --net=host 
 -e SERVER_ID=3 
 -e ADDITIONAL_ZOOKEEPER_1=server.1=localhost:2888:3888 
 -e ADDITIONAL_ZOOKEEPER_2=server.2=localhost:2889:3889 
 -e ADDITIONAL_ZOOKEEPER_3=server.3=localhost:2890:3890 
 -e ADDITIONAL_ZOOKEEPER_4=clientPort=2183 
 garland/zookeeper

查看集群

查看zk1(follower)

root@xixicat:~# echo stat | nc 127.0.0.1 2181
Zookeeper version: 3.4.6-1569965, built on 02/20/2014 09:09 GMT
Clients:
 /127.0.0.1:55531[0](queued=0,recved=1,sent=0)

Latency min/avg/max: 0/1/31
Received: 63
Sent: 62
Connections: 1
Outstanding: 0
Zxid: 0x100000005
Mode: follower
Node count: 5

查看zk2(leader)

root@xixicat:~# echo stat | nc 127.0.0.1 2182
Zookeeper version: 3.4.6-1569965, built on 02/20/2014 09:09 GMT
Clients:
 /127.0.0.1:53244[0](queued=0,recved=1,sent=0)

Latency min/avg/max: 0/0/0
Received: 1
Sent: 0
Connections: 1
Outstanding: 0
Zxid: 0x100000005
Mode: leader
Node count: 5

查看zk3(follower)

root@xixicat:~# echo stat | nc 127.0.0.1 2183
Zookeeper version: 3.4.6-1569965, built on 02/20/2014 09:09 GMT
Clients:
 /127.0.0.1:33983[0](queued=0,recved=1,sent=0)

Latency min/avg/max: 0/0/0
Received: 1
Sent: 0
Connections: 1
Outstanding: 0
Zxid: 0x100000005
Mode: follower
Node count: 5

daocloud加速

之前折腾了好几次,国内访问docker.io的网速还是不行,于是就使用了daocloud加速,前提是你得装docker-machine。试用了下,速度提升了不少。

未分类

Docker创建的集群下使用ansible部署zookeeper

使用文章“Docker创建的集群下使用ansible部署hadoop”中创建的集群进行zookeeper的安装

未分类

在cluster-master上制作zookeeper安装包

下载

官方源下载显得十分缓慢,所以还是选择国内的镜像源,将zookeeper下载到/opt

[root@cluster-master opt]# wget https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/stable/zookeeper-3.4.10.tar.gz

创建链接

下载完成后将zookeeper-3.4.10.tar.gz解压并创建链接,方便管理

[root@cluster-master opt]# tar -zxvf zookeeper-3.4.10.tar.gz
[root@cluster-master opt]# ln -s zookeeper-3.4.10 zookeeper

修改配置文件

/opt/zookeeper/conf中已经提供了zoo_sample.cfg配置模板,复制一份zoo.cfg进行修改即可使用,我的配置项如下:

[root@cluster-master conf]# cp zoo_sample.cfg zoo.cfg
[root@cluster-master conf]# vi zoo.cfg

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/home/zookeeper/data
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

server.2=172.18.0.2:2888:3888
server.3=172.18.0.3:2888:3888
server.4=172.18.0.4:2888:3888
server.5=172.18.0.5:2888:3888

dataDir做了从新定义,server项使用IP的最后一位,也是为了方便管理

创建shell脚本,完成安装步骤

在/opt/zookeeper下新建postinstall.sh创建dataDir目录和myid文件,并写入zoo.cfg中定义的myid值

vi /opt/zookeeper/postinstall.sh

#!/bin/bash

# zookeeper conf file
conf_file="/opt/zookeeper/conf/zoo.cfg"
# get myid
IP=$(/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6 |
     awk '{print $2}')
ID=$(grep ${IP} ${conf_file}|cut -d = -f 1|cut -d . -f 2)

# get dataDir
dataDir=$(grep dataDir ${conf_file}|grep -v '^#'|cut -d = -f 2)

# create dataDir and myid file
mkdir -p ${dataDir}
:>${dataDir}/myid
echo ${ID} > ${dataDir}/myid

打包配置完成后的zookeeper,准备上传至slave主机

将链接zookeeper和目录zookeeper-3.4.10打包并压缩

[root@cluster-master opt]# tar -zcvf zookeeper-dis.tar.gz zookeeper zookeeper-3.4.10

创建yaml,安装zookeeper

[root@cluster-master opt]# vi install-zookeeper.yaml
---
- hosts: slaves
  tasks:
    - name: install ifconfig
      yum: name=net-tools state=latest
    - name: unarchive zookeeper
      unarchive: src=/opt/zookeeper-dis.tar.gz dest=/opt
    - name: postinstall
      shell: bash /opt/zookeeper/postinstall.sh

分发安装文件到slave主机

[root@cluster-master opt]# ansible-playbook install-zookeeper.yaml

启动zookeeper

此时,zookeeper集群已经可以正常启动

[root@cluster-master opt]# ansible cluster -m command -a "/opt/zookeeper/bin/zkServer.sh start"

查看状态

[root@cluster-master bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Mode: follower

运行客户端

[root@cluster-master bin]# ./zkCli.sh -server localhost:2181
Connecting to localhost:2181
2017-08-29 18:05:36,078 [myid:] - INFO  [main:Environment@100] - Client environment:zookeeper.version=3.4.10-39d3a4f269333c922ed3db283be479f9deacaa0f, built on 03/23/2017 10:13 GMT
2017-08-29 18:05:36,091 [myid:] - INFO  [main:Environment@100] - Client environment:host.name=cluster-master
2017-08-29 18:05:36,091 [myid:] - INFO  [main:Environment@100] - Client environment:java.version=1.8.0_141
2017-08-29 18:05:36,098 [myid:] - INFO  [main:Environment@100] - Client environment:java.vendor=Oracle Corporation
2017-08-29 18:05:36,098 [myid:] - INFO  [main:Environment@100] - Client environment:java.home=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.el7_3.x86_64/jre
2017-08-29 18:05:36,098 [myid:] - INFO  [main:Environment@100] - Client environment:java.class.path=/opt/zookeeper/bin/../build/classes:/opt/zookeeper/bin/../build/lib/*.jar:/opt/zookeeper/bin/../lib/slf4j-log4j12-1.6.1.jar:/opt/zookeeper/bin/../lib/slf4j-api-1.6.1.jar:/opt/zookeeper/bin/../lib/netty-3.10.5.Final.jar:/opt/zookeeper/bin/../lib/log4j-1.2.16.jar:/opt/zookeeper/bin/../lib/jline-0.9.94.jar:/opt/zookeeper/bin/../zookeeper-3.4.10.jar:/opt/zookeeper/bin/../src/java/lib/*.jar:/opt/zookeeper/bin/../conf:
2017-08-29 18:05:36,099 [myid:] - INFO  [main:Environment@100] - Client environment:java.library.path=/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
2017-08-29 18:05:36,100 [myid:] - INFO  [main:Environment@100] - Client environment:java.io.tmpdir=/tmp
2017-08-29 18:05:36,100 [myid:] - INFO  [main:Environment@100] - Client environment:java.compiler=<NA>
2017-08-29 18:05:36,100 [myid:] - INFO  [main:Environment@100] - Client environment:os.name=Linux
2017-08-29 18:05:36,100 [myid:] - INFO  [main:Environment@100] - Client environment:os.arch=amd64
2017-08-29 18:05:36,100 [myid:] - INFO  [main:Environment@100] - Client environment:os.version=3.10.0-514.26.2.el7.x86_64
2017-08-29 18:05:36,101 [myid:] - INFO  [main:Environment@100] - Client environment:user.name=root
2017-08-29 18:05:36,101 [myid:] - INFO  [main:Environment@100] - Client environment:user.home=/root
2017-08-29 18:05:36,101 [myid:] - INFO  [main:Environment@100] - Client environment:user.dir=/opt/zookeeper-3.4.10/bin
2017-08-29 18:05:36,124 [myid:] - INFO  [main:ZooKeeper@438] - Initiating client connection, connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@25f38edc
2017-08-29 18:05:36,205 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1032] - Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
Welcome to ZooKeeper!
JLine support is enabled
2017-08-29 18:05:36,730 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@876] - Socket connection established to localhost/127.0.0.1:2181, initiating session
2017-08-29 18:05:36,795 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x25e2f22aa660001, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 1] 

总结

使用到了之前创建的集群和ansible,发现使用ansible部署应用确实很方便。

ansible-playbook之循环(Loops)

1. 标准的Loops:也是最常用的一种循环

例:当需要安装10个软件包时,不用写10次任务,只需要写一次然后进行循环即可

#注:已经安装的软件系统即不在安装
[root@nfs-server playbook]# cat yum_list.yml 
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks: 
  - name: "需要安装的软件清单"
    yum: name={{ item }} state=present
    with_items:
      - lrzsz
      - vim
      - sysstat
[root@nfs-server playbook]# ansible-playbook yum_list.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [需要安装的软件清单] ***************************************************************************************************************************************
changed: [192.168.2.101] => (item=[u'lrzsz', u'vim', u'sysstat'])
changed: [192.168.2.111] => (item=[u'lrzsz', u'vim', u'sysstat'])

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=1    changed=1    unreachable=0    failed=0   
192.168.2.111              : ok=1    changed=1    unreachable=0    failed=0

2. 字典格式的循环:with_items

#写法一:
[root@nfs-server playbook]# cat dict_list.yml
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks:
  - name: "字典格式的循环"
    debug: msg="name ---->{{ item.name }},age---->{{ item.age }}"
    with_items:
      - {name: "Liu Zhengwei",age: 28}
      - {name: "Jia Dongli",age: 25}
#写法二:
[root@nfs-server playbook]# cat dict_list.yml
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks:
  - name: "字典格式的循环"
    debug: msg="name ---->{{ item.name }},age---->{{ item.age }}"
    with_items:
      - name: "Liu Zhengwei"
        age: 28
      - name: "Jia Dongli"
        age: 25
#注:以上两种写法效果是一样的
[root@nfs-server playbook]# ansible-playbook dict_list.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [字典格式的循环] *****************************************************************************************************************************************
ok: [192.168.2.101] => (item={u'age': 28, u'name': u'Liu Zhengwei'}) => {
    "item": {
        "age": 28, 
        "name": "Liu Zhengwei"
    }, 
    "msg": "name ---->Liu Zhengwei,age---->28"
}
ok: [192.168.2.101] => (item={u'age': 25, u'name': u'Jia Dongli'}) => {
    "item": {
        "age": 25, 
        "name": "Jia Dongli"
    }, 
    "msg": "name ---->Jia Dongli,age---->25"
}
ok: [192.168.2.111] => (item={u'age': 28, u'name': u'Liu Zhengwei'}) => {
    "item": {
        "age": 28, 
        "name": "Liu Zhengwei"
    }, 
    "msg": "name ---->Liu Zhengwei,age---->28"
}
ok: [192.168.2.111] => (item={u'age': 25, u'name': u'Jia Dongli'}) => {
    "item": {
        "age": 25, 
        "name": "Jia Dongli"
    }, 
    "msg": "name ---->Jia Dongli,age---->25"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=1    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=1    changed=0    unreachable=0    failed=0

3. 嵌套Loops(列表格式的循环,用于1对多或者多对多关系时)–> with_nes

[root@nfs-server playbook]# cat netsted_list.yml 
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks:
  - name: "实现1对多关系的循环"
    debug: msg="name--->{{ item[0] }},value--->{{  item[1] }}"
    with_nested:
      - ['A']
      - ['a','b','c' ]
[root@nfs-server playbook]# ansible-playbook netsted_list.yml -l 192.168.2.101

PLAY [webservers] **************************************************************************************************************************************

TASK [实现1对多关系的循环] **************************************************************************************************************************************
ok: [192.168.2.101] => (item=[u'A', u'a']) => {
    "item": [
        "A", 
        "a"
    ], 
    "msg": "name--->A,value--->a"
}
ok: [192.168.2.101] => (item=[u'A', u'b']) => {
    "item": [
        "A", 
        "b"
    ], 
    "msg": "name--->A,value--->b"
}
ok: [192.168.2.101] => (item=[u'A', u'c']) => {
    "item": [
        "A", 
        "c"
    ], 
    "msg": "name--->A,value--->c"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=1    changed=0    unreachable=0    failed=0

4. 散列loops:with_dict(支持更丰富的数据结构)

注:with_dict的写法在新版本中进行了改变,必须要写成字典的形式,跟我下面写的格式一样

[root@nfs-server playbook]# cat with_dict.yml 
---
- hosts: webservers
  gather_facts: False
  remote_user: root
  vars:
    user:
      shencan:
        name: shencan
        shell: bash
      ruifengyun:
        name: ruifengyun
        shell: zsh
  tasks:
  - name: debug loops
    debug: "msg=name--->{{ item.key }} value--->{{ item.value.name }} shell--->{{ item.value.shell }}"
    with_dict: "{{ user }}"

[root@nfs-server playbook]# ansible-playbook with_dict.yml -l 192.168.2.101

PLAY [webservers] **************************************************************************************************************************************

TASK [debug loops] *************************************************************************************************************************************
ok: [192.168.2.101] => (item={'key': u'ruifengyun', 'value': {u'shell': u'zsh', u'name': u'ruifengyun'}}) => {
    "item": {
        "key": "ruifengyun", 
        "value": {
            "name": "ruifengyun", 
            "shell": "zsh"
        }
    }, 
    "msg": "name--->ruifengyun"
}
ok: [192.168.2.101] => (item={'key': u'shencan', 'value': {u'shell': u'bash', u'name': u'shencan'}}) => {
    "item": {
        "key": "shencan", 
        "value": {
            "name": "shencan", 
            "shell": "bash"
        }
    }, 
    "msg": "name--->shencan"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=1    changed=0    unreachable=0    failed=0

5. 文件匹配loops:with_fileglob

在工作中,我们经常遇到需要针对一个目录下指定格式的文件进行处理,这个时候直接引用with_fileglob循环去匹配我们需要处理的文件即可

[root@nfs-server playbook]# cat with_fileglob.yml
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks:
  - name: debug loops
    debug: "msg=files-->{{ item }}"
    with_fileglob:
      - /var/log/nginx/*.gz
[root@nfs-server playbook]# ansible-playbook with_fileglob.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [debug loops] *************************************************************************************************************************************
ok: [192.168.2.101] => (item=/var/log/nginx/host.access.log-20170816.gz) => {
    "item": "/var/log/nginx/host.access.log-20170816.gz", 
    "msg": "files-->/var/log/nginx/host.access.log-20170816.gz"
}
ok: [192.168.2.101] => (item=/var/log/nginx/error.log-20170815.gz) => {
    "item": "/var/log/nginx/error.log-20170815.gz", 
    "msg": "files-->/var/log/nginx/error.log-20170815.gz"
}
ok: [192.168.2.101] => (item=/var/log/nginx/error.log-20170823.gz) => {
    "item": "/var/log/nginx/error.log-20170823.gz", 
    "msg": "files-->/var/log/nginx/error.log-20170823.gz"
}

ok: [192.168.2.111] => (item=/var/log/nginx/access.log-20170817.gz) => {
    "item": "/var/log/nginx/access.log-20170817.gz", 
    "msg": "files-->/var/log/nginx/access.log-20170817.gz"
}
ok: [192.168.2.111] => (item=/var/log/nginx/error.log-20170825.gz) => {
    "item": "/var/log/nginx/error.log-20170825.gz", 
    "msg": "files-->/var/log/nginx/error.log-20170825.gz"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=1    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=1    changed=0    unreachable=0    failed=0

6. 随机选择loops:with_random_choice(会从给定的值中随便选取一个显示)

[root@nfs-server playbook]# cat with_random_choice.yml
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks:
  - name: debug loops
    debug: 'msg="name --> {{ item }}"'
    with_random_choice:
      - "Beijing"
      - "Shanghai"
      - "TianJin"
[root@nfs-server playbook]# ansible-playbook with_random_choice.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [debug loops] *************************************************************************************************************************************
ok: [192.168.2.101] => (item=Beijing) => {
    "item": "Beijing", 
    "msg": "name --> Beijing"
}
ok: [192.168.2.111] => (item=Shanghai) => {
    "item": "Shanghai", 
    "msg": "name --> Shanghai"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=1    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=1    changed=0    unreachable=0    failed=0

7. 条件判断loops

在某些情况下,我们需要检测某个task是否达到了预想的状态,如果没有达到,就需要退出整个剧本。

until:检测条件

retries:检测次数

delay:每次检测的间隔时长

[root@nfs-server playbook]# cat if_else.yml
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks:
  - name: "对task的执行结果进行判断"
    shell: cat /etc/fstab
    register: info
    until: info.stdout.startswith("sysfs")
    retries: 5
    delay: 5
[root@nfs-server playbook]# ansible-playbook if_else.yml 

PLAY [webservers] **************************************************************************************************************************************

TASK [对task的执行结果进行判断] **********************************************************************************************************************************
FAILED - RETRYING: 对task的执行结果进行判断 (5 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (5 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (4 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (4 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (3 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (3 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (2 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (2 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (1 retries left).
FAILED - RETRYING: 对task的执行结果进行判断 (1 retries left).
fatal: [192.168.2.111]: FAILED! => {"attempts": 5, "changed": true, "cmd": "cat /etc/fstab", "delta": "0:00:00.003310", "end": "2017-09-04 01:09:18.651001", "failed": true, "rc": 0, "start": "2017-09-04 01:09:18.647691", "stderr": "", "stderr_lines": [], "stdout": "n#n# /etc/fstabn# Created by anaconda on Sun Jun 26 03:08:48 2016n#n# Accessible filesystems, by reference, are maintained under '/dev/disk'n# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more infon#n/dev/mapper/vg_test2-lv_root /                       ext4    defaults        1 1nUUID=8699d6c2-883b-41a0-8282-5be27641ee02 /boot                   ext4    defaults        1 2n/dev/mapper/vg_test2-lv_swap swap                    swap    defaults        0 0ntmpfs                   /dev/shm                tmpfs   defaults        0 0ndevpts                  /dev/pts                devpts  gid=5,mode=620  0 0nsysfs                   /sys                    sysfs   defaults        0 0nproc                    /proc                   proc    defaults        0 0n/dev/cdrom              /media/cdrom            iso9660 defaults        0 0", "stdout_lines": ["", "#", "# /etc/fstab", "# Created by anaconda on Sun Jun 26 03:08:48 2016", "#", "# Accessible filesystems, by reference, are maintained under '/dev/disk'", "# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info", "#", "/dev/mapper/vg_test2-lv_root /                       ext4    defaults        1 1", "UUID=8699d6c2-883b-41a0-8282-5be27641ee02 /boot                   ext4    defaults        1 2", "/dev/mapper/vg_test2-lv_swap swap                    swap    defaults        0 0", "tmpfs                   /dev/shm                tmpfs   defaults        0 0", "devpts                  /dev/pts                devpts  gid=5,mode=620  0 0", "sysfs                   /sys                    sysfs   defaults        0 0", "proc                    /proc                   proc    defaults        0 0", "/dev/cdrom              /media/cdrom            iso9660 defaults        0 0"]}
fatal: [192.168.2.101]: FAILED! => {"attempts": 5, "changed": true, "cmd": "cat /etc/fstab", "delta": "0:00:00.002489", "end": "2017-09-04 01:11:02.560507", "failed": true, "rc": 0, "start": "2017-09-04 01:11:02.558018", "stderr": "", "stderr_lines": [], "stdout": "n#n# /etc/fstabn# Created by anaconda on Sun Jun 26 03:11:47 2016n#n# Accessible filesystems, by reference, are maintained under '/dev/disk'n# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more infon#n/dev/mapper/vg_test3-lv_root /                       ext4    defaults        1 1nUUID=e48217af-0ad9-45be-aa68-b0b1bbc88c97 /boot                   ext4    defaults        1 2n/dev/mapper/vg_test3-lv_swap swap                    swap    defaults        0 0ntmpfs                   /dev/shm                tmpfs   defaults        0 0ndevpts                  /dev/pts                devpts  gid=5,mode=620  0 0nsysfs                   /sys                    sysfs   defaults        0 0nproc                    /proc                   proc    defaults        0 0n/dev/cdrom              /media/cdrom            iso9660 defaults        0 0", "stdout_lines": ["", "#", "# /etc/fstab", "# Created by anaconda on Sun Jun 26 03:11:47 2016", "#", "# Accessible filesystems, by reference, are maintained under '/dev/disk'", "# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info", "#", "/dev/mapper/vg_test3-lv_root /                       ext4    defaults        1 1", "UUID=e48217af-0ad9-45be-aa68-b0b1bbc88c97 /boot                   ext4    defaults        1 2", "/dev/mapper/vg_test3-lv_swap swap                    swap    defaults        0 0", "tmpfs                   /dev/shm                tmpfs   defaults        0 0", "devpts                  /dev/pts                devpts  gid=5,mode=620  0 0", "sysfs                   /sys                    sysfs   defaults        0 0", "proc                    /proc                   proc    defaults        0 0", "/dev/cdrom              /media/cdrom            iso9660 defaults        0 0"]}
    to retry, use: --limit @/ansible/playbook/if_else.retry

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=0    changed=0    unreachable=0    failed=1   
192.168.2.111              : ok=0    changed=0    unreachable=0    failed=1

8. register同时接收多个变量进行传递

#注:接收到的多个值可以用jinja的for循环方式显示每个值
[root@nfs-server playbook]# cat register_vars.yml
---
- hosts: webservers
  remote_user: root
  gather_facts: False
  tasks:
  - name: "register接受多个值测试"
    shell: "{{ item }}"
    with_items:
      - hostname
      - uname
    register: ret
  - name: "显示接收到的值"
    debug: 'msg="{% for i in ret.results %} {{ i.stdout }} {% endfor%}"'
[root@nfs-server playbook]# ansible-playbook register_vars.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [register接受多个值测试] *********************************************************************************************************************************
changed: [192.168.2.101] => (item=hostname)
changed: [192.168.2.111] => (item=hostname)
changed: [192.168.2.101] => (item=uname)
changed: [192.168.2.111] => (item=uname)

TASK [显示接收到的值] *****************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": " lamp1  Linux "
}
ok: [192.168.2.111] => {
    "msg": " lamp2  Linux "
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=1    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=1    unreachable=0    failed=0

Docker部署GitLab

现在docker越来越火, 很多开源服务纷纷支持docker, gitlab也不例外, 现在官方也支持docker部署. 在此之前, 也有第三方支持过docker, 安装步骤相对来讲会复杂一点, 会把gitlab, postgresql和redis分别打包成镜像, 然后通过容器连接使用, 这里不介绍这种方式.

一、确保机器上安装了docker并启动

# 安装docker
yum install docker
# 启动docker
systemctl start docker

安装完成后建议配置加速器(比如阿里云镜像加速), 否则拉取镜像会非常慢, 特别是gitlab镜像, 还是非常大的

二. 拉取镜像并启动

# 拉取镜像
docker pull gitlab/gitlab-ce
# 启动
docker run --detach 
--publish 22443:443 --publish 2280:80  --publish 2222:22 
--name gitlab 
--memory 4g 
--restart always 
--volume /srv/gitlab/config:/etc/gitlab 
--volume /srv/gitlab/logs:/var/log/gitlab 
--volume /srv/gitlab/data:/var/opt/gitlab 
gitlab/gitlab-ce:latest

–publish 暴露了容器的三个端口, 分别是https对应的443, http对应80以及ssh对应的22(如果不需要配置https, 可以不暴露) –memory 限制容器最大内存暂用4G, 这是官方推荐的 –volume 指定挂载目录, 这个便于我们在本地备份和修改容器的相关数据

未分类

三. 修改配置文件并重启

# 打开挂载的配置目录
vi /srv/gitlab/config/gitlab.rb

###################################################
# 添加外部请求的域名(如果不支持https, 可以改成http)
external_url 'https://gitlab.yinnote.com'
# 修改gitlab对应的时区 
gitlab_rails['time_zone'] = 'PRC'
# 开启邮件支持 
gitlab_rails['gitlab_email_enabled'] = true
gitlab_rails['gitlab_email_from'] = '[email protected]'
gitlab_rails['gitlab_email_display_name'] = 'Yinnote GitLab'
# 配置邮件参数
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.mxhichina.com"
gitlab_rails['smtp_port'] = 25
gitlab_rails['smtp_user_name'] = "[email protected]"
gitlab_rails['smtp_password'] = "xxxxxx"
gitlab_rails['smtp_domain'] = "yinnote.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = false        
###################################################

(选配) 如果配置了https, 需要导入证书

# 进入挂载配置目录
cd /srv/gitlab/config
# 创建密钥文件夹, 并放入证书
mkdir ssl
# 内容如下

未分类

重启服务

# 方法一: 重启容器(其中xxxxxx是容器id)
docker restart xxxxxx

# 方法二: 登陆容器, 重启配置
docker exec -it  xxxxxx bash   
gitlab-ctl reconfigure
  1. 通过ssh方式拉取代码时, 记住端口号是2222, 不是默认的22

  2. 如果没有配置https, 是无法通过https路径拉取代码的

ansible变量

ansible变量的使用方法

1、主机变量:直接在/etc/ansible/hosts文件中,主机的后边设置key=value的格式

注:inventory_hostname是ansible自带的变量,代表组中的每个主机

#在配置文件中定义主机变量
[root@nfs-server playbook]# cat /etc/ansible/hosts
[webservers]
192.168.2.101 key=101
192.168.2.111 key=111
#写playbook,测试主机变量
[root@nfs-server playbook]# cat hosts_vars.yml
---
- hosts: webservers
  remote_user: root
  tasks:
  - name: 显示主机变量
    debug: msg="The server ip is {{ inventory_hostname }},The key is {{ key }}"
#执行playbook,查看主机变量是否正常显示
[root@nfs-server playbook]# ansible-playbook hosts_vars.yml 

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.111]
ok: [192.168.2.101]

TASK [显示主机变量] ************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The key is 101"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The key is 111"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0

2、主机组变量

#在配置文件中定义主机组变量
[root@nfs-server playbook]# cat /etc/ansible/hosts
[webservers]
192.168.2.101 
192.168.2.111 
[webservers:vars]
ansiber_version=2.3
key=nginx
#写playbook,测试主机组变量
[root@nfs-server playbook]# cat hosts_group_vars.yml
---
- hosts: webservers
  remote_user: root
  tasks:
  - name: "显示主机组变量"
    debug: msg="The server ip is {{ inventory_hostname }},The key is {{ key }}"
#执行playbook,查看主机组变量是否正常显示
[root@nfs-server playbook]# ansible-playbook hosts_group_vars.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.111]
ok: [192.168.2.101]

TASK [显示主机组变量] *****************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The key is nginx"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The key is nginx"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0

3、通过/etc/ansible下的文件定义主机以及主机组变量

主机变量目录:host_vars(注:需要在此目录下为每个主机创建一个文件,在此文件中定义变量)

组变量目录:group_vars(注:需要在此目录下创建一个以主机组名命名的文件,在此文件中定义变量)

注:以上两个目录均不存在,需要自己手动创建;mkdir /etc/ansible/{host_vars,group_vars}

注:当需要为所有主机或者所有主机组创建一样的变量时,在host_vars和group_vars目录下,创建all文件并写入变量即可~

#在配置文件中定义主机组变量
[root@nfs-server playbook]# cat /etc/ansible/hosts
[webservers]
192.168.2.101 
192.168.2.111 
#在host_vars目录下创建以主机名为名字的文件,并在此文件中配置变量,变量格式为: key: value
[root@nfs-server ansible]# head host_vars/*
==> host_vars/192.168.2.101 <==
key: 101

==> host_vars/192.168.2.111 <==
key: 111
#在group_vars目录创建以主机组为名字的文件,并在此文件中配置变量,变量格式为:key: value
[root@nfs-server ansible]# head group_vars/*
name: nginx
#测试主机变量:利用上面写的主机变量playbook进行测试
[root@nfs-server playbook]# ansible-playbook hosts_vars.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.111]
ok: [192.168.2.101]

TASK [显示主机变量和主机组变量] ************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The key is 101"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The key is 111"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0   
#测试主机组变量:利用上面写的主机组变量playbook进行测试(把key名字改为了name)
[root@nfs-server playbook]# ansible-playbook hosts_group_vars.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.101]
ok: [192.168.2.111]

TASK [显示主机组变量] *****************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The application is nginx"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The application is nginx"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0

4、通过命令行传入变量,或者变量文件

-e,–extra-vars:此参数表示从命令行传入变量

-e “key=value”:直接传递变量的格式

-e “@vars.yml”:传递变量文件的格式

#在playbook中引用一个key为YOURNAME,此key通过命令行传入
[root@nfs-server playbook]# cat hosts_vars.yml 
---
- hosts: webservers
  remote_user: root
  tasks:
  - name: 显示主机变量和主机组变量
    debug: msg="The server ip is {{ inventory_hostname }},The key is {{ YOURNAME }}"
[root@nfs-server playbook]# ansible-playbook hosts_vars.yml -e "YOURNAME=liuzhengwei"

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.101]
ok: [192.168.2.111]

TASK [显示主机变量和主机组变量] ************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The key is liuzhengwei"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The key is liuzhengwei"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0 
#在playbook中引用一个key为YOURNAME,此key通过提前定义的变量文件传入
#下面文件为变量文件
[root@nfs-server playbook]# cat vars/vars.yml 
abc: LIUZHENGWEI
#通过命令行将此变量文件传入
[root@nfs-server playbook]# ansible-playbook hosts_vars.yml -e "@vars/vars.yml"

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.111]
ok: [192.168.2.101]

TASK [显示主机变量和主机组变量] ************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The key is LIUZHENGWEI"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The key is LIUZHENGWEI"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0

5.在playbook文件中定义变量:格式为key: value

#在playbook文件中定义变量abc
[root@nfs-server playbook]# cat hosts_vars.yml
---
- hosts: webservers
  remote_user: root
  vars:
    abc: liuzhengwei
  tasks:
  - name: 显示主机变量和主机组变量
    debug: msg="The server ip is {{ inventory_hostname }},The key is {{ abc }}"
#执行playbook测试变量是否生效
[root@nfs-server playbook]# ansible-playbook hosts_vars.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.111]
ok: [192.168.2.101]

TASK [显示主机变量和主机组变量] ************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The key is liuzhengwei"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The key is liuzhengwei"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0

6、在playbook文件中引用定义了变量的文件:vars_files

#在playbook文件中引用定义了变量的文件vars/vars.yml
[root@nfs-server playbook]# cat vars/vars.yml 
abc: LIUZHENGWEI
[root@nfs-server playbook]# cat hosts_vars.yml 
---
- hosts: webservers
  remote_user: root
  vars_files:
    - vars/vars.yml
  tasks:
  - name: 显示主机变量和主机组变量
    debug: msg="The server ip is {{ inventory_hostname }},The key is {{ abc }}"
#执行playbook测试变量是否生效
[root@nfs-server playbook]# ansible-playbook hosts_vars.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.101]
ok: [192.168.2.111]

TASK [显示主机变量和主机组变量] ************************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The server ip is 192.168.2.101,The key is LIUZHENGWEI"
}
ok: [192.168.2.111] => {
    "msg": "The server ip is 192.168.2.111,The key is LIUZHENGWEI"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=2    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=2    changed=0    unreachable=0    failed=0

7、使用register内的变量

ansible playbook内task之间可以互相传递变量,比如我们总共有2个tasks,其中第2个task是否执行需要判断第一个task运行后的结果,这个时候我们就得在task之间传递数据,需要把第一个task执行的结果传递给第2个task.Ansible task之间传递数据使用register方式。

#注:这是第1个task把执行hostname的结果register给info这个变量
[root@nfs-server playbook]# cat register_var.yml 
---
- hosts: webservers
  remote_user: root
  tasks:
  - name: register variables
    shell: hostname
    register: info
  - name: display variables
    #debug: msg="The variable is {{ info['stdout'] }}"
    debug: msg="The variable is {{ info.stdout }}"
#注:register返回结果是字典的方式,所以用info.stdout或info['stdout']的方式返回结果
[root@nfs-server playbook]# ansible-playbook register_var.yml

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.101]
ok: [192.168.2.111]

TASK [register variables] ******************************************************************************************************************************
changed: [192.168.2.101]
changed: [192.168.2.111]

TASK [display variables] *******************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "The variable is lamp1"
}
ok: [192.168.2.111] => {
    "msg": "The variable is lamp2"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=3    changed=1    unreachable=0    failed=0   
192.168.2.111              : ok=3    changed=1    unreachable=0    failed=0

8、使用vars_prompt传入变量(vars_prompt是以交互式的方式给定义好的参数传入变量值)

注:交互式传入的变量值可以是加密的也可以是不加密的,用private进行定义

#交互式输入变量值,传递给tasks,private=no表示输入的变量值不加密,private=yes表示加密
[root@nfs-server playbook]# cat vars_prompt.yml 
---
- hosts: all
  remote_user: root
  vars_prompt:
  - name: "one"
    prompt: "please input one value"
    private: no
  - name: "two"
    prompt: "please input two value"
    private: yes
  tasks:
  - name: display one value
    debug: msg="one value is {{ one }}"
  - name: display two value
    debug: msg="two value is {{ two }}"
[root@nfs-server playbook]# ansible-playbook vars_prompt.yml 
please input one value: Liu Zhengwei
please input two value: 

PLAY [webservers] **************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************
ok: [192.168.2.101]
ok: [192.168.2.111]

TASK [display one value] *******************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "one value is Liu Zhengwei"
}
ok: [192.168.2.111] => {
    "msg": "one value is Liu Zhengwei"
}

TASK [display two value] *******************************************************************************************************************************
ok: [192.168.2.101] => {
    "msg": "two value is age is 28"
}
ok: [192.168.2.111] => {
    "msg": "two value is age is 28"
}

PLAY RECAP *********************************************************************************************************************************************
192.168.2.101              : ok=3    changed=0    unreachable=0    failed=0   
192.168.2.111              : ok=3    changed=0    unreachable=0    failed=0

Docker compose network笔记

1. 简述

默认docker compose仅仅会为你的app生成一个network, 所有依赖的服务都会加入这个网络中, 在docker-compose文件中定义的Projectname就是每一个镜像的hostname;

# 例子
# For example, suppose your app is in a directory called myapp,
# and your docker-compose.yml looks like this
version: "3"
services:
  web:
    build: .
    ports:
      - "8000:8000"
  db:
    image: postgres
    ports:
      - "8001:5432"
# 说明: A network called myapp_default is created.
我们可以使用 postgres://db:5432 方式链接postgres数据库

2. 自定义network,实现复杂网络结构

现实情况中的场景往往更复杂,一个network很难满足更复杂的需求; 比如说, 不同的Project在不同的网络中;

# 例子:
version: "3"
services:

  proxy:
    build: ./proxy
    networks:
      - frontend
  app:
    build: ./app
    networks:
      - frontend
      - backend
  db:
    image: postgres
    networks:
      - backend
networks:
  frontend:
    # Use a custom driver
    driver: custom-driver-1
  backend:
    # Use a custom driver which takes special options
    driver: custom-driver-2
    driver_opts:
      foo: "1"
      bar: "2"
# 说明
app可以和proxy和db互通, 但是proxy和db之间不能互通;

3. 修改默认网络

version: "3"
services:
  web:
    build: .
    ports:
      - "8000:8000"
  db:
    image: postgres
networks:
  default:
    # Use a custom driver
    driver: custom-driver-1

4. 加入一个已存在的网络

networks:
  default:
    external:
      name: my-pre-existing-network
# 一个实际应用场景
# 在项目重构迁移阶段, 老系统的测试环境old_test_docker_compose.yml, 新系统的测试环境new_test_docker_compose.yml, 老系统有部分服务需要调用新系统的
# 1. 第一步: 创建一个网络
docker network create share
# 2. 第二步: 在新老系统都添加这个网络为默认网络
networks:
  default:
    external:
      name: share

如何使用Docker、Docker-Compose和Rancher搭建部署Pipeline(四)

重磅系列干货,如何使用Docker、Docker Compose和Rancher搭建deployment pipeline。

本文为最后一篇,讨论如何实现consul的服务发现。

在这篇文章中,我们将讨论如何用Rancher实现consul的服务发现。

如果你还没有准备好,推荐你阅读本系列中先前的文章:

第一篇:CI /CD和Docker入门
第二篇:使部署逻辑向使用Docker Compose更进一步
第三篇:借力Rancher完成容器编排

在这构建部署流水线系列的最后一篇文章中,我们将探讨在转换到Rancher进行集群调度时面临的一些挑战。在之前的文章中,我们通过使用Rancher执行调度,让运维人员无须再负责选择每一次容器运行的位置。要使用这个新方案,我们必须让环境的其他部分知道调度程序放置这些服务的位置,以及如何访问它们。我们还将讨论如何使用标签来操作调度程序,以调整容器放置位置,并避免端口绑定冲突。最后,我们将通过利用Rancher的回滚功能优化我们的升级过程。

在引入Rancher之前,我们的环境是一个相当静态的环境。我们总是将容器部署到相同的主机上,而部署到不同的主机则意味着我们需要更新一些配置文件以反映新位置。例如,如果我们要添加’java-service-1’应用程序的一个附加实例,我们还需要更新load balancer以指向附加实例的IP。使用调度器让我们无法预测容器部署的位置,并且我们需要动态配置环境,使其能自动适应变化。为此,我们需要使用服务注册和服务发现。

服务注册表为我们提供了应用程序在环境中的位置的单一来源。和硬编码服务位置不同,我们的应用程序可以通过API查询服务注册表,并在我们的环境发生变化时自动重新配置。Rancher使用Rancher的DNS和元数据服务提供了开箱即用的服务发现。然而,混合使用Docker和非Docker应用程序时,我们不能完全依赖Rancher来处理服务发现。我们需要一个独立的工具来跟踪我们所有服务的位置,consul就符合这个要求。

我们不会详细说明如何在您的环境中设置Consul,但是,我们将简要描述我们在ABC公司使用Consul的方式。在每个环境中,我们都有一个部署为容器的Consul集群。我们在环境中的每个主机上都部署一个Consul代理,如果主机正在运行Docker,我们还会部署一个注册器容器。注册器监视每个守护进程的Docker事件API,并在生命周期事件期间自动更新Consul。例如,在新容器被部署后,注册器会自动在Consul中注册该服务。当容器被删除时,注册器撤销它的注册。

Consul服务列表

在Consul中注册所有服务后,我们可以在负载均衡器中运行consul-template,根据Consul中存储的服务数据动态填充上游列表。对于我们的NGINX负载均衡器,我们可以创建一个模板来填充’java-service-1’应用程序的后端:

# upstreams.conf
upstream java-service-1 { 
{{range _, $element := service "java-service-1"}}    
       server {{.Address}}:{{.Port}}; 
{{else}}     
       server 127.0.0.1:65535; # force a 502{{end}} }

此模板在Consul中查找注册为“java-service-1”的服务的列表。然后它将循环该列表,添加具有该特定应用程序实例的IP地址和端口的服务线。如果在Consul中没有注册任何“java-service-1”应用程序,我们默认抛出502以避免NGINX中的错误。

我们可以在守护进程模式下运行consul-template,使其监控Consul的更改,在发生更改时重新渲染模板,然后重新加载NGINX以应用新配置。

TEMPLATE_FILE=/etc/nginx/upstreams.conf.tmpl
RELOAD_CMD=/usr/sbin/nginx -s reload 
consul-template -consul consul.stage.abc.net:8500      
       -template "${TEMPLATE_FILE}:${TEMPLATE_FILE//.tmpl/}:${RELOAD_CMD}"

通过使用我们的负载均衡器设置来动态地改变其余的环境变化,我们可以完全依赖Rancher调度器来做出我们的服务应该在哪里运行的复杂的决定。但是,我们的“java-service-1”应用程序在Docker主机上绑定TCP端口8080,如果在同一主机上调度了多个应用程序容器,则会导致端口绑定冲突并最终失败。为了避免这种情况,我们可以通过调度规则来操作调度器。

通过在docker-compose.yml文件中使用容器标签来提出条件,是Rancher给我们的一种操作调度器的方法。条件可以包括亲和规则、否定、至“软”强制(意味着尽可能地避免)。在我们使用’java-service-1’应用程序的情况下,我们知道在给定时间只有一个容器可以在主机上运行,因此我们可以基于容器名称设置反关联性规则。这将使调度程序查找一个未运行名称为“java-service-1”的容器的Docker主机。我们的docker-compose.yml文件看起来像下面这样:

java-service-1:   
   image: registry.abc.net/java-service-1:${VERSION}   
   container_name: java-service-1   
   ports:    
         - 8080:8080   
    labels:     
         io.rancher.scheduler.affinity:container_label_ne: io.rancher.stack_service.name=java-service-1

注意“标签”键的引入。所有调度规则都作为标签被添加。标签可以被添加到Docker主机和容器。当我们在Rancher注册我们的主机时,我们可以将它们与标签关联,以后就可以切断调度部署。例如,如果我们有一组使用SSD驱动器进行存储优化的Docker主机,我们可以添加主机标签storage=ssd。

Rancher主机标签

需要利用优化存储主机的容器可以添加标签来强制调度程序仅在匹配的主机上部署它们。我们将更新我们的“java-service-1”应用程序,以便只部署在存储优化的主机上:

java-service-1:   
    image: registry.abc.net/java-service-1:${VERSION}   
    container_name: java-service-1   
    ports:    
         - 8080:8080   
    labels:     
         io.rancher.scheduler.affinity:container_label_ne: io.rancher.stack_service.name=java-service-1     
         io.rancher.scheduler.affinity:host_label: storage=ssd

通过使用标签,我们可以根据所需的容量,而不是个别主机运行特定的容器集,来精细地调整我们的应用程序部署。切换到Rancher进行集群调度,即使您仍然有必须在特定主机上运行的应用程序。

最后,我们可以利用Rancher的回滚功能优化我们的服务升级。在我们的部署工作流中,通过调用rancher-compose来指示Rancher在该服务堆栈上执行升级以部署服务。升级过程大致如下:

  1. 通过拉取一个新的镜像来启动升级
  2. 逐一地,现有容器被停止并且新容器被启动
  3. 部署程序登录到UI并选择“完成升级”时,升级完成,
  4. 已停止的旧服务容器被删除

Rancher升级

当给定服务的部署非常少时,此工作流就好了。但是,当某个服务处于“升级”状态(在部署者选择“完成升级”之前)时,在执行“完成升级”或是“回滚”操作之前,你都不能对它进行任何新的升级”。rancher-compose实用程序让我们可以选择以编程方式选择要执行的操作,以部署程序者的身份执行操作。例如,如果您对服务进行自动测试,则可以在rancher-compose升级返回后调用此类测试。根据这些测试的状态,rancher-compose可以被再次调用,这次我们告诉堆栈“完成升级”或“回滚”。我们部署Jenkins作业的一个原始示例可能如下:

# for the full job, see part 3 of this series
/usr/local/bin/rancher-compose --verbose    
    -f ${docker_dir}/docker-compose.yml    
    -r ${docker_dir}/rancher-compose.yml    
    up -d --upgrade 
JAVA_SERVICE_1_URL=http://java-service-1.stage.abc.net:8080/api/v1/status 
if curl -s ${JAVA_SERVICE_1_URL} | grep -q "OK"; then   

# looks good, confirm or "finish" the upgrade   
    /usr/local/bin/rancher-compose --verbose     
         -f ${docker_dir}/docker-compose.yml      
         -r ${docker_dir}/rancher-compose.yml      
         up --confirm-upgrade 
else   

     # looks like there's an error, rollback the containers   
     # to the previously deployed version   
     /usr/local/bin/rancher-compose --verbose      
        -f ${docker_dir}/docker-compose.yml      
        -r ${docker_dir}/rancher-compose.yml      
        up --rollback 
fi

这个逻辑将调用我们的应用程序端点来执行简单的状态检查。如果输出显示的是‘OK’,那么我们完成升级,否则我们需要回滚到以前部署的版本。如果您没有自动测试,另一个选择是简单地总是完成或“确认”升级。

# for the full job, see part 3 of this series
/usr/local/bin/rancher-compose --verbose    
    -f ${docker_dir}/docker-compose.yml    
    -r ${docker_dir}/rancher-compose.yml    
    up -d --upgrade --confirm-upgrade

如果不久以后,您确定需要回滚,就使用相同的部署作业简单地重新部署以前的版本。这确实不像Rancher的升级和回滚功能那么友好,但它通过使堆栈不处于“升级”的状态来解锁将来的升级。

当服务在Rancher中回滚时,容器将被重新部署到以前的版本。当使用通用标记如“latest”或“master”部署服务时,可能会出现意外的后果。例如,让我们假设’java-service-1’应用程序以前被部署了标签’latest’。对图像进行更改,推送到注册表,Docker标签“latest”被更新为指向此新映像我们使用标签“latest”继续升级,在测试后决定应用程序需要回滚。使用Rancher滚动堆栈仍然会重新部署最新的映像,因为标签“latest”尚未被更新为指向上一个映像。回滚可以在纯技术术语中实现,但是部署最近的工作副本的预期效果完全无法实现。在ABC公司,我们通过始终使用与应用程序版本相关的特定标记来避免这种情况。因此,不要使用标记latest”部署我们的“java-service-1”应用程序,我们可以使用版本标签“1.0.1-22-7e56158”。这保证回滚将始终指向我们的应用程序在环境中的最新工作部署。

我们希望我们分享的经验对你们有所帮助。这有助于我们有条不紊地采用Docker,稳步改进我们的流程,并让我们的团队能熟悉这些概念。对更自动化的部署工作流进行增量更改,使组织能够更快地实现自动化的优势,部署团队可以更加务实地决定他们在流水线中需要什么。我们的经历证明Rancher在可行性、自动化、甚至团队协作方面都是成功的。我们希望分享这些我们在Docker应用过程中获得的经验教训将有助于您自己的应用过程。

如何使用Docker、Docker-Compose和Rancher搭建部署Pipeline(三)

该系列文章分享了用Docker、Docker-Compose和Rancher完成容器部署工作流的经验。本文将探讨怎样运行Rancher,解决只使用Docker-Compose时面临的主机负载失衡、获取服务状态困难、多环境下主机与集群难以管理等的问题。

在这一部分,我们将一步步的走进Rancher,细致的探讨Rancher将如何解决在部署与容器管理时出现的种种的问题。回顾教程的第二部分,你会发现我们已经将应用的部署迁移至Docker Compose,并且已经建立了一系列工作步骤来部署我们的应用。这将使得开发人员能够轻松的对他们的应用部署逻辑进行修正,运维人员也可以查看应用的部署时间。当然,在上一个部分教程的一系列操作中,也存在一些显而易见的问题需要解决。

使用Docker-Compose时面临的挑战

首先,运维人员必须手动地调整所有服务的执行计划。部署人员需要决定将哪一个应用部署至哪一台主机,这意味着部署人员需要时刻对每一台主机的剩余可用资源都有了解,如果某一台主机或者容器崩溃了,部署的操作人员将需要对应用进行重新部署。实际生产中,这意味着主机常常处于负载失衡的状态,并且服务在崩溃之后需要很长时间才能得到恢复。

其次,使用Docker-Compose时,想要获得你的服务的当前状态是十分困难的。举个例子来说,我们经常会从运维人员、项目经理以及开发者口中听到这样的问题:“现在部署环境中运行的到底是XX应用程序的哪个版本?”如果我们采用的是手动调整服务的执行计划的方式,想要得到这个问题的答案通常需要询问指定的进行操作的工程师,工程师们需要登陆服务器并运行docker中的ps命令来查看容器的信息。然而面对这些问题,Rancher将会给我们提供极大的便利:每个人都可以非常容易地获取已经部署的服务的信息,而不需要临时请求运维人员的帮助。

使用Rancher之前,我们试着了解过不少其他能够管理Docker主机或集群的解决方案。然而这些解决方案都没有注意到这是对Docker主机或集群在多种环境(multi-environment)下的管理,这将成为最大的麻烦与负担之一。如果有服务以不同的负载运行在8种不同的环境下,我们需要的是一个统一的方式来管理集群,而不会想要访问8个不同的服务。并且,我们希望让重新构建环境对于我们而言,变成分分钟就能完成的任务,这样开发者就可以随意地更改开发环境。然而,对于生产环境而言,我们希望提供给他们的只是有限的只读访问权限。面对这样的需求,一个采用基于角色的访问控制(RBAC)模型的集中管理方案就显得十分必要了。我们最初决定尝试Rancher就是因为它在部署上非常简单。

当Rancher面临这些挑战

在短短半天的时间里,使用AWS ELB、Elasticache、RDS和现有的Docker主机,我们已经将Rancher部署好并成功运行。能够方便地配置认证信息也是Rancher的优点之一。

我们并不会深入Rancher本身部署的细节,Rancher部署文档中已经说的很明白了。相反,我们将从刚刚完成初始设置那一步开始,说明将如何将原有的设置(教程第一部分和第二部分中所提及的)迁移进来。

我们就从创建不同的环境开始吧,为了使得这个过程尽量简单些,我们将对开发环境(dev)、部署环境(stage)以及生产环境(prod)分别进行设置。每个环境都已有运行在Ubuntu之上的Docker主机,且这些Docker主机是由内部的Ansible配置的,Ansible安装了Docker、我们的监控代理、并进行了一些组织特定的更改。在Rancher上,你只需要运行一条命令,将Docker主机在Rancher server内部进行注册,就可以将已有的Docker主机添加至每个环境中。

添加一台Rancher主机

在大多数情况下,想要添加一台主机需要经过一系列的操作:通过鼠标在网页上完成一些点击,接下来切换至某个特定的环境,最后在终端系统上输入命令。然而,如果你使用Rancher API,我们可以在Ansible工具的帮助下使得这一系列的操作转化为完全自动化的设置。出于好奇,在下面我们截取了playbook中有关这一操作的部分内容(大多是根据 Hussein Galas的repo中的内容做出的逻辑上的修改而得到的)。

name: install dependencies for uri module
  apt: name=python-httplib2 update_cache=yes
  name: check if the rancher-agent is running
  command: docker ps –filter ‘name=rancher-agent’
  register: containers
  name: get registration command from rancher
  uri:
  method: GET
  user: “{{ RANCHER_API_KEY }}”
  password: “{{ RANCHER_SECRET_KEY }}”
  force_basic_auth: yes
  status_code: 200
  url: “https://rancher.abc.net/v1/projects/{{ RANCHER_PROJECT_ID }}/registrationtokens”
  return_content: yes
  validate_certs: yes
  register: rancher_token_url
  when: “‘rancher-agent’ not in containers.stdout”
  name: register the host machine with rancher
  shell: >
  docker run -d –privileged
  -v /var/run/docker.sock:/var/run/docker.sock
  {{ rancher_token_url.json[‘data’][0][‘image’] }}
  {{ rancher_token_url.json[‘data’][0][‘command’].split() | last}}
  when: “‘rancher-agent’ not in containers.stdout”

随着工作的一步步进行,我们已经完成了环境的创建并已经将主机在Rancher server中注册,现在就让我们来了解一下,如何将我们的部署工作流整合至Rancher中。我们知道,对于每一台Docker来说,其中都有着一些正在运行的容器,这些系统的部署是通过Ansible工具借助Jenkins完成的。Rancher提供了以下开箱即用的功能:

  • 管理已有的容器(比如:启动、修改、查看日志、启动一个交互式的shell)
  • 获得关于运行中的和停止运行的容器的信息(比如:镜像信息、初始化命令信息、命令信息,端口映射信息以及环境变量信息)
  • 查看主机和容器层级上的资源使用情况(比如:CPU使用率、内存占用率、以及磁盘和网络的使用情况)

独立的容器

很快,我们就已经将Docker主机注册至Rancher Server中,现在我们可以查看容器在各种环境下的运行状态信息了。不仅如此,如果想要将这些信息分享给其他团队,我们仅仅需要针对某个环境给予他们一些有限的权限。通过以上的方式,在想要获得状态信息时我们就完全没有必要请求操作人员登录Docker主机,再通过人工的方式去查询,同时这样也减少了申请获得环境信息的请求的数目,因为我们已经将某些访问权限分配至各个团队了。举个例子来说,如果为开发团队分配环境信息的只读权限,那么将会在开发团队与部署操作团队之间架起一座沟通的桥梁,这样两个团队都会对这个环境的状态比以往更加的关心。在这个基础上,故障的排除也变成了一种小组间相互合作的过程,而不是以往的那种单向的、依赖同步信息流的解决方式,相互合作的方式也会减少解决突发事件的总时间。

到现在为止,我们已经将已有的Docker主机加入Rancher Server,并且基于已经阅读完了的教程的第一部分关于Jenkins和Rancher的内容,下一步,我们打算改进的部分是我们已有的部署流水线,我们将会对已有的部署流水线进行修改,以便于使用Rancher compose,Rancher Compose将代替之前Ansible工具提到的Docker compose。不过在我们深入下一部分之前,我们首先需要了解关于Rancher的应用、调度、Docker Compose和Rancher Compose的一些信息。

应用与服务:Rancher将每个独立的容器(指的是部署在Rancher之外的容器,或者是通过Rancher UI生成的一次性功能的容器)、应用和服务彼此分离开。简单地说,应用是一组服务,而所有容器都需要利用服务(关于应用和服务的内容之后将会由更加详细的介绍)以构建一个应用。独立的容器需要手动地进行调度。

调度:在之前的部署技术中,运维人员需要决定容器应当在哪一台主机上运行。如果使用的是部署脚本,那么意味着运维人员需要决定部署脚本在哪一台或哪几台主机上运行;如果使用Ansible,这将意味着运维人员需要决定哪些主机或组需要到Jenkins中工作。不论是哪一种方式,都需要运维人员去做一些决定,但是在大多数情况下,他们做出的决定都缺乏一些可靠的依据,这对我们的部署工作很是不利(比如说某一台主机的CPU使用率高达100%)。很多解决方案,比如像Docker Swarm、Kubernetes、Mesos和Rancher都采用了调度器来解决这类问题。对于需要执行的某个操作,调度器将会请求获得一组主机的信息,并判断出哪几台是适合执行这个操作的。调度器会根据默认的需求设定或者用户定义的特定需求,比如CPU使用率高低、亲和性或反亲和性规则(比如:禁止在同一台主机上部署两个相同容器)等类似的需求,以逐渐缩小主机选择的范围。如果我是一个负责部署的运维人员,调度器将会极大的减少我的工作负担(尤其是我在深夜加班忙于部署时),因为调度器对以上信息的计算比我快的多,也准的多。Rancher在我们通过应用部署服务的时候能够提供一个开箱即用调度器。

Docker compose:Rancher使用Docker compose来创建应用并定义服务。由于我们已经将服务转化为Docker compose的文件,我们在此基础上创建应用就变得容易了许多。应用可以手动的从UI界面中创建,也可以通过Rancher compose在命令行(CLI)下快速的创建。

Rancher compose:Rancher compose是一种通过命令行(CLI)让我们得以对Rancher中的每一种环境的应用和服务进行方便的管理的工具。同时,通过rancher-compse.yml文件,Rancher compose还能允许对Rancher工具进行一些其他访问。这是一个纯粹的附加的文件,将不会取代原有的docker-compose.yml文件。在rancher-compose.yml文件中,你可以定义以下内容,比如说:

  • 每种服务的升级策略信息
  • 每种服务的健康检查信息
  • 每种服务的需求规模信息

这些都是Rancher中非常实用的亮点,如果你使用Docker Compose或者Docker daemon,这些内容你都是获取不到的。如果想要查看Rancher Compose能提供的所有特性,你可以查看这个文档

通过将已有的部署工作交给Rancher Compose来替代之前的Ansible工具,我们能够很轻松的将服务迁移并部署为Rancher应用的形式。之后,我们就能够去除DESTINATION参数了,但我们依然保留VERSION参数,因为我们在插入docker-compose.uml文件的时候还要使用它。以下是使用Jenkins部署时,部署逻辑的shell片段:

export RANCHER_URL=http://rancher.abc.net/
  export RANCHER_ACCESS_KEY=…
  export RANCHER_SECRET_KEY=…

if [ -f docker/docker-compose.yml ]; then
  docker_dir=docker
  elif [ -f /opt/abc/dockerfiles/java-service-1/docker-compose.yml ]; then
  docker_dir=/opt/abc/dockerfiles/java-service-1
  else
  echo “No docker-compose.yml found. Can’t continue!”
  exit 1
  fi

if ! [ -f ${docker_dir}/rancher-compose.yml ]; then
  echo “No rancher-compose.yml found. Can’t continue!”
  exit 1
  fi

/usr/local/bin/rancher-compose –verbose 
  -f ${docker_dir}/docker-compose.yml 
  -r ${docker_dir}/rancher-compose.yml 
  up -d –upgrade

阅读完代码段,我们可以发现其主要包括以下内容:

我们定义了以环境变量的方式如何访问我们的Rancher server。
需要找到docker-compose.yml文件,否则将会任务将会报错退出。
需要找到rancher-compose.yml文件,否则任务将会报错退出。
运行Rancher-compose,并告诉它不要block并且使用-d命令输出日志,使用-upgrade命令更新一个已经存在的服务。

也许你已经发现了,在绝大部分,代码的逻辑都是相同的,而最大的区别就是使用rancher-compose代替使用Ansible工具完成部署,并对每一个服务添加了rancher-compose.yml文件。具体到我们的java-service-1应用,docker-compose文件和rancher-compose文件现在是这样的:

docker-compose.yml
  java-service-1:
  image: registry.abc.net/java-service-1:${VERSION}
  container_name: java-service-1
  expose:
  – 8080
  ports:
  – 8080:8080
  rancher-compose.yml
  java-service-1:
  scale: 3

在开始部署工作之前,我们先回顾一下部署工作的流程:

  • 开发人员将代码的修改推送至git上
  • 使用Jenkins对代码进行单元测试,在测试工作结束之后触发下游工作
  • 下游工作采用新的代码构建一个docker镜像,并将其推送至我们自己的Docker镜像仓库中
  • 创建包含应用名、版本号、部署环境的deployment ticket
DEPLOY-111:
  App: JavaService1, branch “release/1.0.1”
  Environment: Production
  • 部署工程师针对应用运行Jenkins的部署工作,运行时需要将版本号作为参数。
  • Rancher compose开始运行,对于某个环境创建或更新应用,并且当达到所需规模的时候,结束这个工作
  • 部署工程师以及开发工程师分别手动地对服务进行校验
  • 部署工程师在Rancher UI中确认完成升级

关键点

使用Rancher进行我们的服务部署时,我们从Rancher内建的调度、弹性伸缩、还原、升级、和回滚等工具中获得极大的便利,使得我们在部署过程中没有花太大的力气。同时我们发现,在将部署工作从Ansible工具中迁移至Rancher的工作量也是很小的,仅仅需要在原有的基础上增加rancher-compose.yml文件。然而,使用Rancher来处理我们容器的调度意味着我们将难以确认我们的应用到底是在哪台主机上运行的。比方说,之前我们并没有决定java-service-1应用在哪里运行,对于后端,在进行负载均衡相关操作时,该应用就没有一个静态的IP。我们需要找到一种办法,使得我们的各种应用之间能够相互察觉到对方。最终,对于我们的java-service-1应用,我们将明确地将应用容器所在的docker主机的8080端口与应用绑定,不过,如果有其他服务与应用绑定为相同的端口,它将会启动失败。通常负责调度决策的工程师将会对以上的事务进行处理。然而,我们最好将这些信息通知调度器以避免这样的事情发生。

在本教程的最后一个部分,我们将继续探索一些方案来解决在使用亲和性规则、主机标签、服务探索以及智能升级和回滚等特性时出现的问题。

如何使用Docker、Docker-Compose和Rancher搭建部署Pipeline(二)

该系列文章分享了用Docker、Docker-Compose和Rancher完成容器部署工作流的经验。

本文将分享如何解决自动化不够、测试困难、追踪及新增环境变量易错等痛点,且使部署逻辑向使用Docker Compose的应用更近一步。

在这一系列文章的第一篇中,我们分享了只用Docker时我们开发的初步的工作流,如何创建一个基础的构建和部署流水线。容器的部署方式不再是在登陆server的时候从内存中输入Docker命令。我们已经通过Jenkins server实现了镜像的自动化构建。我们使用脚本将Docker命令进行封装,将其存储到GitHub中并且设置版本。目前我们正采取措施,通过逐步改善现有过程来实现持续部署。然而,仍有一些痛点需要我们去解决。在这篇文章中,我们将看看如何使用Docker Compose 和 Ansible来改善此设计。

在部署镜像时,工程师需要登录到服务器,并从shell运行我们的Docker wrapper脚本。这不是很好的解决方法,因为它也需要开发者进行等待。没有任何一方会从在这种方式中获益(作为一个工程师,当你去做某件你很了解并且很容易自动化的事情时,你有多少次被打断了?)由于每一次部署都是通过操作者电脑中的SSH会话来执行的,因此部署过程是不可见的。

如果你对我们的部署脚本还有印象,你会发现它看起来像下面的代码段:

未分类

实际上,我们做的是将Docker run命令语句进行抽象,由此工程师将不需要知道每个图像成功运行时所需要的确切的参数。虽然这改善了必须全部记住并且手动输入所有Docker参数的现状,但同时也会带来新的问题:

  • 每个容器的逻辑都存储在同一文件中,这使得对应用程序部署逻辑的更改更难追踪;
  • 当开发者需要测试或者修改参数时,需要被迫理清脚本中的逻辑,而不是能够在某一特定的程序中轻松地阅读和修改参数。

在我们的工作流中,Docker Compose是一个更适合使用的工具,它同样可以将部署参数进行编码,并且在YAML文件中指定,此文件就是docker-compose.yml。Docker Compose不仅帮助我们解决了上面提到的难点,而且也可以使我们从社区未来的工作中获益。下面让我们理清部署脚本,并且为我们的JAVA程序示例创建一个Compose文件。首先,我们需要基于原来的部署逻辑创建一个docker-compose.yml文件:

未分类

现在,部署容器只需要在与docker-compose.yml文件相同目录下输入以下命令:

docker-compose up

它将根据compose文件中设置的参数启动一个容器。在compose文件中一个重要的变量是${VERSION} 。Docker Compose 可以从当前的shell环境中插入compose文件里所列出的参数。我们可以通过简单地运行以下语句来设置参数:

VERSION=1.0.0 docker-compose up

它将从我们的私有镜像仓库挑出标记1.0.0的镜像,以此启动java-service-1程序。如果没有设置VERSION变量,Docker Compose将产生一条警告信息,并且用空字符串代替变量值,由此,具有最新版本标签的镜像将会被挑出。因此,正确地设置变量是相当重要的。

作为开发过程的一部分,我们希望开发人员能够在本地建立服务并且测试他们的服务。然而,由于docker-compose.yml指向私有镜像仓库的镜像,运行docker-compose将从最近构建的镜像中开启服务而不是从本地资源中开启。理想情况下,开发者可以通过运行以下代码使用典型的docker-compose工作流:

未分类

Docker Compose能在不修改docker-compose.yml文件的情况下,让我们做到这一点。我们可以使用多个文件来覆盖我们在本地测试中想要改变的任何参数。在docker-compose.override.yml中,我们指定一个key而不是一个镜像,并且移除了对VERSION变量的需求。由于这是一个覆盖文件,我们不需要复制任何额外的设置,如端口设置:

未分类

使用Docker Compose而非部署脚本之后,我们可以:

  • 在源代码中存储每个compose文件,这与Dockerfile类似;
  • 不再需要复杂的部署脚本;
  • 允许开发人员在本地轻松地测试并修改应用程序。

现在我们有了java-service-1程序的compose文件,我们可以将它从我们的部署脚本中删除,因此文件组织与下面的结构类似:

未分类

此时,我们仍然没有解决镜像构建和部署之间的问题。在docker-compose.yml文件中包含了所有的部署逻辑,但是它如何在环境中运行直至结束的呢?正好现在我们在运行与UNIX和TPC socket相关的Docker守护进程,是时候讨论一些与安全有关的问题了。

我们的情况是,工程师登录到服务器上,手动运行每个服务器所需容器的部署脚本。默认情况下,当在局部运行Docker命令时,它将使用UNIX socket /var/run/docker.sock连接Docker守护进程;或者让守护进程监听TCP socket,这允许用户远程连接到每个Docker守护进程,使得工程师能够像登录到主机一样运行命令。这为连接方式提供了更大的灵活性,但是没有考虑到一些开销和安全问题:

  • 通过网络连接增加了安全隐患;
  • 增加了对于基于主机或者基于网络的ACLs需求;
  • 保护守护进程需要分布式CA和客户端认证。

另一种可能的方法是不使用基于UNIX socket的方式运行Docker守护进程,而使用SSH来运行命令。已经建立的ACLs将保护SSH端口,并且它只允许通过SSH授权的特定的用户才能使用Docker守护进程。虽然这不是最简洁的方法,但是它有助于保持较低的运行开销,并且使安全隐患降到最低。这点是非常重要的,尤其是对于细粒度的稀疏的任务队列而言。

为了有利于通过SSH运行Docker命令,我们可以使用Ansible——一个流行的编排和配置管理工具。它是无代理的,并且允许通过SSH连接运行“剧本”(服务器任务集合)。一个运行docker-compose命令的简单的剧本如下所示:

未分类

如果你对Ansible没有过多了解,你也许可以通过上面的剧本大致了解到我们想做什么。它们按顺序一步步执行,具体如下所示:

  1. Ansible将通过SSH连接到目标服务器(允许通过使用DESTINATION变量来指定主机)

  2. 在每个服务器中,Ansible会通过执行shell命令登录到公司私有的镜像仓库

  3. Ansible将位于Jenkins(运行ansible剧本的服务器)中的docker-compose.yml文件复制到每个目标服务器中的/tmp/docker-compose.yml下

  4. 在每个目标服务器中运行docker-compose命令

  5. 通过删除远程的/tmp/docker-compose.yml文件进行清理

一个shell脚本可以被运用在同一个事件中。然而在Ansible中,我们将很容易的使任务并行化并且得到经过良好测试的模块,通过使Ansible与新的部署剧本相结合,我们可以远程启动容器,相较于工程师登录到主机、人工运行命令,这是一个重要的进步。为了在部署过程和状态中提供更大的可视性,我们将建立Jenkins任务来运行Ansible代码。通过使用Jenkins,在未来我们可以轻松地将构建和部署任务集成起来,从而得到额外的好处。

Jenkins任务需要两个参数:目标主机(传递给剧本中的DESTINATION变量)和部署镜像的版本(在docker-compose.yml文件中插入VERSION变量)。大多数任务的构建部分是一个shell构建器,它将试图找到程序中的docker-compose.yml文件,然后通过传递变量(用-e)到剧本中,运行ansible-playbook命令:

未分类

虽然看起来我们似乎只对工作流做了微小的变化,但是我们正一步一步地向构建一个持续部署模型迈进:

  • 部署是可以被审查的。我们使用日志来记录输出什么、何时输出、以及哪些主机是目标主机等信息,这一切都归功于Jenkins。
  • 程序部署逻辑已经从一个单一的脚本分散到存储在程序源代码中的单独的docker-compose.yml文件中,这意味着我们可以轻松地通过git更改程序部署逻辑。在程序源文件或者部署文件发生变化时,我们也可以容易地进行构建和部署。

虽然这些改进解决了某些问题,但是它们所带来的新的问题也成为了焦点:

  • 哪个容器的哪个版本会被部署到何地?
  • 容器在被部署后会处于哪种状态?
  • 我们如何确定哪个主机成为程序的目标主机?

在这一系列接下来的文章中,我们将探讨怎样运行Rancher以及使用它的原因,尤其是它如何解决上述的问题。与此同时,我们也讨论它在业务和开发团队中所起到的意想不到的桥梁作用。

学习使用Docker、Docker-Compose和Rancher搭建部署Pipeline(一)

这篇文章是一系列文章的第一篇,在这一系列文章中,我们想要分享我们如何使用Docker、Docker-Compose和Rancher完成容器部署工作流的故事。我们想带你从头开始走过pipeline的革命历程,重点指出我们这一路上遇到的痛点和做出的决定,而不只是单纯的回顾。

幸好有很多优秀的资源可以帮助你使用Docker设置持续集成和部署工作流。这篇文章并不属于这些资源之一。一个简单的部署工作流相对比较容易设置。但是我们的经验表明,构建一个部署系统的复杂性主要在于原本容易的部分需要在拥有很多依赖的遗留环境中完成,以及当你的开发团队和运营组织发生变化以支持新的过程的时候。希望我们在解决构建我们的pipeline的困难时积累下的经验会帮助你解决你在构建你的pipeline时遇到的困难。

在这第一篇文章里,我们将从头开始,看一看只用Docker时我们开发的初步的工作流。在接下来的文章中,我们将进一步介绍Docker-compose,最后介绍如何将Rancher应用到我们的工作流中。

为了为之后的工作铺平道路,假设接下来的事件都发生在一家SaaS提供商那里,我们曾经在SaaS提供商那里提供过长时间服务。仅为了这篇文章的撰写,我们姑且称这家SaaS提供商为Acme Business Company, Inc,即ABC。这项工程开始时,ABC正处在将大部分基于Java的微服务栈从裸机服务器上的本地部署迁移到运行在AWS上的Docker部署的最初阶段。这项工程的目标很常见:发布新功能时更少的前置时间(lead time)以及更可靠的部署服务。

为了达到该目标,软件的部署计划大致是这样的:

未分类

这个过程从代码的变更、提交、推送到git仓库开始。当代码推送到git仓库后,我们的CI系统会被告知运行单元测试。如果测试通过,就会编译代码并将结果作为产出物(artifact)存储起来。如果上一步成功了,就会触发下一步的工作,利用我们的代码产出物创建一个Docker镜像并将镜像推送到一个Docker私有注册表(private Docker registry)中。最后,我们将我们的新镜像部署到一个环境中。

要完成这个过程,如下几点是必须要有的:

  • 一个源代码仓库。ABC已经将他们的代码存放在GitHub私有仓库上了。
  • 一个持续集成和部署的工具。ABC已经在本地安装了Jenkins。
  • 一个私有registry。我们部署了一个Docker registry容器,由Amazon S3支持。
  • 一个主机运行Docker的环境。ABC拥有几个目标环境,每个目标环境都包含过渡性(staging)部署和生产部署。

这样去看的话,这个过程表面上简单,然而实际过程中会复杂一些。像许多其它公司一样,ABC曾经(现在仍然是)将开发团队和运营团队划分为不同的组织。当代码准备好部署时,会创建一个包含应用程序和目标环境详细信息的任务单(ticket)。这个任务单会被分配到运营团队,并将会在几周的部署窗口内执行。现在,我们已经不能清晰地看到一个持续部署和分发的方法了。

最开始,部署任务单可能看起来是这样的:

DEPLOY-111:
App: JavaService1, branch "release/1.0.1"
Environment: Production

部署过程是:

  • 部署工程师用了一周时间在Jenkins上工作,对相关的工程执行”Build Now“,将分支名作为参数传递。之后弹出了一个被标记的Docker镜像。这个镜像被自动的推送到了注册表中。工程师选择了环境中的一台当前没有在负载均衡器中被激活的Docker主机。工程师登陆到这台主机并从注册表中获取新的版本。
docker pull registry.abc.net/javaservice1:release-1.0.1
  • 找到现存的容器。
docker ps
  • 终止现存容器运行。
docker stop [container_id]

开启一个新容器,这个容器必须拥有所有正确启动容器所需的标志。这些标志可以从之前运行的容器那里,主机上的shell历史,或者其它地方的文档借鉴。

docker run -d -p 8080:8080 … registry.abc.net/javaservice1:release-1.0.1
  • 连接这个服务并做一些手工测试确定服务正常工作。
curl localhost:8080/api/v1/version
  • 在生产维护窗口中,更新负载均衡器使其指向更新过的主机。

  • 一旦通过验证,这个更新会被应用到环境中所有其它主机上,以防将来需要故障切换(failover)。

不可否认的是,这个部署过程并不怎么让人印象深刻,但这是通往持续部署伟大的第一步。这里有好多地方仍可改进,但我们先考虑一下这么做的优点:

  • 运营工程师有一套部署的方案,并且每个应用的部署都使用相同的步骤。在Docker运行那一步中需要为每个服务查找参数,但是大体步骤总是相同的:Docker pull、Docker stop、Docker run。这个过程非常简单,而且很难忘掉其中一步。

  • 当环境中最少有两台主机时,我们便拥有了一个可管理的蓝绿部署(blue-green deployment)。一个生产窗口只是简单地从负载均衡器配置转换过来。这个生产窗口拥有明显且快速的回滚方法。当部署变得更加动态时,升级、回滚以及发现后端服务器变得愈发困难,需要更多地协调工作。因为部署是手动的,蓝绿部署代价是最小的,并且同样能提供优于就地升级的主要优点。

好吧,现在看一看痛点:

  • 重复输入相同的命令。或者更准确地说,重复地在bash命令行里敲击输入。解决这一点很简单:使用自动化技术!有很多工具可以帮助你启动Docker容器。对于运营工程师,最明显的解决方案是将重复的逻辑包装成bash脚本,这样只需一条命令就可以执行相应逻辑。如果你将自己称作一个开发-运营(devops)工程师,你可能会去使用Ansible、Puppet、Chef或者SaltStack。编写脚本或剧本(playbooks)很简单,但是这里仍有几个问题需要说明:部署逻辑到底放在那里?你怎样追踪每个服务的不同参数?这些问题将带领我们进入下一点。

  • 即便一个运营工程师拥有超能力,在办公室工作一整天后的深夜里仍能避免拼写错误,并且清晰的思考,他也不会知道有一个服务正在监听一个不同的端口并且需要改变Docker端口参数。问题的症结在于开发者确实了解应用运行的详细信息(但愿如此),但是这些信息需要被传递给运营团队。很多时候,运营逻辑放在另外的代码仓库中或这根本没有代码仓库。这种情况下保持应用相关部署逻辑的同步会变得困难。由于这个原因,一个很好的做法是将你的部署逻辑只提交到包含你的Dockerfile的代码仓库。如果在一些情况下无法做到这点,有一些方法可以使这么做可行(更多细节将在稍后谈到)。把细节信息提交到某处是重要的。代码要比部署任务单好,虽然在一些人的脑海中始终认为部署任务单更好。

  • 可见性。对一个容器进行一个故障检测须要登陆主机并且运行相应命令。在现实中,这就意味着登陆许多主机然后运行“docker ps”和“docker logs –tail=100”的命令组合。有很多解决方案可以做到集中登陆。如果你有时间的话,还是相当值得设置成集中登陆的。我们发现,通常情况下我们缺少的能力是查看哪些容器运行在那些主机上的。这对于开发者而言是个问题。开发者想要知道什么版本被部署在怎样的范围内。对于运营人员来说,这也是个主要问题。他们须要捕获到要进行升级或故障检测的容器。

基于以上的情况,我们开始做出一些改变,解决这些痛点。

第一个改进是写一个bash脚本将部署中相同的步骤包装起来。一个简单的包装脚本可以是这样的:

!/bin/bash
APPLICATION=$1
VERSION=$2

docker pull "registry.abc.net/${APPLICATION}:${VERSION}"
docker rm -f $APPLICATION
docker run -d --name "${APPLICATION}" "registry.abc.net/${APPLICATION}:${VERSION}"

这样做行得通,但仅对于最简单的容器而言,也就是那种用户不需要连接到的容器。为了能够实现主机端口映射和卷挂载(volume mounts),我们须要增加应用程序特定的逻辑。这里给出一个使用蛮力实现的方法:

APPLICATION=$1
VERSION=$2

case "$APPLICATION" in
java-service-1)
 EXTRA_ARGS="-p 8080:8080";;
java-service-2)
 EXTRA_ARGS="-p 8888:8888 --privileged";;
*)
 EXTRA_ARGS="";;
esac

docker pull "registry.abc.net/${APPLICATION}:${VERSION}"
docker stop $APPLICATION
docker run -d --name "${APPLICATION}" $EXTRA_ARGS "registry.abc.net/${APPLICATION}:${VERSION}"

现在这段脚本被安装在了每一台Docker主机上以帮助部署。运营工程师会登陆到主机并传递必要的参数,之后脚本会完成剩下的工作。部署时的工作被简化了,工程师的需要做的事情变少了。然而将部署代码化的问题仍然存在。我们回到过去,把它变成一个关于向一个共同脚本提交改变并且将这些改变分发到主机上的问题。通常来说,这样做很值得。将代码提交到仓库会给诸如代码审查、测试、改变历史以及可重复性带来巨大的好处。在关键时刻,你要考虑的事情越少越好。

理想状况下,一个应用的相关部署细节和应用本身应当存在于同一个源代码仓库中。有很多原因导致现实情况不是这样,最突出的原因是开发人员可能会反对将运营相关的东西放入他们的代码仓库中。尤其对于一个用于部署的bash脚本,这种情况更可能发生,当然Dockerfile文件本身也经常如此。

这变成了一个文化问题并且只要有可能的话就值得被解决。尽管为你的部署代码维持两个分开的仓库确实是可行的,但是你将不得不耗费额外的精力保持两个仓库的同步。本篇文章当然会努力达到更好的效果,即便实现起来更困难。在ABC,Dockerfiles最开始在一个专门的仓库中,每个工程都对应一个文件夹,部署脚本存在于它自己的仓库中。

未分类

Dockerfiles仓库拥有一个工作副本,保存在Jenkins主机上一个熟知的地址中(就比如是‘/opt/abc/Dockerfiles’)。为了为一个应用创建Docker镜像,Jenkins会搜索Dockerfile的路径,在运行”docker build“前将Dockerfile和伴随的脚本复制进来。由于Dockerfile总是在掌控中,你便可能发现你是否处在Dockerfile超前(或落后)应用配置的状态,虽然实际中大部分时候都会处在正常状态。这是来自Jenkins构建逻辑的一段摘录:

if [ -f docker/Dockerfile ]; then
 docker_dir=Docker
elif [ -f /opt/abc/dockerfiles/$APPLICATION/Dockerfile ]; then
 docker_dir=/opt/abc/dockerfiles/$APPLICATION
else
 echo "No docker files. Can’t continue!"
 exit 1
if
docker build -t $APPLICATION:$VERSION $docker_dir

随着时间的推移,Dockerfiles以及支持脚本会被迁移到应用程序的源码仓库中。由于Jenkins最开始已经查看了本地的仓库,pipeline的构建不再需要任何变化。在迁移了第一个服务后,仓库的结构大致是这样的:

未分类

我们使用分离的仓库时遇到的一个问题是,如果应用源码或打包逻辑任意一个发生改变,Jenkins就会触发应用的重建。由于Dockerfiles仓库包含了许多项目的代码,当改变发生时我们不想触发所有的仓库重建。解决方法是:使用在Jenkins Git插件中一个很隐蔽的选项,叫做Included Regions。当配置完成后,Jenkins将一个变化引起的重建隔离在仓库的某个特定子集里面。这允许我们将所有的Dockerfiles放在一个仓库里,并且仍然能做到当一个改变发生时只会触发特定的构建(与当改变发生在仓库里特定的目录时构建所有的镜像相比)。

未分类

关于这个初步的工作流的另一个方面是部署工程师必须在部署前强制构建一个应用镜像。这将导致额外的延迟,尤其是构建存在问题并且开发人员需要参与其中的时候。为了减少这种延迟,并为更加持续的部署铺平道路,我们开始为熟知分支中的每一个提交构建Docker镜像。这要求每一个镜像有一个独一无二的版本标识符,而如果我们仅仅依赖官方的应用版本字符串往往不能满足这一点。最终,我们使用官方版本字符串、提交次数和提交sha码的组合作为版本标识符。

commit_count=$(git rev-list --count HEAD)
commit_short=$(git rev-parse --short HEAD)
version_string="${version}-${commit_count}-${commit_short}"

这样得到的版本字符串看起来是这样的:1.0.1-22-7e56158

在结束pipeline的Docker file部分的讨论之前,还有一些参数值得提及。如果我们不会在生产中操作大量的容器,我们很少用到这些参数。但是,它们被证明有助于我们维护Docker集群的线上运行。

  • 重启策略(Restart Policy)-一个重启策略允许你指定当一个容器退出时,每个容器采取什么动作。尽管这个可以被用作应用错误(application panic)时的恢复或当依赖上线时保持容器再次尝试连接,但对运营人员来说真正的好处是在Docker守护进程(daemon)或者主机重启后的自动恢复。从长远来看,你将希望实现一个适当的调度程序(scheduler),它能够在新主机上重启失败的容器。在那天到来之前,节省一些工作,设置一个重启策略吧。在现阶段的ABC中,我们将这项参数默认为“–restart always”,这将会使容器始终重启。简单地拥有一个重启策略就会使计划的(和非计划的)主机重启变得轻松得多。

  • 资源约束(Resource Constraints)-使用运行时的资源约束,你可以设置容器允许消耗的最大内存和CPU。它不会把你从一般的主机过载(over-subscription)中拯救出来,但是它可以抑制住内存泄漏和失控的容器。我们先对容器应用一个充足的内存限制(例如:–memory=”8g”) 。我们知道当内存增长时这样会产生问题。尽管拥有一个硬性限制意味着应用最终会达到内存不足(Out-of-Memory)的状态并产生错误(panic),但是主机和其它容器会保持正确运行。

结合重启策略和资源约束会给你的集群带来更好的稳定性,与此同时最小化失败的影响,缩短恢复的时间。这种类型的安全防护可以让你和开发人员一起专注于“起火”的根本原因,而不是忙于应付不断扩大的火势。

简而言之,我们从一个基础的构建pipeline,即从我们的源码仓库中创建被标记的Docker镜像开始。从使用Docker CLI部署容器一路到使用脚本和代码中定义的参数部署容器。我们也涉及了如何管理我们的部署代码,并且强调了几个帮助运营人员保持服务上线和运行的Docker参数。

此时此刻,在我们的构建pipeline和部署步骤之间仍然存在空缺。部署工程师会通过登入一个服务器并运行部署脚本的方法填补这个空缺。尽管较我们刚开始时有所改进,但仍然有进一步提高自动化水平的空间。所有的部署逻辑都集中在单一的脚本内,当开发者需要安装脚本以及应付它的复杂性时,会使本地测试会变得困难得多。此时此刻,我们的部署脚本也包含了通过环境变量处理任何环境特定信息的方法。追踪一个服务设置的环境变量以及增加新的环境变量是乏味且容易出错的。

在下一篇文章中,我们将看一看怎样通过解构(deconstructing)共同的包装脚本解决这些痛点,并使部署逻辑向使用Docker Compose的应用更近一步。

您也可以下载免费的电子书《Continuous Integration and Deployment with Docker and Rancher》(http://info.rancher.com/cicd-with-docker-ebook),这本书讲解了如何利用容器帮助你完成整个CI/CD过程。