Docker自动化开发环境
在后端语言是php的项目或者公司中,如果希望使用docker开发环境,那么如果直接使用传统docker的build镜像或者dockerfile那一套流程,对于开发者不太友好,而且过程会较复杂 因此,开发者们希望有一个类似vagrant的平台,他们没有太多特殊的配置,只是希望有一个和线上相同版本和配置的php的开发环境,特别是新人入职时,可以很快就生成一套开发环境
理论部分
- 核心想法是将docker作为虚拟机使用,使用一台宿主机,在其安装Docker, 为每人启动一个容器,容器内部运行了完整的开发需要的进程,比如多个php版本,redis,nginx等应用
 - 公司内使用的DNS是自建DNS服务
 - 开发者电脑开启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
 
- 在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命令,直接退出宿主机
 
结语
至此,自动化开发环境搭建完成


...