如何讓 Ansible 和 Docker 愉快地在一起
Docker 之所以如此流行,是因為它創(chuàng)造了一種采用方便的命令行接口 (CLI) 和 HTTP API 工具來封裝、運行和維護容器的獨特方式。這種簡化降低了此技術的入門門檻,使得將應用程序及其運行時環(huán)境封裝到一個簡單 Dockerfile 中的獨立鏡像中變得可行。Docker 使您能夠開發(fā)更復雜的項目,但您仍需要配置這些容器。在本文中,我將展示 Ansible 如何通過更清晰的語法帶來配置管理器的特性。您將學習如何僅使用已安裝的 Python 和 Docker 構建任何堆棧。
在介紹 Ansible 的細節(jié)之前,我首先將介紹 Ansible 的 分析 中提到的一些要點:
- 盡管容器導致一些新工作流出現,但編排和配置工具仍然非?;钴S。
- Ansible 和 Salt 等新參與者正在挑戰(zhàn)現有的工具,比如 Chef 和 Puppet。
- 許多與 Docker 有關聯的開發(fā)人員也很關心這些工具。
更確切地講,借助 Docker,您可以在幾秒內實現完全隔離的堆棧環(huán)境,或者在服務器之間復制準確的設置。但是,Docker 不包含提供端到端體驗的可靠工具,無論是針對開發(fā)還是生產。Docker 團隊通過新的集群工具解決了這些不斷演變的挑戰(zhàn),嘗試將 Docker 轉變?yōu)橐粋€大規(guī)模運行容器的可靠解決方案。然而,Docker 仍然需要您手動硬編碼任務和重復常見的設置。所以,針對容器的編排和配置管理的關鍵 Docker 流程仍有待解決。在本文中,您將學習如何結合使用 Ansible 和 Docker 來幫助解決這些問題。
DevOps 的興起
在部署到生產中之前,現代應用程序通常涉及到一個復雜的部署管道。最佳實踐建議在每次小型迭代后盡早地、頻繁地發(fā)布代碼。任務的手動執(zhí)行無法擴展,組織已開始完善介于開發(fā)人員與系統(tǒng)管理員之間的流程,所以 DevOps 就誕生了。從那時起,敏捷團隊就開始嘗試強化和自動化測試代碼,以及將其交付給用戶的方式。
通過實現最新的技術和方法,公司對其服務器上的代碼建立了信心。然而,隨著應用程序在規(guī)模和復雜性上不斷增長,開發(fā)人員和系統(tǒng)管理員繼續(xù)面臨著無數挑戰(zhàn)。現在比以往更需要為產品提供受支持的社區(qū)驅動工具。
Ansible 的可擴展設計
在此環(huán)境中,Ansible 提供了一個有趣的框架來管理基礎架構。您可以獲得服務器定義的控制權,比如要安裝的包或要復制的文件,并將該配置擴展到數千個服務器。Ansible playbook 構成了集群的目標狀態(tài)的一種安全表示。它的 YAML 語法和龐大的模塊列表生成了任何開發(fā)人員都能快速理解的易讀性配置文件。不同于 Chef 或 Puppet,Ansible 是無代理的,這意味著您要在遠程主機上運行命令,只需一個 SSH 連接即可??梢钥吹剑珹nsible 可輕松地處理 DevOps 復雜性。
但是,Ansible 是在容器快速興起和它們在云開發(fā)環(huán)境中帶來革命之前設計的。那么 Ansible 是否仍然有用?微型服務的范式和復雜開發(fā)環(huán)境引入了新的需求:
- 輕量型鏡像。為了容易傳輸或節(jié)省成本,鏡像被剝離到僅剩下最低限度的依賴項。
- 單一用途,單一流程。如果應用程序不是非常需要 SSH 守護進程,則無需運行它。
- 短暫性。容器隨時可能死亡、轉移和復活。
在此上下文中,Ansible 的可擴展架構解決了這些問題。一個 Docker 模塊在較高層面上管理主機和容器。盡管您可能會爭論哪個編排工具(來自 Google 的 Kubernetes,還是來自 New Relic 的 Centurion)最適合此環(huán)境,但 Docker 模塊執(zhí)行效率很高,這正是我在本文中使用它的原因。但是,您還可以構建從其官方 Ansible 鏡像啟動的容器,然后在本地模式下從內部運行 playbook。盡管此方法非常適合 Packer,而且肯定也適合許多使用情況,但它的缺點通常極為關鍵。
您被鎖定在一個基礎鏡像中,無法再利用特殊的秘訣或其他堆棧。
最終的工件已安裝 Ansible 和它的依賴項,它們與實際應用程序毫無關系,這讓工件變得更笨重。
盡管 Ansible 可管理數千個服務器,但它只配備(Provision)了一個容器。
此方法將容器視為小型 VM,您可以在其中使用一個特定的解決方案。幸運的是,Ansible 擁有模塊化設計。模塊分散在不同的存儲庫中,而且 Ansible 的大部分功能都可以通過插件進行擴展。
在下一節(jié)中,您將設置一個有效的環(huán)境,針對您的需求來調整 Ansible。
設置一個 Ansible 環(huán)境
假設您想要一個很容易部署的工具,它在輕量型容器中配置應用程序環(huán)境。由于與這些容器分離,您需要一個安裝了 Ansible 的客戶端,您將使用它來向 Docker 守護進程發(fā)送命令。此設置如 圖 1 中所示。
圖 1. 使用 Ansible 配備容器所需的組件
您在此配置中必須管理的依賴項,已通過從容器中運行 Ansible 來最小化。此架構將主機限制為容器與命令之間的一個通信橋梁。
可通過許多選項在您服務器上安裝 Docker:
使用 docker-machine 將它安裝在遠程主機上。
安裝在本地。順便說一下,您可能不想親自管理一個嚴格基于容器的基礎架構;在這種情況下,可以考慮采用外部提供程序。
依賴于外部提供程序。
使用 boot2docker,這是一個在 Windows 和 Mac 上運行 Docker 容器的輕量型 Linux 發(fā)行版。
無論選擇何種解決方案,請確保部署了 Docker 1.3 版或更高版本(1.3 版引入了進程注入)。您還需要運行一個 SSH 服務器來安全地處理 Ansible 命令。
清單 1 中的命令使用公鑰設置了一種方便可靠的身份驗證方法。
清單 1. 使用公鑰設置身份驗證的命令
- # install dependencies
- sudo apt-get install -y openssh-server libssl-dev
- # generate private and public keys
- ssh-keygen -t rsa -f ansible_id_rsa
- # allow future client with this public key to connect to this server
- cat ansible_id_rsa.pub >> ~/.ssh/authorized_keys
- # setup proper permissions
- chmod 0700 ~/.ssh/
- chmod 0600 ~/.ssh/authorized_keys
- # make sure the daemon is running
- sudo service ssh restart
配置 SSH 和安全性問題不屬于本文的討論范圍。細心的讀者可查閱 /etc/ssh/sshd_config 文件,進一步了解配置 SSH 的可用選項。
下一步是將公鑰加載到運行 Ansible 的客戶端容器上并配備構建器容器。使用一個 Dockerfile 來配備構建器。參見 清單 2。
清單 2. 配備構建器的 Dockerfile
- FROM python:2.7
- # Install Ansible from source (master)
- RUN apt-get -y update && \
- apt-get install -y python-httplib2 python-keyczar python-setuptools python-pkg-resources
- git python-pip && \
- apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
- RUN pip install paramiko jinja2 PyYAML setuptools pycrypto>=2.6 six \
- requests docker-py # docker inventory plugin
- RUN git clone http://github.com/ansible/ansible.git /opt/ansible && \
- cd /opt/ansible && \
- git reset --hard fbec8bfb90df1d2e8a0a4df7ac1d9879ca8f4dde && \
- git submodule update --init
- ENV PATH /opt/ansible/bin:$PATH
- ENV PYTHONPATH $PYTHONPATH:/opt/ansible/lib
- ENV ANSIBLE_LIBRARY /opt/ansible/library
- # setup ssh
- RUN mkdir /root/.ssh
- ADD ansible_id_rsa /root/.ssh/id_rsa
- ADD ansible_id_rsa.pub /root/.ssh/id_rsa.pub
- # extend Ansible
- # use an inventory directory for multiple inventories support
- RUN mkdir -p /etc/ansible/inventory && \
- cp /opt/ansible/plugins/inventory/docker.py /etc/ansible/inventory/
- ADD ansible.cfg /etc/ansible/ansible.cfg
- ADD hosts /etc/ansible/inventory/hosts
這些指令改編自官方構建版本,自動化了一次來自 Ansible 主分支上的提交 fbec8bfb90df1d2e8a0a4df7ac1d9879ca8f4dde 有效安裝。
主機和 ansible.cfg 配置文件(參見 清單 3 和 清單 4)已封裝。通過使用容器,可以確保您將共享同一個環(huán)境。在本示例中,Dockerfile 安裝了 Python 2.7.10 版和 Ansible 2.0.0。
清單 3. 主機配置文件
- # hosts
- # this file is an inventory that Ansible is using to address remote servers.
- Make sure to replace the information with your specific setup and variables
- that you don't want to provide for every command.
- [docker]
- # host properties where docker daemon is running
- 192.168.0.12 ansible_ssh_user=xavier
清單 4. Ansible 配置文件
- # ansible.cfg
- [defaults]
- # use the path created from the Dockerfile
- inventory = /etc/ansible/inventory
- # not really secure but convenient in non-interactive environment
- host_key_checking = False
- # free you from typing `--private-key` parameter
- priva_key_file = ~/.sh/id_rsa
- # tell Ansible where are the plugins to load
- callback_plugins = /opt/ansible-plugins/callbacks
- connection_plugins = /opt/ansible-plugins/connections
#p#
在構建 Ansible 容器之前,您必須導出 DOCKER_HOST 環(huán)境變量,因為 Ansible 將使用它連接到遠程 Docker 守護進程。在使用 HTTP 端點時,需要修改 /etc/default/docker(參見 清單 5)。
清單 5. 修改 /etc/default/docker
- # make docker to listen on HTTP and default socket
- DOCKER_OPTS="-H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock"
輸入命令 sudo service docker restart 來重新啟動 Docker 守護進程,以便讓對它的配置文件的更改生效。
以下命令將會構建并驗證您用來輸入命令的 Ansible 容器(參見 清單 6)。
清單 6. 構建和驗證 Ansible 容器的命令
- # you need DOCKER_HOST variable to point to a reachable docker daemon
- # pick the method that suits your installation
- # for boot2docker users
- eval "$(boot2docker shellinit)"
- # for docker-machine users, provisioning the running VM was named "dev"
- eval "$(docker-machine env dev)"
- # for users running daemon locally
- export DOCKER_HOST=tcp://$(hostname -I | cut -d" " -f1):2375
- # finally users relying on a remote daemon should provide the server's public ip
- export DOCKER_HOST=tcp://1.2.3.4:2375
- # build the container from Dockerfile
- docker build -t article/ansible .
- # provide server API version, as returned by `docker version | grep -i "server api"`
- # it should be at least greater or equal than 1.8
- export DOCKER_API_VERSION=1.19
- # create and enter the workspace
- docker run -it --name builder \
- # make docker client available inside
- -v /usr/bin/docker:/usr/bin/docker \
- -v /var/run/docker.sock:/var/run/docker.sock \
- # detect local ip
- -e DOCKER_HOST=$DOCKER_HOST \
- -e DEFAULT_DOCKER_API_VERSION=DOCKER_API_VERSION \
- -v $PWD:/app -w /app \ # mount the working space
- article/ansible bash
- # challenge the setup
- $ container > ansible docker -m ping
- 192.168.0.12 | SUCCESS => {
- "invocation": {
- "module_name": "ping",
- "module_args": {}
- },
- "changed": false,
- "ping": "pong"
- }
目前為止,一切順利。您能夠從容器輸入命令。在下一節(jié)中,將對 Ansible 使用特定于 Docker 的擴展。
使用 playbook 和插件擴展 Ansible 環(huán)境
實質上,Ansible 通過 playbook 自動化了它的執(zhí)行,這些 playbook 是指定要執(zhí)行的每個任務和它們的屬性的 YAML 文件(參見清單 7)。
Ansible 還使用了清單 (inventory) 來將用戶提供的主機映射到基礎架構中的具體端點。不同于上一節(jié)中使用的靜態(tài) hosts 文件,Ansible 也支持動態(tài)內容。內置的列表包含一個 Docker 插件,該插件可查詢 Docker 守護進程并向 Ansible playbook 共享大量信息。
清單 7. 一個 Ansible playbook
- # provision.yml
- - name: debug docker host
- hosts: docker
- tasks:
- - name: debug infrastructure
- # access container data : print the state
- debug: var=hostvars["builder"]["docker_state"]
- # you can target individual containers by name
- - name: configure the container
- hosts: builder
- tasks:
- - name: run dummy command
- command: /bin/echo hello world
清單 8 中的命令查詢 Docker 主機,導入事實,打印一些事實,并使用它們對構建器容器執(zhí)行第二個任務(如 清單 7 中所示)。
清單 8. 查詢 Docker 主機的命令
- ansible-playbook provision.yml -i /etc/ansible/inventory
- # ...
- TASK [setup] ********************************************************************
- fatal: [builder]: FAILED! => {"msg": "ERROR! SSH encountered an unknown error during the
- connection. Re-run the command using -vvvv, which enables SSH debugging
- output to help diagnose the issue", "failed": true}
- # ...
Ansible 不能連接容器,因為它沒有運行 SSH 服務器。SSH 服務器是一個要管理的額外進程,它與實際應用程序完全無關。在下一節(jié)中,我們將使用一個連接插件來排除此困難。
連接插件是實現傳輸命令(比如 SSH 或本地執(zhí)行)的類。Docker 1.3 隨帶了 docker exec,并能夠在容器命名空間內運行任務。此外,因為您之前已學習如何連接特定的目標容器,所以您可以使用此功能來處理 playbook。
像其他插件類型一樣,連接掛鉤(參見 清單 9)繼承一個抽象類,會在您將其放到預期的目錄(您在配置文件 ansible.cfg 中定義的是 /opt/ansible-plugins/connections)時自動可用。
清單 9. 連接插件
- # saved as ./connection_plugins/docker.py
- import subprocess
- from ansible.plugins.connections import ConnectionBase
- class Connection(ConnectionBase):
- @property
- def transport(self):
- """ Distinguish connection plugin. """
- return 'docker'
- def _connect(self):
- """ Connect to the container. Nothing to do """
- return self
- def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False,
- executable='/bin/sh', in_data=None, su=None,
- su_user=None):
- """ Run a command within container namespace. """
- if executable:
- local_cmd = ["docker", "exec", self._connection_info.remote_addr, executable, '-c', cmd]
- else:
- local_cmd = '%s exec "%s" %s' % ("docker", self._connection_info.remote_addr, cmd)
- self._display.vvv("EXEC %s" % (local_cmd), host=self._connection_info.remote_addr)
- p = subprocess.Popen(local_cmd,
- shell=isinstance(local_cmd, basestring),
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- stdout, stderr = p.communicate()
- return (p.returncode, '', stdout, stderr)
- def put_file(self, in_path, out_path):
- """ Transfer a file from local to container """
- pass
- def fetch_file(self, in_path, out_path):
- """ Fetch a file from container to local. """
- pass
- def close(self):
- """ Terminate the connection. Nothing to do for Docker"""
- pass
#p#
此代碼掛鉤到 Ansible 模塊中,以便通過一個本地 docker exec 而不是默認的 ssh 來運行命令。您需要重新排列一些設置步驟來告訴 Ansible 使用此插件(參見 清單 10)。
清單 10. docker exec 的連接插件
- # modify the builder Dockerfile to upload the plugin code
- where Ansible is expecting connection plugins
- echo "ADD connection_plugins/docker.py /opt/ansible-plugins/connections/docker.py" >> Dockerfile
- # then, you need to explicitly tell which connection hook to use
- when executing playbooks.
- # you can achieve this by inserting the 'connection' property at the top
- of provision tasks in provision.yml
- - name: configure the container
- connection: docker
- hosts: builder
- # you are ready to redeploy the builder container
- # (providing DOCKER_HOST and DOCKER_API_VERSION are still set like before)
- # rebuild the image
- docker build -t article/ansible .
- # restart the builder environment
- docker run -it --name builder \
- # make docker client available inside
- -v /usr/bin/docker:/usr/bin/docker \
- -v /var/run/docker.sock:/var/run/docker.sock \
- # detect local ip
- -e DOCKER_HOST=$DOCKER_HOST \
- -e DEFAULT_DOCKER_API_VERSION=DOCKER_API_VERSION \
- -v $PWD:/app -w /app \ # mount the working space
- article/ansible bash
- # rerun provisioning from inside
- ansible-playbook -i /etc/ansible/inventory provision.yml
- # ... Hurrah, full green output ...
目前為止,您在容器中運行 Ansible 任務,對容器或主機沒有太多需求。盡管此實現滿足了初始需求,但仍有一些不嚴密的地方需要解決。
前面的代碼在同一個節(jié)點上運行任務。一種更逼真的工作流會啟動一個新基礎鏡像,配置它,最終提交、推送和關閉得到的工件。得益于 Ansible 中內置的 Docker 模塊,這些步驟無需額外的代碼即可實現(參見 清單 11)。
清單 11. Ansible 中啟動一個新基礎鏡像的 Docker 模塊
- ---
- - name: initialize provisioning
- hosts: docker
- - name: start up target container
- docker:
- image: python:2.7
- name: lab
- pull: missing
- detach: yes
- tty: yes
- command: sleep infinity
- state: started
- # dynamically update inventory to make it available down the playbook
- - name: register new container hostname
- add_host: name=lab
- - name: provision container
- connection: docker
- hosts: lab
- tasks:
- # ...
- - name: finalize build
- hosts: docker
- tasks:
- - name: stop container
- docker:
- name: lab
- image: python:2.7
- state: stopped
前面已經提到過,自動命名和存儲在成功配備后構建的鏡像會很方便。不幸的是,Ansible 中的 Docker 模塊沒有實現方法來標記和推送鏡像。您可以使用簡單的 shell 命令來克服此限制(參見 清單 12)。
清單 12. 命名和存儲鏡像的 shell 命令
- # name the resulting artifact under a human readable image tag
- docker tag lab article/lab:experimental
- # push this image to the official docker hub
- # make sure to replace 'article' by your own Docker Hub login (https://hub.docker.com)
- # (this step is optional and will only make the image available from any docker host.
- You can skip it or even use your own registry)
- docker push article/lab:experimental
我們的工具正在成形,但它仍缺少一個必要特性:層緩存。
在使用 Dockerfile 構建容器時,通常需要迭代許多次才能完成。為了顯著加快該過程,成功的步驟會被緩存并在后續(xù)運行中重用。
要復制此行為,我們的工具在每次成功完成任務后提交了容器狀態(tài)。如果發(fā)生構建錯誤,該工具會從上次的快照位置重新啟動配備過程。Ansible 承諾實現冪等的任務,所以以前成功的任務不會處理兩次。
借助 Ansible,您可以使用回調插件來掛住任務事件(參見 清單 13)。這些類應實現了特定的回調,這些回調在 playbook 生命周期的各個步驟上觸發(fā)。
清單 13. 掛住任務事件的回調插件
- # save as callback_plugins/docker-cache.py
- import hashlib
- import os
- import socket
- # Hacky Fix `ImportError: cannot import name display`
- # pylint: disable=unused-import
- import ansible.utils
- import requests
- import docker
- class DockerDriver(object):
- """ Provide snapshot feature through 'docker commit'. """
- def __init__(self, author='ansible'):
- self._author = author
- self._hostname = socket.gethostname()
- try:
- err = self._connect()
- except (requests.exceptions.ConnectionError, docker.errors.APIError), error:
- ansible.utils.warning('Failed to contact docker daemon: {}'.format(error))
- # deactivate the plugin on error
- self.disabled = True
- return
- self._container = self.target_container()
- self.disabled = True if self._container is None else False
- def _connect(self):
- # use the same environment variable as other docker plugins
- docker_host = os.getenv('DOCKER_HOST', 'unix:///var/run/docker.sock')
- # default version is current stable docker release (10/07/2015)
- # if provided, DOCKER_VERSION should match docker server api version
- docker_server_version = os.getenv('DOCKER_VERSION', '1.19')
- self._client = docker.Client(base_url=docker_host,
- version=docker_server_version)
- return self._client.ping()
- def target_container(self):
- """ Retrieve data on the container you want to provision. """
- def _match_container(metadatas):
- return metadatas['Id'][:len(self._hostname)] == self._hostname
- matchs = filter(_match_container, self._client.containers())
- return matchs[0] if len(matchs) == 1 else None
- def snapshot(self, host, task):
- tag = hashlib.md5(repr(task)).hexdigest()
- try:
- feedback = self._client.commit(container=self._container['Id'],
- repository='factory',
- tag=tag,
- author=self._author)
- except docker.errors.APIError, error:
- ansible.utils.warning('Failed to commit container: {}'.format(error))
- self.disabled = True
- # pylint: disable=E1101
- class CallbackModule(object):
- """Emulate docker cache.
- Commit the current container for each task.
- This plugin makes use of the following environment variables:
- - DOCKER_HOST (optional): How to reach docker daemon.
- Default: unix://var/run/docker.sock
- - DOCKER_VERSION (optional): Docker daemon version.
- Default: 1.19
- - DOCKER_AUTHOR (optional): Used when committing image. Default: Ansible
- Requires:
- - docker-py >= v0.5.3
- Resources:
- - http://docker-py.readthedocs.org/en/latest/api/
- """
- _current_task = None
- def playbook_on_setup(self):
- """ initialize client. """
- self.controller = DockerDriver(self.conf.get('author', 'ansible'))
- def playbook_on_task_start(self, name, is_conditional):
- self._current_task = name
- def runner_on_ok(self, host, res):
- if self._current_task is None:
- # No task performed yet, don't commit
- return
- self.controller.snapshot(host, self._current_task)
因為您已將代碼上傳到期望的位置,并重新構建了構建器容器,所以您可以像 docker exec 連接插件一樣注冊此插件。
清單 14. 注冊回調插件的命令
- # modify the builder Dockerfile to upload the code where Ansible is expecting callback plugins
- echo "ADD callback_plugins/docker-cache.py /opt/ansible-plugins/callbacks/docker-cache.py" >> Dockerfile
重新構建構建器容器并重新運行 Ansible playbook 后,該模塊會自動加載,您可以查看中間容器是如何創(chuàng)建的(參見 清單 15)。
清單 15. Docker 鏡像
- REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
- factory bc0fb8843e88566c bbdfab2bd904 32 seconds ago 829.8 MB
- factory d19d39e0f0e5c133 e82743310d8c 55 seconds ago 785.2 MB
結束語
配備是一個復雜流程,您在本教程中執(zhí)行的實現為未來的開發(fā)奠定了基礎。代碼本身已經過簡化,而且一些步驟仍需人類干預。緩存實現肯定值得更多關注,例如更具體的提交命名或清理技能。
盡管如此,您創(chuàng)建了一個可運行 Ansible playbook 來管理容器配置的工具。借助此實現,您可以通過為基礎架構的微型服務來組合、重用和設置陳述性的構建文件,充分利用 Ansible 的全部威力。此解決方案可幫助避免鎖定問題。您開發(fā)的插件包裝了 playbook,您可對不同的目標重用這些 playbook,而且極低的需求使得該項目能兼容大部分提供程序。
原文鏈接:http://www.ibm.com/developerworks/cn/cloud/library/cl-provision-docker-containers-ansible/index.html