IFTTT在开发环境中使用Docker的经验
jopen
9年前
IFTTT是“if this then that”的缩写,事实上是让你的网络行为能够引发连锁反应、让你使用更为方便,其宗旨是“Put the internet to work for you”(让互联网为你服务)。Docker在IFTTT中也在开发实践,以下是Nicholas Silva的一些介绍。
IFTTT是一款新兴的互联网工具型应用,正如他们给自己的介绍“If This Then That”,让用户可以根据他们设计的流程设计一些小程序,例如你可以让它帮忙监视女朋友的推ter,当Tweet内容中出现“出差”这个词的时 候,自动在Google Calendar里面添加一个晚上的聚会晚餐,并且在非死book发表一条消息开始呼朋唤友。
IFTTT目前正处于从基础架构向容器集装体系结构转移的过程中。我们有一大批微服务和容器将会按照这个架构进行管理。改造我们的生产环境架构之前,我们决定首先从本地开发环境开始实践。这样我们就可以在冒险上生产环境之前发现应用程序的的一些问题。
另外,本地开发环境已经偏离了我们现存的生产环境。我们使用Chef(一个系统集成框架,为整个架构提供配置管理功能)和Vagrant(一款用 来构建虚拟开发环境的工具)提供管理本地虚拟机。它虽然一直在工作,但是我们知道它工作不了太久。我们需要采取行动,而不是浪费时间把测试环境同步到一个 即将弃用的生产环境中。于是我们决定直接完全跃过现存系统,直接开发我们想要的。
下面将要介绍的是我们的工程师如何在开发环境中运行使用Docker的。
目前IFTTT的工程师都使用苹果电脑开发,所以这里的系统都是Mac OS,由于不考虑跨平台所以不会太复杂。
我们在Dash项目中开源了所有收集的代码。这些代码相当难以理解,如果你盲目的运行必定会浪费很多时间。运行之前我们先来看下它都是做什么的。
Part1:启动项目
我们使用Homebrew和Ansible运行curlbash自动化操作整个过程:bash <(curl -fsSL https://raw.githubusercontent.com/IFTTT/dash/master/bin/bootstrap)
引导脚本安装Homebrew和Ansible成功后下载Dash的代码库,使用Ansible安装并配置VirtualBox、Docker、 Docker Machine、Docker Compose以及开发环境的DNS,然后编译Docker Machine VM。
Ansible通常被用来管理远程服务器,但也可以用来配置本地机器。通过参数目录中的IP host参数127.0.0.1,你可以使用Ansible palybook执行一个本地任务。
ansible-playbook /usr/local/dev-env/ansible/mac.yml -i 127.0.0.1, --ask-become-pass
IP地址之后必须有一个逗号,因为它使参数显示为列表,而不是一个文件的名称。
Ansible playbook只是一个YAML文件,它列举了一些正在运行的任务和状态。我不会深入讲这个文件,但是如果你有兴趣可以看整个文件。
通过Ansible,我们安装了提供Homebrew不包含二进制文件Caskroom,以及大量的packages和配置文件。
包括:
- VirtualBox
- Docker
- Docker Machine
- Docker Compose
- DNS .dev resolution to the VM
- NFS exports
- Shell Environment
这里比较有意思的是DNS解决方案。在/etc/resolver/dev创建文件:
nameserver 192.168.99.100
所有的.dev请求被路由到Docker Machine中。运行一个简单的dnsmasq容器,路由所有的.dev请求回到VM,另一个nginx代理容器路由请求到合适的容器内(待续)。不要 去修改/etc/hosts文件!来自虚拟机和你本机系统的Host类似ifttt.dev的域名时,请求可以被路由到合适的服务器。
Part2:创建Docker Machine
在dev命令中,我已经把几个复杂的命令通过别名的方式整合为一个简单文件命令。例如创建一个dev machine我们使用:docker-machine create \ --driver virtualbox docker-machine scp \ /usr/local/dev-env/docker/bootsync.sh \ dev:/tmp/bootsync.sh docker-machine ssh dev \ "sudo mv /tmp/bootsync.sh /var/lib/boot2docker/bootsync.sh" docker-machine restart dev
这条命令意思显而易见。使用NFS和dev DNS,我们要拷贝下面这个脚本到VM中,然后重启VM。
这个脚本并不是很复杂:
#!/bin/sh sudo umount /Users sudo /usr/local/etc/init.d/nfs-client start sleep 1 sudo mount.nfs 192.168.99.1:/Users /Users -v -o \ rw,async,noatime,rsize=32768,wsize=32768,proto=udp,udp,nfsvers=3 grep '\-\-dns' /var/lib/boot2docker/profile || { echo 'EXTRA_ARGS="$EXTRA_ARGS --dns 192.168.99.100 \ --dns 8.8.8.8 --dns 8.8.4.4"' | sudo tee -a \ /var/lib/boot2docker/profile } echo -e "nameserver 8.8.8.8\nnameserver 8.8.4.4" \ | sudo tee /etc/resolv.conf
首先卸载掉标准的vboxfs并开启NFS客户端,然后挂载本机创建的共享目录。Docker Machine的引导程序会在这个文件目录存在的情况下被同步执行。
我也是尝试使用多种方式运行挂载命令,却很少成功。最终成功搞定后,我就成为NFS专家了。
首先去dnsmasq容器设置DNS文件其他剩余的部分,第二步是设置DNS服务器为8.8.X.X。这个同时关注dev域和访问外网的请求。如 果不添加8.8.X.XDNS服务器,当你的Docker Machine的网络发生变化,你的DNS服务器缓存将自动结束,而且不得不通过重启机器才能转换网络。
这时候你应该能连接到VM中正在运行的Docker进程。你可以通过正在运行的Docker Machine dev环境看到是如何连接的。也可以通过Docker ps 测试是都有正在运行安装的Docker,可以看到:
CONTAINER ID IMAGE COMMAND CREATED
看到这个显示说明你已经正确安装配置环境。
Part3:开始在容器中开发
在容器中开发需要一些思维上的转换,很多时候与我们过去写代码、运行代码、测试等不太一样。你有一个或者多个本地数据库,或者可能模拟S3和DynamoDB的依赖。在使用容器开发之前的本地开发,你可能直接就可以关闭本机的OS或者虚拟机,或者你可以安装一切你所需要的软件,随着时间的发展,你可以不断增加 对系统和程序的配置。也可能是一个Snowflake Server。可能在管理依赖的开始会存在一些问题,因此像Bundler、pip、virtualenv和RVM他们可以统一混合使用帮助解决问题。尽 管你可以测试出这是一个新版的MYSQL,但是真正做起来就不是那么简单了。
在容器的范畴中,你没必要拥有一个持久并不断完善的来发环境。你可以做你想做的,但是这并不是推荐的工作流方式。传统上拥有一个能在上面运行代码的“VM”,被取而代之的是创建一个更轻量级可视化的叫做“容器”的层。( 了解更多,从Docker获取更多说明)
这些容器都是从镜像创建的。镜像的本质是一些基础的只读模板,通过模板可以创建你的应用程序开发环境,如果移动容器,容器内所有相关的东西都将被 移动。你也可以总是通过重启相同镜像中的容器,当然这也是经常做的。这也就意味着你可以拥有不同的容器做不同的配置,而且完全不用担心管理混乱。你可以一 个上面使用的是Ruby2的代码,另一个是Ruby1.9的代码。创建容器(基于以前使用的的Ruby镜像)仅仅使用一个新的gem即可。当你安装gem 处理容器时,处于其他原因,Rails2需要依赖安装其他很多gem,你不得不考虑把gem安装在你的系统中。
为了获得更多“官方”的应用程序依赖(node、mysql-client等),当运行一个存在所有应用程序的容器时,你可以用Dockerfile检测你的应用程序库去创建一个已经包含这些程序的镜像。
存在很多依赖的情况下,我们通常分离进程,分割到额外的容器中。例如我们其中一种容器,同时依赖Mysql、redis和S3。我们就把他们同时组合进入我们的容器。通过项目根目录中的YAML文件整合进入我们的容器中。例如:
web: build: . dockerfile: Dockerfile.development volumes: - .:/app links: - redis:redis.myapp.dev - s3:s3.myapp.dev - mysql:mysql.myapp.dev command: puma -C config/puma.development.rb mysql: image: mysql/mysql-server:5.6 volumes: - /mnt/sda1/var/lib/mysql:/var/lib/mysql redis: image: redis:2.6 s3: image: bbcnews/fake-s3
通过此设置,很容易就可以看到依赖关联关系。我们已经从这个目录和Dockerfile .development文件构建web services。把这个文件作为容器的存储部分挂上去,给这个目录的代码一定的执行权限就可以运行代码。被分离的Dockerfile是最主要的,因为 在生产中我们根据此文件编译所有的代码以快照的形式进入容器,所以在开发环境如果我们想更改它,就必须重新编译一次才能实现。我们覆盖了已经在 Dockerfile中被定义的命令,因此我们能加载开发环境的配置。
当我们开始运行web的容器时,Docker生成会意识到他关联了redis、mysql、s3等容器,因此会自动启动他们。除此,Docker 还将dev域名和正确的容器写入/etc/hosts文件。由于这样操作的话其他的人不需要什么配置,这样就容易很多了。我们可以使用特定版本的 redis和mysql。Mysq装载在虚拟机的一个目录这样使数据可以在扩容器中做到持久化保存。
每一个项目都有一个自定义的application-level配置。这个配置文件需要运行在Dockerfile,其他的服务运行在docker-compose.yum文件。例如下面开始一个新项目,在Dash中使用dev命令:
git clone [GIT_URL] dev up# (simply an alias to docker-compose up -d)
Part 4: Web浏览器&服务间通信
你可能会想,怎么才能看到编译的结果?这又不是本地环境。完全正确,我还没有讲到怎么请求访问到你的容器。如果你以前一直在操作系统中开发而不是通过虚拟机,你可能经常使用 http://localhost:8000访问你的APP。在Docker和Docker Machine中,现在是两个完全不一样的抽象概念。
然后在发生交换时,增加了分离的概念。容器在孤立我们每一个服务的同时,我们也可以更好的了解什么是真正的服务。
为了达到和本地环境一样的简单级别,我们还必须做点什么才行。
就像我得Part1中所说的一样,一个小的dns服务器环境,不包括dnsmasq,Dev TLD的所有的请求都被路由到Docker Machine VM。我们只需要怎么把这些请求路由到正确的容器即可。不幸的是,默认的容器网络接口并没有暴露,VM内部容器的IP也是随机分配。你可以绑定主机的端口 到容器的端口之上,但是必须仔细处理这之间的冲突。
这就是Jason Wilder的Nginx反转代理容器所讲的内容。它利用Docker内部的原理关注容器的开关,然后通过nginx反转代理实现动态配置。它也是通过绑 定了VM的80端口实现。一个新的容器都包含VIRTUAL_HOST环境变量,可以将大量的流量路由到容器中。由于能都实时运行,我们就很容易添加两行 代码进入Docker-compose.yml中:
web: ... environment: - VIRTUAL_HOST=myapp.dev # For nginx proxy
停止容器(停止dev中的web),删除容器(删除dev中的web),然后启动所有的备份(dev环境)。这是另一个集中化环境需要转换思维的例子。首先我停止服务,然后更改新的环境变量,重新开启。因为新的环境变量加入新建的容器,最好的办法是删除容器,并重新开始。
反转代理也解决了对服务开发的问题:怎么定义两个我正在开发的服务之间的依赖关系?
把两个不同的服务定义在两个不同容器的docker-compose.yml中可以让他变得更复杂。他们每次构建一个其他容器时,会循环依赖引用 另一个容器,从而进入噩梦。然而使用我们的dnsmasq容器,每一个dev的请求都被路由到nginx。只要你的服务在dev TLD而且注册在虚拟机下,那么每一个请求都能请求所有的链接。我们有自己的开发者入口,可以设置为ifttt.dev开发身份,这样就可以请求访问 ifttt.dev内部的程序。如果你访问的程序不在运行,nginx会返回503。
Part5: 使用包管理
对生产代码来说,Dockerfile一步一步安装相关联的程序包是很有意义的。对于Ruby项目来说,我们使用的是Bundler做的。我们创建镜像时可以运行bundler安装,但是你必须保证包存在情况下才可以运行成功。没必要但是整个Bundler打包的过程。然而在开发环境不同,如果我们每次都安装一个完整的Bundler添加一个自己的gem,那将变得很脆弱。更糟糕的是如果你不包含在Dockerfile内,就不得不每次都重新启动一个新的容器!幸运的是,我们有更好的办法解决了这个问题:
web: ... volumes_from: - bundler-cache ... bundler-cache: image: ruby:2.2 command: /bin/true volumes: - /usr/local/bundle
通过创建另一个以Ruby为基础的镜像服务作为我们的应用程序(使用Dockerfile定义),我们可以利用一些Docker内部更深的东西。 bundler-cache容器定义了一个使用gem安装到系统路径的存储容量,一运行即可。即使容器没有激活,我们也可以从bundler-cache 容器挂载在存储上使用。如果删除了这个web容器,我们依旧可以保持这个bundler-cache容器,等待下次重新创建容器时,直接挂载这个存贮容量 后所有的gem就存在了。如果你想清除缓存并重启,很容易在开发直接删除bundler-cache即可。
对每一个项目都使用包管理的模式,我们发现这是一个非常方便快捷安装管理的方法。不过最大的问题是如果你以外地删除bundler-cache,你就必须重新安装所有的gem。
总结
容器化与Docker在基础设施层是一个非常不错的工具。如果你计划以后转移到容器上,我非常推荐你在本地开发环境首先尝试。自从 内部开始部署Dash,我们看到新开发员工的管理时间从几天到几小时不等。我们能够在一周之内做到迁移(包括实际更改Dash本身),我们的工程师们已经 开始对此做出自己的贡献。原文链接:Developing with Docker at IFTTT (翻译 :ylzhang 审校:魏小红)
来自:http://dockone.io/article/743