Docker自动化开发环境

在后端语言是php的项目或者公司中,如果希望使用docker开发环境,那么如果直接使用传统docker的build镜像或者dockerfile那一套流程,对于开发者不太友好,而且过程会较复杂 因此,开发者们希望有一个类似vagrant的平台,他们没有太多特殊的配置,只是希望有一个和线上相同版本和配置的php的开发环境,特别是新人入职时,可以很快就生成一套开发环境

理论部分

  1. 核心想法是将docker作为虚拟机使用,使用一台宿主机,在其安装Docker, 为每人启动一个容器,容器内部运行了完整的开发需要的进程,比如多个php版本,redis,nginx等应用
  2. 公司内使用的DNS是自建DNS服务
  3. 开发者电脑开启samba共享,宿主机将此目录挂载到本地磁盘上,这个本地磁盘目录又被挂载到容器内部

架构图

为新开发者创建开发环境

只需要让开发者配置好samba共享,然后使用创建脚本即可生成自己的开发环境

请求流程

  • 在开发者自己的电脑上,按规则新建对应的项目目录,必须以*.dev.项目名为文件名(*为自己的用户名)
  • 此目录被宿主机挂载,又被宿主机挂载进容器内部
  • 访问该域名(上一步新建的目录名)(*.dev.项目名开头的域名会被收录进DNS,这步需要将域名加入DNS)
  • 这些统配域名统一被解析到Docker宿主机,请求到宿主机上的openresty
  • 由lua脚本检测到该域名前缀的用户名,然后使用脚本获取用户对应的容器IP,将请求反向代理到这个容器
  • 容器内部Nginx收到请求,使用域名通配,root目录通配,root定为到对应的目录,然后proxy_pass到php-fpm处理
  • mysql暂时没有容器化,统一访问的是开发环境的mysql

openresty中nginx的配置文件

openresty安装目录为 /usr/local/openresty/

~]# cat /usr/local/openresty/nginx/conf/nginx.conf
worker_processes  1;

events {
    worker_connections  1024;
}


http {
	client_max_body_size 100m;
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

        server {
                set_by_lua $info '
                        local s = ngx.var.host
                        local t = io.popen("echo " ..s.. " |grep -Eo ^[^.]+")
                        name = t:read("*l")
                        t:close()

                        local file = io.popen("grep " ..name.. " /usr/local/openresty/nginx/server_name_ip")
                        content = file:read("*l")
                        file:close()

                        return content
                ';

                if ($info ~ (.*)\s(.*)) {
                        set $proxy_ip $2;
                }

                listen 80;
                server_name default;

                location / {
                        proxy_redirect off;
                        proxy_set_header Host $host;
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_pass http://$proxy_ip:80;
                }

        }
    include conf.d/*.conf;

}

/usr/local/openresty/nginx/server_name_ip是为用户创建一个开发环境时就会写入一条配置,记录用户名和容器IP,内容例:

zhangsan 172.17.0.2
lisi 172.17.0.3
wangwu 172.17.0.4

容器内nginx配置

worker_processes  1;
events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '"$http_x_forwarded_for" - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" $remote_addr';

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen 80 default_server;
        root /data/dev.www/${host};
        access_log  /data/dev.www/logs/access.log main;
        error_log /data/dev.www/logs/error.log;
        index index.html index.php;

        location / {
                try_files $uri $uri/ /index.php$is_args$args;
        }

        location ~ \.php$ {
                try_files $uri =404;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                if ($host !~ (.*news\.7654\.com$|.*guangsussapi\.com$)) {
                        fastcgi_pass 127.0.0.1:9055;
                }
                if ($host ~ .*news\.bbb\.com$) {
                        fastcgi_pass 127.0.0.1:9072;
                }
                if ($host ~ .*xxx\.com$) {
                        fastcgi_pass 127.0.0.1:9072;
                }
        }
    }

}

创建开发环境的脚本

用法:Script.sh -u USERNAME -h IP_ADDR -p PASSWORD -s windows|mac create_dev.sh

#!/bin/bash

source /etc/init.d/functions

images_version='zm-dev-base:v2.7'


while getopts "u:h:p:s:" opt;do
    case $opt in
        u)
            name="${OPTARG}"
            ;;
        h)
            Client_IP="${OPTARG}"
            ;;
        p)
            PASSWORD="${OPTARG}"
            ;;
        s)
            SYSTEM="${OPTARG}"
            ;;
        *)
			echo "Usage: Script.sh -u USERNAME -h IP_ADDR -p PASSWORD -s windows|mac"
            exit 3
            ;;
    esac
done


check(){ #格式化输出结果
    if [ $? -ne 0 ];then
        action "$1" /bin/false
        exit
    else
        action "$1" /bin/true
    fi
}


useradd_user(){
	# 创建用户,对用户web目录授权
	output=`/bin/bash /app/shell/useradd_user.sh ${name}`
	if [ -z "${output}" ];then
		mkdir -pv /data/dev.www/${name} &> /dev/null
		chown -R ${name}:${name} /data/dev.www/${name}
		USER_STATUS='True'
	else
		mkdir -pv /data/dev.www/${name} &> /dev/null
		chown -R ${name}:${name} /data/dev.www/${name}
		echo > /dev/null
		check "Adduser: ${name} already exists"
	fi
}

mount_test() {
	umount /mnt &> /dev/null
    	#判断操作系统,选择对应的samba版本号
    	if [[ "${SYSTEM}" == "windows" ]];then
        	SYS_VERSION=1.0
    	elif [[ "${SYSTEM}" == "mac" ]];then
        	SYS_VERSION=3.0
    	fi	

	mount -t cifs -o uid=${name},gid=${name},username=${name},password=${PASSWORD},dir_mode=0777,file_mode=0777,vers=${SYS_VERSION} //${Client_IP}/dev.www /mnt
	check "Mount Test"
	if grep -q "${name}" /app/shell/mount_info;then
		sed -i "s/${name}.*/${name} ${Client_IP} ${PASSWORD} ${SYS_VERSION}/" /app/shell/mount_info
	else
		echo "${name} ${Client_IP} ${PASSWORD} ${SYS_VERSION}" >> /app/shell/mount_info
	fi
	umount /mnt
}

