在AWS spot实例通过 Github Runner使得CI更灵活

Blog
Author:
Omer MishaniaOmer Mishania
Published On:
3月 9, 2022
Estimated reading time:
2 minutes

本文介绍如何在 AWS spot实例上设置 GitHub Actions 自托管运行器。但在此之前,我们先来聊聊什么是自托管 GitHub 运行器,为什么我们应该用它。

什么是 GitHub Actions 运行器?

GitHub Actions 运行器是一种在机器上运行 GitHub Actions 工作流中作业的应用程序。
您可以使用在 GitHub 服务器上运行的现成 GitHub 托管运行器(WindowsUbuntu MacOSX,每个都有特定硬件),也可以使用您自己环境中的自托管运行器。

GitHub 运行器可用于:

  • 编译
  • 运行测试
  • 静态代码分析 (SCA)
  • 构建容器图片

等等!

我们之前在这篇文章中讨论过如何在 C++ 项目中使用 GitHub 操作,并且还在这篇文章中讨论过如何将其与 Incredibuild 结合使用。今天,我们将重点介绍如何使用 GitHub 操作来触发 AWS 竞价实例上的操作。

为什么使用自托管运行器?

如果您想要快速轻松地运行工作流,那么 GitHub 托管运行器会是一个非常好的选择。但是,在某些情况下,自托管是您的最佳选择(与 GitHub 托管运行器相反):

  • 您正在使用一个私有仓库,并且不希望在运行作业的每一分钟都要花钱。
  • 您正在运行一个复杂作业,并且所用机器的硬件需要优于 GitHub 托管运行器的机器硬件

您可以在 GitHub 文件中找到关于自托管运行器的更多信息。

什么是 AWS 竞价实例?

AWS 提供了各种可在云上部署的实例类型,例如按需实例、预留实例和竞价实例。

这三种实例类型在运行时都具有相同的功能,而定价是唯一的区别。按需实例是没有长期承诺的实例,您必须支付一定的每秒费率。预留实例是在特定时间段内以低于按需实例的每秒费率租用的实例。

与前两个实例不同,竞价实例使用备用 AWS 容量,并且价格是这些实例类型中最低的(比按需价格低 90%)。 然而,这里有一个问题。由于您正在使用未使用的资源,您的实例可能会被中断(带有 2 分钟通知)。

尽量避免将竞价实例用于包含敏感信息的有状态应用程序、数据库或工作负载。

对于应用测试、持续集成、数据分析、图像渲染等运行时间短且可中断的无状态应用,竞价实例是理想的选择。

那我们该怎么办?

在此例中,工作流将使用 Flake8 Pylint 验证仓库中 Python 文件的语法(当然,任何其他工作流也可以)。

您需要设置一个 AWS 竞价实例来运行 GitHub Actions 自托管运行器,并将其配置为在 AWS 将其中断时重新部署,从而以最低成本提高持续集成 (CI) 作业的灵活性。

该自托管运行器将执行工作流作业。

用于部署竞价实例的所有文件都能在 GitHub 仓库 aws-files 目录下找到。在此仓库中,还能找到 GitHub Actions 工作流示例。

预排先决条件:

创建和配置竞价实例

创建并正确配置竞价实例时,只需要使用 3 个文件。

user-data.sh:一个 Bash 脚本文件,包含将在启动时在实例上运行的命令。在我们的例子中,脚本将在已部署的每个实例上运行,甚至是在用于替换中断的竞价机器的新实例上运行。该文件中包含一个脚本,用于安装和配置自托管运行器并启动其服务。(有关 user-data.sh 文件的更多信息,请参阅 AWS 文件)。

#!/bin/bash

github-user="Your GitHub Username"
github-repo="Your GitHub Repository name"
PAT="Your Super Secret PAT"

# Download jq for extracting the Token
yum install jq -y

# Create and move to the working directory
mkdir /actions-runner && cd /actions-runner

# Download the latest runner package
curl -o actions-runner-linux-x64-2.286.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.286.1/actions-runner-linux-x64-2.286.1.tar.gz

# Extract the installer
tar xzf ./actions-runner-linux-x64-2.286.1.tar.gz

