新聞中心
如何讓 Ansible 和 docker 愉快地在一起
作者:Xavier Bruhiere 2015-10-20 16:48:06
云計算 Docker 和它的容器工作流可封裝、共享和部署您的應(yīng)用程序環(huán)境。Ansible 是一個與 Docker 高度兼容的自動化工具,它使用一個強(qiáng)大的接口來管理遠(yuǎn)程服務(wù)器上的容器。在本文中,我將探索為何和如何使用 Ansible 的可擴(kuò)展設(shè)計來將 Docker 和 Ansible 的最佳功能合并在一起。

Docker 之所以如此流行,是因為它創(chuàng)造了一種采用方便的命令行接口 (CLI) 和 HTTP API 工具來封裝、運(yùn)行和維護(hù)容器的獨(dú)特方式。這種簡化降低了此技術(shù)的入門門檻,使得將應(yīng)用程序及其運(yùn)行時環(huán)境封裝到一個簡單 Dockerfile 中的獨(dú)立鏡像中變得可行。Docker 使您能夠開發(fā)更復(fù)雜的項目,但您仍需要配置這些容器。在本文中,我將展示 Ansible 如何通過更清晰的語法帶來配置管理器的特性。您將學(xué)習(xí)如何僅使用已安裝的 Python 和 Docker 構(gòu)建任何堆棧。
在介紹 Ansible 的細(xì)節(jié)之前,我首先將介紹 Ansible 的 分析 中提到的一些要點(diǎn):
- 盡管容器導(dǎo)致一些新工作流出現(xiàn),但編排和配置工具仍然非?;钴S。
- Ansible 和 Salt 等新參與者正在挑戰(zhàn)現(xiàn)有的工具,比如 Chef 和 Puppet。
- 許多與 Docker 有關(guān)聯(lián)的開發(fā)人員也很關(guān)心這些工具。
更確切地講,借助 Docker,您可以在幾秒內(nèi)實(shí)現(xiàn)完全隔離的堆棧環(huán)境,或者在服務(wù)器之間復(fù)制準(zhǔn)確的設(shè)置。但是,Docker 不包含提供端到端體驗的可靠工具,無論是針對開發(fā)還是生產(chǎn)。Docker 團(tuán)隊通過新的集群工具解決了這些不斷演變的挑戰(zhàn),嘗試將 Docker 轉(zhuǎn)變?yōu)橐粋€大規(guī)模運(yùn)行容器的可靠解決方案。然而,Docker 仍然需要您手動硬編碼任務(wù)和重復(fù)常見的設(shè)置。所以,針對容器的編排和配置管理的關(guān)鍵 Docker 流程仍有待解決。在本文中,您將學(xué)習(xí)如何結(jié)合使用 Ansible 和 Docker 來幫助解決這些問題。
DevOps 的興起
在部署到生產(chǎn)中之前,現(xiàn)代應(yīng)用程序通常涉及到一個復(fù)雜的部署管道。最佳實(shí)踐建議在每次小型迭代后盡早地、頻繁地發(fā)布代碼。任務(wù)的手動執(zhí)行無法擴(kuò)展,組織已開始完善介于開發(fā)人員與系統(tǒng)管理員之間的流程,所以 DevOps 就誕生了。從那時起,敏捷團(tuán)隊就開始嘗試強(qiáng)化和自動化測試代碼,以及將其交付給用戶的方式。
通過實(shí)現(xiàn)最新的技術(shù)和方法,公司對其服務(wù)器上的代碼建立了信心。然而,隨著應(yīng)用程序在規(guī)模和復(fù)雜性上不斷增長,開發(fā)人員和系統(tǒng)管理員繼續(xù)面臨著無數(shù)挑戰(zhàn)?,F(xiàn)在比以往更需要為產(chǎn)品提供受支持的社區(qū)驅(qū)動工具。
Ansible 的可擴(kuò)展設(shè)計
在此環(huán)境中,Ansible 提供了一個有趣的框架來管理基礎(chǔ)架構(gòu)。您可以獲得服務(wù)器定義的控制權(quán),比如要安裝的包或要復(fù)制的文件,并將該配置擴(kuò)展到數(shù)千個服務(wù)器。Ansible playbook 構(gòu)成了集群的目標(biāo)狀態(tài)的一種安全表示。它的 YAML 語法和龐大的模塊列表生成了任何開發(fā)人員都能快速理解的易讀性配置文件。不同于 Chef 或 Puppet,Ansible 是無代理的,這意味著您要在遠(yuǎn)程主機(jī)上運(yùn)行命令,只需一個 SSH 連接即可。可以看到,Ansible 可輕松地處理 DevOps 復(fù)雜性。
但是,Ansible 是在容器快速興起和它們在云開發(fā)環(huán)境中帶來革命之前設(shè)計的。那么 Ansible 是否仍然有用?微型服務(wù)的范式和復(fù)雜開發(fā)環(huán)境引入了新的需求:
- 輕量型鏡像。為了容易傳輸或節(jié)省成本,鏡像被剝離到僅剩下最低限度的依賴項。
- 單一用途,單一流程。如果應(yīng)用程序不是非常需要 SSH 守護(hù)進(jìn)程,則無需運(yùn)行它。
- 短暫性。容器隨時可能死亡、轉(zhuǎn)移和復(fù)活。
在此上下文中,Ansible 的可擴(kuò)展架構(gòu)解決了這些問題。一個 Docker 模塊在較高層面上管理主機(jī)和容器。盡管您可能會爭論哪個編排工具(來自 Google 的 Kubernetes,還是來自 New Relic 的 Centurion)最適合此環(huán)境,但 Docker 模塊執(zhí)行效率很高,這正是我在本文中使用它的原因。但是,您還可以構(gòu)建從其官方 Ansible 鏡像啟動的容器,然后在本地模式下從內(nèi)部運(yùn)行 playbook。盡管此方法非常適合 Packer,而且肯定也適合許多使用情況,但它的缺點(diǎn)通常極為關(guān)鍵。
您被鎖定在一個基礎(chǔ)鏡像中,無法再利用特殊的秘訣或其他堆棧。
最終的工件已安裝 Ansible 和它的依賴項,它們與實(shí)際應(yīng)用程序毫無關(guān)系,這讓工件變得更笨重。
盡管 Ansible 可管理數(shù)千個服務(wù)器,但它只配備(Provision)了一個容器。
此方法將容器視為小型 VM,您可以在其中使用一個特定的解決方案。幸運(yùn)的是,Ansible 擁有模塊化設(shè)計。模塊分散在不同的存儲庫中,而且 Ansible 的大部分功能都可以通過插件進(jìn)行擴(kuò)展。
在下一節(jié)中,您將設(shè)置一個有效的環(huán)境,針對您的需求來調(diào)整 Ansible。
設(shè)置一個 Ansible 環(huán)境
假設(shè)您想要一個很容易部署的工具,它在輕量型容器中配置應(yīng)用程序環(huán)境。由于與這些容器分離,您需要一個安裝了 Ansible 的客戶端,您將使用它來向 Docker 守護(hù)進(jìn)程發(fā)送命令。此設(shè)置如 圖 1 中所示。
圖 1. 使用 Ansible 配備容器所需的組件
您在此配置中必須管理的依賴項,已通過從容器中運(yùn)行 Ansible 來最小化。此架構(gòu)將主機(jī)限制為容器與命令之間的一個通信橋梁。
可通過許多選項在您服務(wù)器上安裝 Docker:
使用 docker-machine 將它安裝在遠(yuǎn)程主機(jī)上。
安裝在本地。順便說一下,您可能不想親自管理一個嚴(yán)格基于容器的基礎(chǔ)架構(gòu);在這種情況下,可以考慮采用外部提供程序。
依賴于外部提供程序。
使用 boot2docker,這是一個在 Windows 和 Mac 上運(yùn)行 Docker 容器的輕量型 Linux 發(fā)行版。
無論選擇何種解決方案,請確保部署了 Docker 1.3 版或更高版本(1.3 版引入了進(jìn)程注入)。您還需要運(yùn)行一個 SSH 服務(wù)器來安全地處理 Ansible 命令。
清單 1 中的命令使用公鑰設(shè)置了一種方便可靠的身份驗證方法。
清單 1. 使用公鑰設(shè)置身份驗證的命令
- # 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 和安全性問題不屬于本文的討論范圍。細(xì)心的讀者可查閱 /etc/ssh/sshd_config 文件,進(jìn)一步了解配置 SSH 的可用選項。
下一步是將公鑰加載到運(yùn)行 Ansible 的客戶端容器上并配備構(gòu)建器容器。使用一個 Dockerfile 來配備構(gòu)建器。參見 清單 2。
清單 2. 配備構(gòu)建器的 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
這些指令改編自官方構(gòu)建版本,自動化了一次來自 Ansible 主分支上的提交 fbec8bfb90df1d2e8a0a4df7ac1d9879ca8f4dde 有效安裝。
主機(jī)和 ansible.cfg 配置文件(參見 清單 3 和 清單 4)已封裝。通過使用容器,可以確保您將共享同一個環(huán)境。在本示例中,Dockerfile 安裝了 Python 2.7.10 版和 Ansible 2.0.0。
清單 3. 主機(jī)配置文件
- # 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#
在構(gòu)建 Ansible 容器之前,您必須導(dǎo)出 DOCKER_HOST 環(huán)境變量,因為 Ansible 將使用它連接到遠(yuǎn)程 Docker 守護(hù)進(jìn)程。在使用 HTTP 端點(diǎn)時,需要修改 /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 守護(hù)進(jìn)程,以便讓對它的配置文件的更改生效。
以下命令將會構(gòu)建并驗證您用來輸入命令的 Ansible 容器(參見 清單 6)。
清單 6. 構(gòu)建和驗證 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 的擴(kuò)展。
使用 playbook 和插件擴(kuò)展 Ansible 環(huán)境
實(shí)質(zhì)上,Ansible 通過 playbook 自動化了它的執(zhí)行,這些 playbook 是指定要執(zhí)行的每個任務(wù)和它們的屬性的 YAML 文件(參見清單 7)。
Ansible 還使用了清單 (inventory) 來將用戶提供的主機(jī)映射到基礎(chǔ)架構(gòu)中的具體端點(diǎn)。不同于上一節(jié)中使用的靜態(tài) hosts 文件,Ansible 也支持動態(tài)內(nèi)容。內(nèi)置的列表包含一個 Docker 插件,該插件可查詢 Docker 守護(hù)進(jìn)程并向 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 主機(jī),導(dǎo)入事實(shí),打印一些事實(shí),并使用它們對構(gòu)建器容器執(zhí)行第二個任務(wù)(如 清單 7 中所示)。
清單 8. 查詢 Docker 主機(jī)的命令
- 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 不能連接容器,因為它沒有運(yùn)行 SSH 服務(wù)器。SSH 服務(wù)器是一個要管理的額外進(jìn)程,它與實(shí)際應(yīng)用程序完全無關(guān)。在下一節(jié)中,我們將使用一個連接插件來排除此困難。
連接插件是實(shí)現(xiàn)傳輸命令(比如 SSH 或本地執(zhí)行)的類。Docker 1.3 隨帶了 docker exec,并能夠在容器命名空間內(nèi)運(yùn)行任務(wù)。此外,因為您之前已學(xué)習(xí)如何連接特定的目標(biāo)容器,所以您可以使用此功能來處理 playbook。
像其他插件類型一樣,連接掛鉤(參見 清單 9)繼承一個抽象類,會在您將其放到預(yù)期的目錄(您在配置文件 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 而不是默認(rèn)的 ssh 來運(yùn)行命令。您需要重新排列一些設(shè)置步驟來告訴 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 ...
目前為止,您在容器中運(yùn)行 Ansible 任務(wù),對容器或主機(jī)沒有太多需求。盡管此實(shí)現(xiàn)滿足了初始需求,但仍有一些不嚴(yán)密的地方需要解決。
前面的代碼在同一個節(jié)點(diǎn)上運(yùn)行任務(wù)。一種更逼真的工作流會啟動一個新基礎(chǔ)鏡像,配置它,最終提交、推送和關(guān)閉得到的工件。得益于 Ansible 中內(nèi)置的 Docker 模塊,這些步驟無需額外的代碼即可實(shí)現(xiàn)(參見 清單 11)。
清單 11. Ansible 中啟動一個新基礎(chǔ)鏡像的 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
前面已經(jīng)提到過,自動命名和存儲在成功配備后構(gòu)建的鏡像會很方便。不幸的是,Ansible 中的 Docker 模塊沒有實(shí)現(xiàn)方法來標(biāo)記和推送鏡像。您可以使用簡單的 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 構(gòu)建容器時,通常需要迭代許多次才能完成。為了顯著加快該過程,成功的步驟會被緩存并在后續(xù)運(yùn)行中重用。
要復(fù)制此行為,我們的工具在每次成功完成任務(wù)后提交了容器狀態(tài)。如果發(fā)生構(gòu)建錯誤,該工具會從上次的快照位置重新啟動配備過程。Ansible 承諾實(shí)現(xiàn)冪等的任務(wù),所以以前成功的任務(wù)不會處理兩次。
借助 Ansible,您可以使用回調(diào)插件來掛住任務(wù)事件(參見 清單 13)。這些類應(yīng)實(shí)現(xiàn)了特定的回調(diào),這些回調(diào)在 playbook 生命周期的各個步驟上觸發(fā)。
清單 13. 掛住任務(wù)事件的回調(diào)插件
- # 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. """
- &nb
當(dāng)前名稱:如何讓Ansible和Docker愉快地在一起
文章來源:http://www.dlmjj.cn/article/ccsdsoo.html


咨詢
建站咨詢