start_docker(){

	#挂载用户samba目录
	mount -t cifs -o uid=${name},gid=${name},username=${name},password=${PASSWORD},dir_mode=0777,file_mode=0777,vers=${SYS_VERSION} //${Client_IP}/dev.www /data/dev.www/${name}

	#创建目录
	mkdir -p /data/dev.www/${name}/logs

	#检测是否已启动容器
	if docker ps | grep -q "${name}";then
		echo "Info: ${name} already start"
		exit
	fi

	# 启动容器
	docker rm ${name} &> /dev/null
	docker run -it -d --hostname="web_docker" -c=512 -m 2g --device-read-bps /dev/mapper/centos-root:50mb  --name ${name} -v /etc/localtime:/etc/localtime:ro -v /data/dev.www/${name}:/data/dev.www ${images_version} /usr/sbin/init &> /dev/null

	#cp nginx.conf
	docker cp /ngx_conf/nginx.conf ${name}:/app/nginx/conf/nginx.conf

	# 启动服务
	docker exec -d ${name} /bin/bash /app/shell/start_dev.sh
	
	#chown -R ${name}:${name} /data/dev.www/${name}

	#获取容器IP
	IP=`docker exec  ${name}  ip addr | grep "inet.*eth0" |awk '{print $2}'| awk -F/ '{print $1}'`

	# 关联容器IP
	if grep -q "${name}" /usr/local/openresty/nginx/server_name_ip;then
		sed -i "/${name}/s/.*/${name} ${IP}/" /usr/local/openresty/nginx/server_name_ip
	else
		echo "${name} ${IP}" >> /usr/local/openresty/nginx/server_name_ip
	fi


	if docker ps | grep -q "${name}";then
		check "start docker ${name}"
		echo
	else
		check "start docker ${name}"
	fi
}

send_mail(){
	echo "${PASSWORD}" | passwd --stdin ${name} &> /dev/null
	if ! grep -q ${name} /etc/sudoers;then
		echo "${name} ALL=NOPASSWD: /usr/bin/docker" >> /etc/sudoers
	fi
 	if ! grep -q "sudo docker" /home/${name}/.bashrc;then
        echo "sudo docker exec -it ${name} /bin/bash ; exit" >> /home/${name}/.bashrc
    fi
	sed "s/dev_name/${name}/" /app/shell/mail_info | sed "s/password/${PASSWORD}/" | mailx -s "dev环境账户创建"  ${name}@shzhanmeng.com
	
}

