Many developers reach for Kubernetes and other container orchestration solutions for deploying containerized applications. Yet, there is still a case for using plain Docker Compose. Orchestration systems entail extra maintenance costs and increase on-boarding time for new hires. A small team’s application with a handful containers won’t reap many benefits from Kubernetes. This is truer on-premise, outside of managed services like AWS EKS.
许多开发人员可以使用Kubernetes和其他容器编排解决方案来部署容器化的应用程序。 但是,仍然存在使用普通Docker Compose的情况。 编排系统需要额外的维护成本,并增加新员工的入职时间。 一个带有少量容器的小型团队的应用程序不会从Kubernetes获得很多好处。 在诸如AWS EKS之类的托管服务之外,这在内部环境中更为真实。
This article will show a sane strategy for deploying Docker Compose services. We will use two external services to do this. One is GitHub for CI (using Actions) and source control, and the second is Docker Hub for hosting our Docker images. We will also use Ansible for configuring our remote hosts, but you can choose your own services and tools. The concepts should transfer.
本文将展示部署Docker Compose服务的明智策略。 我们将使用两个外部服务来执行此操作。 一个是用于CI(使用Actions)和源代码控制的GitHub,第二个是用于托管我们的Docker映像的Docker Hub。 我们还将使用Ansible来配置我们的远程主机,但是您可以选择自己的服务和工具。 概念应该转移。
Prerequisites:
先决条件:
- GitHub account GitHub帐户
- Docker Hub account Docker Hub帐户
- Access to a remote machine 访问远程机器
- Python 3 on your development machine 开发机器上的Python 3
战略 (Strategy)
Using GitHub Actions, we build each repository’s Docker image, run tests and push it to Docker Hub. Then GitHub will execute the appropriate deploy script over SSH, which restarts the app with the new Docker image(s).
使用GitHub Actions,我们构建每个存储库的Docker映像,运行测试并将其推送到Docker Hub。 然后GitHub将通过SSH执行适当的部署脚本,该脚本将使用新的Docker映像重新启动应用程序。
To make this happen, you’ll setup some repositories in Docker Hub and GitHub, configure your remote machine with a (relatively) unprivileged user to perform the deployments, and add some keys to GitHub.
为此,您将在Docker Hub和GitHub中设置一些存储库,使用(相对)无特权的用户配置远程计算机以执行部署,并向GitHub添加一些密钥。
创建存储库(GitHub) (Create repositories (GitHub))
First, create two repositories in GitHub (it’ll be easier to fork mine: https://github.com/jayhardee9/hello-web-app and https://github.com/jayhardee9/reverse-proxy). One will be a Flask app, and the other will be a reverse proxy (using NGINX). If you’re creating your own repositories, call them hello-web-app and reverse-proxy. That way you change fewer names, strings, etc. in the upcoming example code.
首先,在GitHub中创建两个存储库(比较容易: https : //github.com/jayhardee9/hello-web-app和https://github.com/jayhardee9/reverse-proxy )。 一个将是Flask应用程序,另一个将是反向代理(使用NGINX)。 如果要创建自己的存储库,则将它们称为hello-web-app和reverse-proxy 。 这样,您将在接下来的示例代码中更改更少的名称,字符串等。
创建存储库(Docker Hub) (Create repository (Docker Hub))
Docker Hub will host the Docker image for the Flask app (you don’t need a paid account). Create a repository called hello-web-app. The reverse-proxy uses a publicly available NGINX image with a custom configuration file, so we don’t need another repository for it.
Docker Hub将托管Flask应用程序的Docker映像(您不需要付费帐户)。 创建一个名为hello-web-app的存储库。 反向代理使用带有自定义配置文件的公共可用NGINX映像,因此我们不需要其他存储库。
Ansible概述和设置 (Ansible Overview and Setup)
We will use Ansible to set up the host — the big benefits we will get from Ansible from a maintenance standpoint will be:
我们将使用Ansible设置主机-从维护的角度来看,我们将从Ansible获得的最大好处是:
- File templates to instantiate multiple files that share the same structure 文件模板以实例化共享同一结构的多个文件
- Encrypting secrets, like environment variables for each app and credentials 加密机密,例如每个应用程序的环境变量和凭据
- Retrieving public and private keys we need to set up CI 检索我们需要设置CI的公钥和私钥
- Easily adding more Docker Compose applications down the road 轻松添加更多Docker Compose应用程序
Now, I’m using a local VM running Ubuntu Server to dog-food everything here. You may have to tweak the playbooks some to get them working with your machine. Clone the docker-compose-infra repository to your local machine, and let’s go over the files therein.
现在,我正在使用运行Ubuntu Server的本地VM来跟踪此处的所有内容。 您可能需要对剧本进行一些调整,以使它们与您的机器配合使用。 将docker-compose-infra存储库克隆到本地计算机,然后浏览其中的文件。
The first file is hosts.yml — what is known as the inventory in Ansible terminology:
第一个文件是hosts.yml -Ansible术语中的清单 :
It defines a single host called my-host, some variables with credentials, and specifies an apps array, which contains the app names. Instead of duplicating setup actions and files for each app, we will be able to iterate over the apps array and use templates, as we shall see later.
它定义了一个名为my-host的主机 ,其中包含一些带有凭据的变量,并指定一个apps数组,其中包含应用程序名称。 无需为每个应用程序复制设置操作和文件,我们将能够遍历应用程序数组并使用模板,这将在后面看到。
Following the TODOs, make the following changes:
遵循TODO,进行以下更改:
- Add your GitHub account name on line 5 在第5行上添加您的GitHub帐户名
- Put the IP address or domain name of your remote host on line 12 将您的远程主机的IP地址或域名放在第12行
Now to add your Docker Hub credentials, we’re going to use Ansible Vault to encrypt them first — then add them to the inventory so that they remain secret. Run these commands to install Vault (and Ansible itself):
现在要添加您的Docker Hub凭据,我们将首先使用Ansible Vault对其进行加密- 然后将其添加到清单中,以使它们保持秘密。 运行以下命令以安装Vault(以及Ansible本身):
Now, one more thing before you encrypt your credentials: pick a vault password. You’ll need it whenever you encrypt or decrypt anything.
现在,在对凭据进行加密之前,还有一件事:选择一个库密码。 每当您加密或解密任何内容时,都将需要它。
With your password handy, run ansible-vault encrypt-string
twice for encrypting your username and password. An example session with ansible-vault looks like:
ansible-vault encrypt-string
密码,即可运行ansible-vault encrypt-string
两次,以对用户名和密码进行加密。 ansible-vault的示例会话如下所示:
You then copy the string beginning with !vault |
and paste into the hosts.yml like so:
然后,您复制以!vault |
开头的字符串!vault |
并粘贴到hosts.yml中,如下所示:
Moving on to setup.yml, you’ll see a list of Ansible tasks to run to perform the initial setup of your machine. This is an Ansible playbook — a logical grouping of tasks. It assumes you have a sudoer user on the remote server. Briefly, it performs these steps:
转到setup.yml ,您将看到要执行的Ansible 任务列表,以执行计算机的初始设置。 这是Ansible 剧本 -逻辑分组的任务。 假定您在远程服务器上有一个sudoer用户。 简要地说,它执行以下步骤:
- Installing Docker and Docker Compose 安装Docker和Docker Compose
- Adding a special user for performing deploys 添加用于执行部署的特殊用户
- Generating key pairs for GitHub -> machine, and machine -> GitHub SSH access 为GitHub->机器和机器-> GitHub SSH访问生成密钥对
- Uploading deploy scripts 上载部署脚本
- Copying SSH keys to operator’s machine to add to GitHub and CI 将SSH密钥复制到操作员的机器上以添加到GitHub和CI
- Adding GitHub’s servers to the known-hosts file for the deploy user 将GitHub的服务器添加到部署用户的已知主机文件中
A few of these points deserve a deeper explanation, beginning with the SSH setup. For each app, we’re going to generate a separate key pair. Why?
从SSH设置开始,其中一些要点值得更深入的说明。 对于每个应用程序,我们将生成一个单独的密钥对。 为什么?
GitHub deploy keys (which allow machine users, like deploy, to access repositories) have to be unique across all repositories. We have two apps. Ergo, different keys for each app.
GitHub部署密钥(允许诸如deploy的机器用户访问存储库)在所有存储库中必须是唯一的 。 我们有两个应用程序。 嗯,每个应用程序都有不同的键。
We lock down our machine user’s SSH access so that it can execute only one command — the deploy script. We wish to establish one-to-one relationships between applications, deploy scripts and SSH keys. So there is a separate script for each app, each app has its own key pair, and the public key is locked to the corresponding deploy script/app. This seems a little complicated, but Ansible loops and templates help out a lot here. See update-authorized-keys.sh in the Ansible repository.
我们锁定了计算机用户的SSH访问权限,以便它只能执行一个命令-部署脚本。 我们希望在应用程序之间建立一对一的关系,部署脚本和SSH密钥。 因此,每个应用程序都有一个单独的脚本,每个应用程序都有自己的密钥对,并且公钥已锁定到相应的部署脚本/应用程序。 这似乎有些复杂,但是Ansible循环和模板在这里有很大帮助。 见update-authorized-keys.sh在Ansible库。
Another SSH tidbit that’s interesting is the deploy user’s ~/.ssh/config:
另一个有趣的SSH技巧是部署用户的〜/ .ssh / config :
It’s a Jinja template that creates a config entry for each app in the deploy user’s SSH configuration. It tells SSH that if it sees us connecting to, for example, hello-web-app.github.com, that it should:
这是一个Jinja模板,可为部署用户的SSH配置中的每个应用程序创建一个配置条目。 它告诉SSH,如果看到我们连接到例如hello-web-app.github.com ,则它应该:
use the key at ~/.ssh/hello-web-app.id_ed25519
使用〜/ .ssh / hello-web-app.id_ed25519中的密钥
connect as the git user
以git用户身份连接
actually connect to github.com instead of hello-web-app.github.com
实际上连接到github.com而不是hello-web-app.github.com
Why? To setup Git access, we need to use our deploy keys, which differ from app to app. When we use the git CLI tool, we need a way of telling Git, “Hey, use this key for repository A, and this other one for repository B.” So in the deploy user’s SSH config, we’re going to bind the key for hello-web-app to hello-web-app.github.com. So when we run git clone git@hello-web-app.github.com/<your account>/hello-web-app.git
, it will use the correct key pair. If we just used github.com, git wouldn’t know which key to use, and we would get a unauthorized error.
为什么? 要设置Git访问,我们需要使用我们的部署密钥,这对于不同的应用程序是不同的。 当我们使用git CLI工具时,我们需要一种告诉Git的方法:“嘿,将此密钥用于存储库A,而另一个用于存储库B。” 因此,在部署用户的SSH配置中,我们将把hello-web-app的密钥绑定到hello-web-app.github.com 。 因此,当我们运行git clone git@hello-web-app.github.com/<your account>/hello-web-app.git
,它将使用正确的密钥对。 如果我们只是使用github.com ,则git将不知道要使用哪个密钥,并且会得到未授权的错误。
There are two other playbooks in the docker-compose-infra repository: clone-projects.yml and deploy.yml. The first sets up each application’s GitHub repository and environment variable file:
docker -compose-infra存储库中还有另外两本剧本: clone-projects.yml和deploy.yml 。 首先设置每个应用程序的GitHub存储库和环境变量文件:
The other deploys the latest commit for each app:
另一个为每个应用程序部署最新的提交:
The deploy scripts that the latter runs deserve a few comments. Each app has its own instantiated from the deploy-app.sh.j2 template:
后者运行的部署脚本值得一提。 每个应用程序都有一个自己的实例,它来自deploy-app.sh.j2模板:
Each script simply pulls the latest commit from master, sets the environment variables, and spins up the Docker Compose application with the production Docker Compose config.
每个脚本都简单地从master提取最新提交,设置环境变量,并使用生产Docker Compose配置启动Docker Compose应用程序。
设置远程机器 (Set up remote machine)
Let’s run the first Ansible playbook, setup.yml. Depending on your SSH setup, you may need to use different flags. Refer to ansible-playbook -h
for more options.
让我们运行第一个Ansible剧本setup.yml 。 根据您的SSH设置,您可能需要使用不同的标志。 有关更多选项,请参考ansible-playbook -h
。
After it completes, you should have a directory called keys— it contains a key pair for each app. The public key will be the GitHub deploy key, and the private key will allow GitHub Actions to run a deploy script. Before we run the other playbooks, we must get the keys into GitHub.
完成后,您应该有一个名为keys的目录-它包含每个应用程序的密钥对。 公钥将是GitHub部署密钥,而私钥将允许GitHub Actions运行部署脚本。 在运行其他手册之前,我们必须将密钥输入GitHub。
Beginning with the deploy key, copy the contents of /keys/my-host/home/deploy/.ssh/hello-web-app.id_ed25519.pub. Add the key as a deploy key for your hello-web-app repository. Do the same for reverse-proxy.
从部署密钥开始,复制/keys/my-host/home/deploy/.ssh/hello-web-app.id_ed25519.pub的内容。 将密钥添加为hello-web-app存储库的部署密钥。 对反向代理执行相同的操作。
Moving on to the private keys, go to Settings > Secrets of the hello-web-app repository to add a secret called SSH_KEY. The value should be the contents of /keys/my-host/home/deploy/.ssh/hello-web-app.id_ed25519. Again, do the same for reverse-proxy.
转到私钥,转到hello-web-app存储库的“设置”>“秘密”以添加一个名为SSH_KEY的秘密。 该值应为/keys/my-host/home/deploy/.ssh/hello-web-app.id_ed25519的内容。 同样,对反向代理执行相同的操作。
Before running the next playbook, create hello-web-app-envrc. The playbook expects, for each app, a script called <app name>-envrc that sets environment variables. Although hello-web-app doesn’t expect anything top-secret as an environment variable, let’s pretend otherwise and create an encrypted hello-web-app-envrc file using the following command (using the same Vault password as before):
在运行下一个剧本之前,请创建hello-web-app-envrc 。 对于每个应用程序,剧本都希望有一个名为<app name> -envrc的脚本来设置环境变量。 尽管hello-web-app不会期望任何绝密内容作为环境变量,但我们还是假装并使用以下命令创建加密的hello-web-app-envrc文件(使用与以前相同的保险柜密码):
Just add
只需添加
export NAME=<your-name>
in the text editor that just opened, save it and exit. Now if you open hello-web-app-envrc, you should see a bunch of gibberish, as desired.
在刚打开的文本编辑器中,保存并退出。 现在,如果您打开hello-web-app-envrc ,则应该看到一堆乱七八糟的东西。
Do the same process for reverse-proxy-envrc, but leave it blank, since that app doesn’t have any environment variables.
对reverse-proxy-envrc进行相同的处理,但是将其留空,因为该应用程序没有任何环境变量。
Now you should be able to run the clone-projects.yml playbook (using a similar command as the setup.yml playbook). Afterwards, you should see the project repositories cloned to deploy’s home directory, each containing a .envrc file. The next steps will be getting set up with the two apps, and triggering our first builds!
现在,您应该能够运行clone-projects.yml剧本(使用与setup.yml剧本类似的命令)。 之后,您应该会看到已克隆项目存储库以部署其主目录,每个存储库均包含一个.envrc文件。 下一步将是使用这两个应用程序进行设置,并触发我们的第一个版本!
设置hello-web-app (Set up hello-web-app)
The first app we will deploy will be hello-web-app. All it will do is render “Hello, <name>”, inserting the NAME environment variable. As you see in main.py below, it is a basic Flask app that renders a greeting when visiting localhost:5000/. We’re going to get the project working on your local machine before triggering the first deployment.
我们将部署的第一个应用程序将是hello-web-app 。 它所要做的就是渲染“ Hello,<name>”,并插入NAME环境变量。 正如您在下面的main.py中看到的那样,它是一个基本的Flask应用程序,在访问localhost:5000 /时会发出问候。 在触发首次部署之前,我们将使项目在本地计算机上运行。
Next, let’s check out the Dockerfile:
接下来,让我们检查Dockerfile :
Again, simple (and likely not optimal), but it gets our web app running. Also nothing to tweak here.
再次,简单(可能不是最佳),但是它使我们的Web应用程序运行。 这里也没有什么要调整的。
Now docker-compose.yml is more interesting:
现在docker-compose.yml更有趣了:
The big stand-out is the network configuration. For our Docker Compose applications to talk to each other, we need to set up a Docker network for inter-application DNS resolution. In other words, we would like to resolve Docker image names to IP addresses that belong to other containers. You’ll want to run docker network create my_services
now to create that network.
最突出的是网络配置。 为了使我们的Docker Compose应用程序能够相互通信,我们需要建立一个Docker网络以实现应用程序间DNS解析。 换句话说,我们想将Docker映像名称解析为属于其他容器的IP地址。 您现在要运行docker network create my_services
来创建该网络。
Another interesting thing from docker-compose.yml is that we don’t specify an image or Dockerfile location for our web service. This is because during development, we’d like to build images, and on our “production” server, we want to pull the image from Docker Hub. If we look at docker-compose.override.yml, we see build: .
. So running docker-compose up --build
will build the Flask image and start the app. On the remote server, the deploy scripts run the following to use the “production” configuration:
docker-compose.yml的另一件有趣的事情是,我们没有为我们的Web服务指定映像或Dockerfile位置。 这是因为在开发过程中,我们想构建映像,并且在“生产”服务器上,我们想从Docker Hub中提取映像。 如果我们查看docker-compose.override.yml ,我们会看到build: .
:。 。 因此,运行docker-compose up --build
将构建Flask映像并启动应用程序。 在远程服务器上,部署脚本运行以下命令以使用“生产”配置:
A bit more complicated… but it just:
有点复杂……但是它只是:
specifies which image to pull using the COMMIT variable, and
使用COMMIT变量指定要拉出的图像,以及
sets the Docker Compose configuration by merging docker-compose.yml and docker-compose-prod.yml (see the rules here)
通过合并docker-compose.yml和docker -compose-prod.yml来设置Docker Compose配置(请参阅此处的规则)
Now checking out docker-compose-prod.yml, we see that it pulls the image tagged with the current commit hash like I said — also be sure to put your Docker Hub account name on line 8!
现在检查docker-compose-prod.yml ,我们看到它像我说的那样拉出带有当前提交哈希标记的图像- 还请确保将您的Docker Hub帐户名放在第8行!
Why not use the latest tag? Because if we always push to latest, we can’t revert back in case of any issues on prod. Keeping each deployed commit available to pull means we can roll things back if needed.
为什么不使用最新标签? 因为如果我们始终坚持到最新 ,则在产品出现任何问题时我们将无法还原。 保持每个已部署的提交可用于拉动意味着我们可以在需要时回滚。
That wraps up hello-web-app. Let’s fire it up locally:
结束了hello-web-app 。 让我们在本地启动它:
$ export NAME=<your name here>
$ docker-compose up --build
Navigating to http://localhost:5000, you should be greeted!
导航到http:// localhost:5000 ,您应该受到欢迎!
设置反向代理 (Set up reverse proxy)
Let’s dig into the reverse proxy some. It’s just an NGINX instance that listens on port 80, and forwards requests to /name/ to our Flask app’s port 5000. It should be relatively clear how to easily add other routes for more apps in the future.
让我们深入研究反向代理。 这只是一个侦听端口80的NGINX实例,并将请求转发到/ name /到我们的Flask应用程序的端口5000。应该相对清楚一点,以后如何轻松地为其他应用程序添加其他路由。
We actually don’t need to push any images for this app because we’re only customizing NGINX by mounting our desired nginx.conf into the app container (refer to its docker-compose.yml ). There are no environment variables either, so just run docker-compose up --build , and you should be able to navigate to http://localhost/hello/ and see your greeting again.
实际上,我们不需要为此应用程序推送任何图像,因为我们只是通过将所需的nginx.conf装入应用容器(请参阅其docker-compose.yml ) 来自定义NGINX 。 也没有环境变量,因此只需运行docker-compose up --build ,您应该能够导航到http:// localhost / hello /并再次看到您的问候。
触发初始构建 (Trigger initial builds)
Our remote server is ready to run our apps, but GitHub isn’t quite ready to deploy them. Again, we’re using GitHub Actions to do our CI, so let’s check out the configuration for hello-web-app (at .github/workflows/ci.yml):
我们的远程服务器已准备好运行我们的应用程序,但是GitHub尚未准备好部署它们。 同样,我们使用GitHub Actions来执行CI,因此让我们检查一下hello-web-app的配置(位于.github / workflows / ci.yml ):
The steps are:
这些步骤是:
- Checkout the code 签出代码
- Build and run the app 生成并运行应用
- Get logs for debugging 获取调试日志
- Run the tests 运行测试
- Tag the image with the current commit hash, and push it to Docker Hub 使用当前提交哈希标记图像,并将其推送到Docker Hub
- If on the master branch, execute the deploy script on the remote host 如果在master分支上,请在远程主机上执行deploy脚本
Notice secrets.DOCKER_USERNAME, secrets.DOCKER_PASSWORD, and so on? We need to add those under the repository’s Settings > Secrets — the same process we used to add the SSH_KEY secret. Add these to both GitHub repositories:
注意到secrets.DOCKER_USERNAME , secrets.DOCKER_PASSWORD等吗? 我们需要将它们添加到存储库的“设置”>“秘密”下-与添加SSH_KEY秘密所使用的过程相同。 将它们添加到两个 GitHub存储库中:
DOCKER_USERNAME — your Docker Hub username
DOCKER_USERNAME —您的Docker Hub用户名
DOCKER_PASSWORD — your Docker Hub password
DOCKER_PASSWORD-您的Docker Hub密码
SSH_HOST — your remote machine hosting the apps
SSH_HOST-托管应用程序的远程计算机
SSH_PORT — your remote machine’s SSH port
SSH_PORT —远程计算机的SSH端口
Next, go to hello-web-app repository’s Actions tab and click the I understand my workflows, go ahead and run them button (if you do, in fact, understand them). Once that Action is finished, head over to the reverse-proxy repository to kick off the workflow (make sure to add the above secrets first).
接下来,转到hello-web-app存储库的“操作”选项卡,然后单击“ 我了解我的工作流程,继续并运行它们”按钮(如果确实如此,则请理解它们)。 完成该操作后,转到反向代理存储库以启动工作流(确保首先添加以上秘密)。
Once reverse-proxy finishes deploying, navigate to https://your-remote-host/hello/ , and you should see a greeting!
一旦反向代理完成部署,请导航至https:// your-remote-host / hello / ,您应该会看到问候!
结论 (Conclusion)
That’s it! Now you have multiple Docker Compose applications being continuously deployed to your remote host, securely and with run-of-the-mill server tools like Ansible and SSH. It’s a simple setup (no automated rollbacks, no load balancing across horizontally-scaled services, and so on), but anyone with some Linux sysadmin experience can understand in an hour (I hope) how everything works together. Often, that’s more important.
而已! 现在,您具有多个Docker Compose应用程序,这些应用程序正在通过安全的运行工具(如Ansible和SSH)安全地连续部署到远程主机。 这是一个简单的设置(没有自动回滚,没有在水平扩展的服务之间进行负载平衡,等等),但是任何具有Linux sysadmin经验的人都可以在一个小时内(我希望)了解所有功能如何协同工作。 通常,这更重要。