# Change the owner of the directory to ec2-user
chown ec2-user -R /actions-runner

# Get the runner's token
token=$(curl -s -XPOST -H "authorization: token $PAT" https://api.github.com/repos/$github-user/$github-repo/actions/runners/registration-token | jq -r .token)

# Create the runner and start the configuration experience
sudo -u ec2-user ./config.sh --url https://github.com/<github-username>/$github-user --token $token --name "spot-runner-$(hostname)" --unattended

# Create the runner's service
./svc.sh install

# Start the service
./svc.sh start

spot-instance-launch-template.json:一个 JSON 文件,描述竞价实例的启动模板配置(ImageTypeSecurityGroups 等)。出于我们的目的,它还将包含以 base64 编码的 user-data.sh 文件内容。

{
  "ImageId": "ami-001089eb624938d9f",
  "InstanceType": "t2.micro",
  "KeyName": "instance key pair name",
  "SecurityGroups": ["security group name"],
  "UserData": "user-data.sh file content encoded in base64",
  "InstanceMarketOptions": {
      "MarketType": "spot",
      "SpotOptions": {
      "MaxPrice": "0.03",
      "SpotInstanceType": "one-time",
      "InstanceInterruptionBehavior": "terminate"
      }
  }
}

spot-instance-auto-scaling-group.json:一个 JSON 文件,描述竞价实例的自动缩放组配置,比如要使用的启动模板、最小和最大实例数量等。在我们的场景中,自动缩放实际上用于创建中断竞价机器的替代方案,而不是用于动态灵活增长。

这一点很重要,因为通过使用自动缩放组,如果一个竞价实例被中断并且 AWS 终止了该实例,另一个具有相同配置的竞价实例将自动部署。

{
    "AutoScalingGroupName": "spot-instance-asg",
    "LaunchTemplate": {
    "LaunchTemplateName": "spot-instance-launch-template"
    },
    "MinSize": 1,
    "MaxSize": 1,
    "AvailabilityZones": ["your Availability Zones"]
}

好,我们开始部署

克隆仓库并将目录更改为基本文件目录

git clone https://github.com/omermishania/github-runner-on-aws-spot.git && cd github-runner-on-aws-spot/aws-files/
 

创建一个 AWS 安全组
使用以下规则创建安全组:
入站 – SSH(端口 22
出站 – HTTPS(端口 443

创建 user-data.sh 文件
# 配置文件:

用您最喜欢的文本编辑器编辑文件,并在以下情况下将变量值更改为您需要的值:

    • Github-user = 您的 GitHub 用户名
    • github-repo = 您的 GitHub 仓库名
    • PAT = 您的 GitHub PAT(个人访问令牌)
vim user-data.sh

例如:

…
github-user="omermishania"
github-repo="github-runner-on-aws-spot"
PAT="MY-SECRET-GITHUB-PAT"

完成后,保存并退出文件

# base64 user-data.sh 文件内容进行编码

cat user-data.sh | base64 -w 0

# 复制编码内容,将其保存在安全的地方。

 

创建 spot-instance-launch-template.json 文件
#
 配置文件

编辑文件并做如下变更:

    • ‘ImageId’ 值变更为您选择的 EC2 图像 ID
      您可以在图像名旁边找到 EC2 图像 ID(在红框中):
    • ‘InstanceType’ 值变更为您想要的实例类型
    • ‘SecurityGroups’ 值变更为含有您在几分钟前创建的安全组名称的列表
    • ‘UserData’ 值变更为您复制的 base64 编码内容
    • ‘MaxPrice’ 值变更为竞价实例的最高价格
vim spot-instance-launch-template.json

例如:

{
  "ImageId": "ami-001089eb624938d9f",
  "InstanceType": "t2.micro",
  "SecurityGroups": ["gh-runner-spot-sg"],
  "UserData":
"IyEvYmluL2Jhc2gKCgojIERvd25sb2FkIGpxIGZvciBleHRyYWN0aW5nIHRoZSBUb2tlbgp5dW0gaW5zdGFsbCBqcSAteQoKIyBDcmVhdGUgYW5kIG1vdmUgdG
8gdGhtUiAvYWN0aW9ucy1ydW5uZXIKCiMgR2V0IHRoZSBydW5uZXIncyB0b2tlbgpQQVQ9ImdocF81SmxTU3V16YXc2ggaW5zdGFsbAplY2hvICJzdmMgaW5zdG
FsbGVkIiA+PiB0ZXN0LmxvZwoKIyBTdGFydCB0aGUgc2VydmljZQouL3N2Yy5zaCBzdGFydAplY2hvICJzdmMgc3RhcnRlZCIgPj4gdGVzdC5sb2cK",
  "InstanceMarketOptions": {
      "MarketType": "spot",
      "SpotOptions": {
      "MaxPrice": "0.03",
      "SpotInstanceType": "one-time",
      "InstanceInterruptionBehavior": "terminate"
      }
  }
}

# 运行命令:完成后,保存并退出文件。

确保您仅在粘贴用 base64 编码的 user-data.sh 内容后才运行此命令

aws ec2 create-launch-template --launch-template-name spot-instance-launch-template --launch-template-data file://spot-instance-launch-template.json

vim spot-instance-auto-scaling-group.json

创建 spot-instance-auto-scaling-group.json 文件
配置文件

编辑文件,并将 ‘AvailabilityZones’ 值变更为含有所需可用性区域的列表。如果想要部署多个运行器,那么您还可以更改最小和最大实例数。

vim spot-instance-auto-scaling-group.json

完成后,保存并退出文件

# 运行命令

aws autoscaling create-auto-scaling-group --cli-input-json file://spot-instance-auto-scaling-group.json

恭喜!您已经在您的竞价实例上创建好了一个自托管运行器!

配置 GitHub Actions 工作流,以使用自托管运行器

我们的全新自托管运行器已启动并运行!
但是,GitHub Actions 工作流仍然不会使用它。为此,我们需要编辑工作流文件并将运行值更改为自托管

例如,我使用过下列工作流来检验有效的 Python 语法。您的工作流可以用于您想要的任何目的,您只需将运行字段的值更改为自托管(标记为蓝色):

name: Python Linting
on:
  push:
  pull_request:
jobs:
  lint-python-code:
       runs-on: self-hosted
       steps:
       - uses: actions/checkout@v2
       - name: Install dependencies
       run: |
       python3 -m pip install --upgrade pip
       pip3 install flake8
       pip3 install pylint
       if [ -f requirements.txt ]; then pip3 install --target=/usr/bin -r requirements.txt; fi
       - name: Lint with flake8
       run: |
      /usr/local/bin/flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics
      /usr/local/bin/flake8 src --count --max-complexity=10 --max-line-length=88 --statistics
      - name: Lint with Pylint
      run: |
      /usr/local/bin/pylint src

就是这样!

我们来回顾一下到目前为止我们所做的事情:

  • 创建和配置了一个自修复竞价实例。
  • 在该实例上配置了一个 GitHub 操作运行器。
  • 配置了要使用该运行器的 GitHub Actions 工作流。

现在,该工作流在云的竞价实例中新创建的 GitHub 运行器上运行,如果竞价实例被中断和终止,将创建一个带有新的 GitHub 运行器的新实例。

结论

通过在您的云环境中的竞价实例上使用 GitHub 自托管运行器,您可以灵活地使用任何自定义配置和工具来完全满足您的持续集成目的,甚至可以节省不必要的成本。

在我们的配置中,竞价实例需要处于活动状态才能侦听 GitHub 运行器命令。因此,我们实际上是为持续运行的竞价实例付费(尽管竞价关税很低)。我们可以考虑另一种可能的配置,也就是使用 GitHub 托管运行器只触发竞价实例,并让它完成工作。这样,我们将只在调用和运行 GitHub 托管运行器时付费,并且只在有限的时间内为竞价实例付费。但是,这需要一个更复杂的设置,超出了本文的范围。

为了实现持续集成构建的竞价实例的自动灵活性,而无需手动管理它们,您可以使用 Incredibuild。有关如何使用的内容, 咱们下期继续讨论!