if [ -z "${name}" -o -z "${Client_IP}" -o -z "${PASSWORD}" ];then
    	echo "Usage: Script.sh -u USERNAME -h IP_ADDR -p PASSWORD -s windows|mac"
	exit 1
fi
if [ "${SYSTEM}" != "windows" -a "${SYSTEM}" != "mac" ];then
    echo "Usage: Script.sh -u USERNAME -h IP_ADDR -p PASSWORD -s windows|mac"
    exit 1
fi
umount /data/dev.www/${name} &> /dev/null
useradd_user
mount_test
start_docker
if [ "${USER_STATUS}" != 'True' ];then
	send_mail
fi

注意: mac版本的samba一般是samba3.0,默认win10使用的是1.0版本, 所以在这里使用-s指定是用的哪个系统的选项

添加用户脚本useradd_user.sh,仅被创建开发环境的脚本调用

#!/bin/bash

if [ -z "$1" ];then
	echo Input Err
fi

name=$1

mail(){
	useradd $1
}

id ${name} &> /dev/null
if [ $? -eq 0 ];then
	echo "Info: ${name} already exists"
else
	mail ${name}
fi

检测用户主机是否在线脚本

若不检测用户主机是否在线,并及时卸载,则当用户主机关机时,在宿主机上不能使用df等命令 此脚本应该后台长期运行,及时剔除下线主机,并卸载该主机对应的samba目录 当该主机上线时,自动给其挂载samba目录 samba挂载需要的信息被存于 mount_info这个文件中,该文件由创建开发环境的脚本生成

mount_info

zhangsan 172.18.15.155 asda@qq 1.0
lisi 172.18.23.24 115aasyu 1.0
wangwu 172.18.22.82 estineuan 1.0

onlineCheck.sh

#!/bin/bash


output='/app/shell/check_line.txt'

while read line;do
  set -- ${line}
  Name=$1
  Ip=$2
  Password=$3
  Version=$4
  ping -c2 -w4 $Ip &> /dev/null

#判断主机是否在线;不在线-->判断是否在挂载中,如果是则卸载,无论卸载命令是否成功均追加至$(output)中
#如果主机在线,但未挂载,则将相关用户挂载,无论挂载结果如何,将挂载信息追加至$(output)
  if [ $? -ne 0 ];then
	if mount|grep -q "/data/dev.www/${Name}";then
      umount -f /data/dev.www/${Name} &> /dev/null  # -f 强制卸载,不然客户端离线时会卡住
	  if [ "$?" -eq 0 ];then
        echo -e "`date +%F-%H:%M:%S`: umount /data/dev.www/${Name} \033[32mSUCCESS\033[0m!" >> ${output} 
      else
        echo -e "`date +%F-%H:%M:%S`: umount /data/dev.www/${Name} \033[31mFAILED\033[0m!" >> ${output}
	  fi
	fi
  else
    if ! mount|grep -q "/data/dev.www/${Name}";then
      mount -t cifs -o uid=${Name},gid=${Name},username=${Name},password=${Password},dir_mode=0777,file_mode=0777,vers=${Version} //${Ip}/dev.www /data/dev.www/${Name} &> /dev/null
	  if [ "$?" -eq 0 ];then
	  	echo -e "`date +%F-%H:%M:%S`: mount /data/dev.www/${Name} \033[32mSUCCESS\033[0m!" >> ${output} 
	  else
		echo -e "`date +%F-%H:%M:%S`: mount /data/dev.www/${Name} \033[31mFAILED\033[0m!" >> ${output}
	  fi
    fi
  fi
done < /app/shell/mount_info

DNS配置

具体的域名解析配置文件

$TTL 1D
$TTL 600
@       IN       SOA    ns.dev.project1.xxx.com.      root.dev.project1.xxx.com. (
                        0       ; serial
                        1D      ; refresh
                        1H      ; retry
                        1W      ; expire
                        3H )    ; minimum
                IN              NS      ns
                IN              A       172.18.15.15
ns              IN              A       172.18.15.15
*               IN              A       172.18.15.15

这里的域使用二级域,一般情况下这里都是xxx.com一级域,但是为了匹配我们的域名规则 姓名.dev.项目.根域,所以有这个DNS配置的设计

named-zones配置

zone "dev.project1.xxx.com" IN {     #这个是正向
        type master;
        file "/var/named/dev-docker/dev.project1.xxx.com.zone";
        allow-update { none; };
};

