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命令,直接退出宿主机
结语
至此,自动化开发环境搭建完成
...