zone "com.xxx.project1.dev.in-addr.arpa" IN {     #这个是反向
        type master;
        file "/var/named/dev-docker/dev.project1.xxx.com.zone";
        allow-update { none; };
};

将开发环境域名加入公司DNS

所有的开发环境域名 dns_domain

abcapi.xiaoluduoduo.com
abckantu.7654.com

make_dns.sh

#!/bin/bash

echo > /etc/named.dev-docker.zones
rm -rf /var/named/dev-docker/* &> /dev/null

while read line;do

rev=`echo "${line}" | awk -F. '{for(i=NF;i>=1;i--)printf $i"."}'`

cat << EOF >> /etc/named.dev-docker.zones
zone "dev.${line}" IN {     #这个是正向
        type master;
        file "/var/named/dev-docker/dev.${line}.zone";
        allow-update { none; };
};

zone "${rev}dev.in-addr.arpa" IN {     #这个是反向
        type master;
        file "/var/named/dev-docker/dev.${line}.zone";
        allow-update { none; };
};
EOF

cat << EOF >> /var/named/dev-docker/dev.${line}.zone
\$TTL 1D
\$TTL 600
@       IN       SOA    ns.dev.${line}.      root.dev.${line}. (
                        0       ; serial
                        1D      ; refresh
                        1H      ; retry
                        1W      ; expire
                        3H )    ; minimum
                IN              NS      ns
                IN              A       172.18.15.15
ns              IN              A       172.18.15.15
*               IN              A       172.18.15.15
EOF

done < /app/shell/dns_domain
service named reload

客户机samba配置

使用帮助手册

启动流程

添加新同学

  • 运行脚本 /app/shell/create_dev.sh -u USERNAME -p IPADDR -P PASSWORD -s windows|mac 会调用以下3个函数
  • useradd_user添加用户 创建用户 ${name} ,创建/data/dev.www/${name}, 修改权限 chown -R ${name}:${name} /data/dev.www/${name}

mount_test 挂载测试

  • mount -t cifs -o uid=${name},gid=${name},username=${name},password=${PASSWORD},dir_mode=0777,file_mode=0777,vers=${SYS_VERSION} //${Client_IP}/dev.www /mnt
  • 测试挂载成功后将用户名、IP、密码、系统类型写入/app/shell/mount_info文件中

start_docker

  • 挂载用户samba目录
  • 创建log目录 /data/dev.www/${name}/logs
  • 检测该用户容器是否已启动
  • 删除之前未删除的僵尸容器
  • 以systemd启动容器,限制CPU,内存,磁盘使用率
  • 挂载/data/dev.www/${name}:/data/dev.www目录
  • 复制nginx.conf配置文件
  • 启动容器内各服务组件 nginx php5 php7 php7.2 redis
  • 获取容器IP,将用户名和IP记录到/usr/local/openresty/nginx/server_name_ip

mail

  • 在docker宿主机为该用户设置密码,密码为samba挂载的密码
  • 添加visudo 可使用 sudo docker
  • 将sudo docker exec -it ${name} /bin/bash ; exit 写入该用户 ~/.bashrc以限制该用户登录宿主机时直接登录到自己的容器内
  • 给用户发送邮件,登录名、主机、密码、端口等信息

服务

  • 宿主机
  • 宿主机启动组件
  • mysql-5.6
  • 172.17.0.1:3306

nginx代理

  • 由域名前缀反代到对应容器,使用ngx_lua

目录挂载映射

  • 宿主机 ==> 容器内

  • /data/dev.www/${name}

  • /data/dev.www

  • 容器启动组件

  • nginx1.9.7 ==> 0.0.0.0:80

  • php5.5 ==> 127.0.0.1:9055

  • php7.0 ==> 127.0.0.1:9070

  • redis3.2 ==> 127.0.0.1:6379

  • php7.2 ==> 127.0.0.1:9072

扩展

如何登录进自己的容器 在创建开发环境脚本中,有如下命令

if ! grep -q "sudo docker" /home/${name}/.bashrc;then
  echo "sudo docker exec -it ${name} /bin/bash ; exit" >> /home/${name}/.bashrc
fi
  • 用户使用 用户名@宿主机IP 登录宿主机,在执行用户.bashrc时会执行sudo docker exec -it ${name} /bin/bash ; exit
  • 登录时执行docker exec就登录进了自己的容器
  • 退出容器后,接着运行exit命令,直接退出宿主机

结语

至此,自动化开发环境搭建完成