学习自动扩展AWS中的自我托管运行程序

130 阅读15分钟

自我托管的运行程序允许你在你的私有云或企业内部托管你自己的可扩展的执行环境,让你更灵活地定制和控制你的CI/CD基础设施。有独特安全或计算要求的团队可以在五分钟内设置并开始使用自我托管的运行器。设置完成后,您的团队可以使用CircleCI云平台上的一系列流行功能,包括并行性和测试分割,用SSH调试,以及直接在CircleCI用户界面上管理自我托管的运行器。

大多数团队在整个工作日都会经历资源需求的波动,维持未使用的计算能力会导致不必要的成本。为了防止这种情况,你可以实施一个扩展解决方案,根据队列中作业的数量自动旋转和拆除自我托管的运行器,让你按需访问你所需要的计算能力,而不会有在闲置资源上浪费金钱的风险。在本教程中,您将学习如何使用AWS自动扩展组(ASG)为CircleCI的自托管运行器设置一个基本的自动扩展解决方案。

自我托管的运行器 - 在您完全控制下的执行环境

自我托管的运行器提供了一个完全可定制的执行环境。当使用自我托管的运行器时,CircleCI将作业发送到您的计算,以执行所需的CI/CD步骤。如果您的应用程序需要访问内部数据库或敏感资源以进行适当的测试,您可以将其部署到您的防火墙后面的自我托管的运行器。

Self-hosted runners diagram

运行器被设计成尽可能地易于配置、管理和部署。下面的插图提供了与你将在本教程中实施的解决方案有关的运行器行为的一些细节。关于自我托管的运行程序的更多信息可在常见问题运行程序概念页面上找到。

  • 资源类:自托管的运行程序被分组为唯一命名的resource classes ,用于识别和分配作业。所有自我托管的运行器资源类必须有一个唯一的名称,以确保运行器能够被正确识别,并在CircleCI网络界面内进行管理。
  • 计算的一致性:最好的做法是在资源类中保持自托管运行器的底层计算一致--每台机器应以相同的架构和环境进行配置。这可以防止任何意外行为,并使故障排除更容易。在本教程中,我们将使用EC2模板来确保运行器代理的配置是统一的。
  • 工作排队:如果没有运行器,CircleCI作业将等待,直到所需资源类的自我托管运行器可用。如果你已经实施了自动扩展解决方案,这将使你的自我托管运行器有时间启动
  • 连接要求:运行器轮询CircleCI的新作业,所以不需要从互联网上传入连接。这意味着你不需要配置任何传入防火墙规则或端口转发,运行器也不需要一个静态的公共IP地址。

现在你已经熟悉了使用CircleCI的自托管运行器的基本知识,让我们开始学习教程。

使用AWS自动扩展组自动扩展自我托管的运行器

这个例子实现演示了如何使用AWS自动扩展功能来扩展自托管的运行器,以根据需求增加和减少可用的自托管运行器的数量。

示例库包括一个基本的Node.js应用程序和一个CircleCI管道配置来测试它。为了执行这个CircleCI管道,你将设置一个自我托管的运行器作为基于Ubuntu的AWS EC2启动模板。该启动模板和自动扩展组将用于根据运行器API为给定的运行器资源类提供的队列深度(队列中的作业数)值来启动实例--所有这些都由定期检查API的Lambda函数触发。

Runners with AWS Auto Scaling

在CircleCI中设置一个运行器资源类

第一步是创建一个资源类。你可以在CircleCI用户界面上点击一下就完成了。

Create custom resource class

一旦你创建了资源类,请注意为其生成的认证令牌。**它将不会再次显示。**在下一步,您将需要这个令牌来验证CircleCI的自我托管运行器。

准备自我托管的运行器的安装脚本

接下来,您将需要创建一个安装脚本,以便在AWS中自动安装和配置自我托管的运行程序。当AWS实例从下一步创建的模板中启动时,这个Bash脚本将在它完成启动后作为根用户被调用。

使用上一步的资源类和令牌,更新脚本模板中的以下变量。

  • RUNNER_NAME。可以是任何你想给你的运行器的字母数字名称,因为它将出现在CircleCI UI中。
  • AUTH_TOKEN:应替换为创建资源类时在用户界面中出现的资源类令牌。

然后,你将需要添加步骤来安装任何你想在作业运行时作为执行环境一部分的依赖或包。这必须在运行器服务被启用和启动之前在脚本中完成。

例如,如果你正在开发和测试一个Node.js应用程序,你将想在脚本中添加步骤来安装Node.js。

# aws_config/install_runner_ubuntu.sh

#------------------------------------------------------------------------------
# Configure your runner environment
# This script must be able to run unattended - without user input
#------------------------------------------------------------------------------
apt install -y nodejs npm

由于该脚本将在启动时为在自动缩放组中创建的每个实例执行,它必须能够在无人看管的情况下运行(没有用户输入)。

在安装脚本中,注意CircleCI运行器的短(1m )idle_timeout的时间。这有助于缩减自我托管的运行器和不再需要的实例的规模。

# aws_config/install_runner_ubuntu.sh

#------------------------------------------------------------------------------
# Install the CircleCI runner configuration
# CircleCI Runner will be executing as the configured $USERNAME
# Note the short idle timeout - this script is designed for auto-scaling scenarios - if a runner is unclaimed, it will quit and the system will shut down as  defined in the below service definition
#------------------------------------------------------------------------------

cat << EOF >$CONFIG_PATH
api:
 auth_token: $AUTH_TOKEN
runner:
 name: $UNIQUE_RUNNER_NAME
 command_prefix: ["sudo", "-niHu", "$USERNAME", "--"]
 working_directory: /opt/circleci/workdir/%s
 cleanup_working_directory: true
 idle_timeout: 1m
 max_run_time: 5h
 mode: single-task
EOF

并注意到相关服务ExecStopPost 设置中的关闭命令。

# aws_config/install_runner_ubuntu.sh

#------------------------------------------------------------------------------
# Create the service
# The service will shut down the instance when it exits - that is, the runner  has completed with a success or error
#------------------------------------------------------------------------------

cat << EOF >$SERVICE_PATH
[Unit]
Description=CircleCI Runner
After=network.target
[Service]
ExecStart=$prefix/circleci-launch-agent --config $CONFIG_PATH
ExecStopPost=shutdown now -h
Restart=no
User=root
NotifyAccess=exec
TimeoutStopSec=18300
[Install]
WantedBy = multi-user.target
EOF

这可以确保任何没有认领工作的空闲运行器和任何已经完成任务的运行器将被迅速终止,以避免浪费资源。

配置服务时,如果需要修改,请参考systemd文档。前面的例子中,如果服务运行时间超过5小时(18300秒),将终止服务,与运行器的max_run_time

创建启动模板

登录AWS管理控制台,导航到管理EC2的服务页面。你将需要创建一个启动模板,并像这样填写字段。

  • 给你的新模板起个合理的名字,如cci-runner-template。
  • 选择 "提供指导,帮助我建立一个可以使用EC2自动缩放的模板 "的复选框。
  • 对于启动模板内容AMI,选择快速启动,然后选择Ubuntu 22.04 LTS
  • 选择一个实例类型--你需要根据你的要求挑选一个。
  • 选择一个用于登录的Key pair 。当你需要通过SSH登录来排除一个实例的故障时,这很有帮助。
  • 对于网络设置和安全组,选择一个现有的安全组或创建一个。明智的做法是只允许SSH来自一个受信任的IP地址,并阻止所有其他传入的流量。
    • 自我托管的运行器会轮询CircleCI的新作业,不需要任何传入的连接。
  • 在高级网络配置中,点击添加网络接口,并启用自动分配公共IP给该接口。
  • 配置存储 - 如果你认为你需要的话,增加每个实例的硬盘大小。
  • 对于 "高级 "的细节,将 "高级 "的内容全部复制并粘贴到 "用户数据 "中。 install_runner_ubuntu.sh的全部内容粘贴到用户数据字段中。这个字段的内容将在实例启动时作为一个shell脚本执行
  • 其他都可以保持默认值。

请注意,资源类认证令牌存储在启动模板中(作为运行器安装脚本的一部分),所以不要分享它

创建自动扩展组

接下来,你将需要创建一个自动扩展组。为每个部分输入这些值。

  • 第1步:选择启动模板或配置。
    • 给你的组起个合理的名字,比如cci-runner-auto-scaling-group
    • 确保你创建的模板被设置为Launch template
    • 其他都保持原样。
  • 第2步:选择实例启动选项。
    • 对于实例启动选项,选择一个可用性区域和子网。如果你的实例需要与其他AWS资产通信,请将它们分配到适当的区域/子网。
    • 让其他一切保持原样。
  • 第3步:配置高级选项。
    • 让一切都保持原样。
  • 第4步:配置组大小和缩放策略。
    • 将期望容量、最小容量和最大容量设置为0。在随后的步骤中创建的Lambda函数将更新这些值以符合您的扩展要求。
    • 选择启用扩展保护复选框,以保护实例不被过早终止。工作可能不按提交顺序完成,减少队列深度,导致自动扩展组终止较早的实例--即使它们可能不是已经完成任务的自我托管运行者。
    • 让其他一切保持原样。
  • 跳过步骤5和6。
  • 第7步:审查。
    • 查看你的配置,并保存它。

一旦启动,实例将负责自己的生命周期。自主运行器将根据idle_timeout flag ,在短暂的空闲时间后终止。由于运行器处于单任务模式,自主运行器也将在完成任务(成功或失败)后优雅地终止。我们还配置了服务,使其在退出时关闭实例。

创建IAM策略和角色

Lambda函数需要有监控队列和改变自动扩展参数的权限。你需要设置一个身份和访问管理(IAM)策略和相关角色来授予这些权限。

创建一个具有所需权限的策略。你可以从我们的示例库中的aws_config/lambda_iam.json文件或下面的代码块中复制并粘贴该策略。该策略将赋予更新自动扩展组和从AWS秘密管理器中读取秘密的权限--这是Lambda函数所需的两个权限。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "autoscaling:UpdateAutoScalingGroup",
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": "*"
        }
    ]
}

一旦策略被设置好,为你的Lambda函数创建一个新的角色,并将新的策略分配给该角色。

同样,保持命名的一致性,以便你以后可以轻松地找到和识别AWS组件。给IAM组件起一个合理的名字,如:cci-runner-lambda-iam-policycci-runner-lambda-iam-role

创建(和保持)秘密

AWS的秘密管理器提供了一种安全的方式来存储API密钥和其他敏感信息,以便在Lambda函数中使用。秘密被存储为键/值对。用这些选项创建一个秘密

  • 第1步:选择秘密类型。
    • 选择其他类型的秘密
    • 添加以下键/值对。
      • resource_class_: 以用户名/类名的格式为CCI中的运行者提供资源类。
      • circle_token_: 这将是一个用于轮询运行器API的CircleCI个人令牌--它不是上面安装脚本中使用的运行器令牌。
    • 将加密密钥设置为aws/secretsmanager
  • 第2步:配置你的秘密。
    • 给你的秘密起个合理的名字,比如ci-runner-lambda-secrets。
    • 其他部分保持原样。
  • 第3步:配置旋转 - 可选。
    • 保持原样。
  • 第4步:审查。
    • 审查并保存你的秘密。
    • 在这一点上不需要复制和粘贴生成的代码--它已经包含在仓库中的Lambda函数示例中。请确保你记下了秘密的名称和区域。

创建Lambda函数

AWS Lambda函数是执行代码的无服务器函数。这个例子使用一个按时间表触发的Lambda函数来运行一个Python脚本,该脚本检查CircleCI runner API并改变自动缩放组以提高或降低运行实例的数量。用以下配置从头开始创建一个Lambda函数

  • 给你的函数起一个合理的名字,如cci-runner-lambda-function
  • 将运行时间设置为Python 3.8
  • 将架构设置为x64_64
  • 点击执行角色,然后使用现有角色。选择你之前创建的IAM角色。

复制并粘贴aws_config/lambda_function.py文件的内容到AWS控制台的函数源。

# aws_config/lambda_function.py

import json, urllib3, boto3, base64, os

# This script polls the queue depth for a Circle CI runner class, and sets the parameters for an AWS Auto Scaling group 
# It uses the CircleCI runner API https://circleci.com/docs/2.0/runner-api/
# It requires the included IAM role and should be triggered every minute using an EventBridge Cron event

# Retrieve environment variables
secret_name   = os.environ['SECRET_NAME'] 
secret_region = os.environ['SECRET_REGION']
auto_scaling_max = os.environ['AUTO_SCALING_MAX'] 
auto_scaling_group_name = os.environ['AUTO_SCALING_GROUP_NAME']
auto_scaling_group_region = os.environ['AUTO_SCALING_GROUP_REGION']

# Function to retrieve secrets from AWS Secrets manager
# https://aws.amazon.com/secrets-manager/
def get_secret(secret_name, region_name):
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    get_secret_value_response = client.get_secret_value(
        SecretId=secret_name
    )
  
    if 'SecretString' in get_secret_value_response:
        secret = get_secret_value_response['SecretString']
        return(secret)
    else:
        decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary'])
        return(decoded_binary_secret)

# Make a HTTP GET request with the given URL and headers
def get_request(url, headers):
    http = urllib3.PoolManager()
    r = http.request('GET', url, headers=headers)
    r_json = json.loads(r.data.decode("utf-8"))
    return(r_json)

# The handler function - executed every time the Lambda function is triggered
def lambda_handler(event, context):
    # Get secrets
    secrets = json.loads(get_secret(secret_name, secret_region))
    
    # Configure Runner API endpoint https://circleci.com/docs/2.0/runner-api/#endpoints
    endpoint_url = 'https://runner.circleci.com/api/v2/tasks?resource-class=' + secrets['resource_class']
    headers = {'Circle-Token': secrets['circle_token']}

    # Get result from API endpoint
    result = get_request(endpoint_url, headers)

    # Update the auto scaling group with a desired number of instances set to  the number of jobs in the queue, or the maximum, whichever is smallest
    instances_min = 0
    instances_max = int(auto_scaling_max)
    instances_desired = int(result["unclaimed_task_count"]) if int(result["unclaimed_task_count"]) < int(auto_scaling_max) else int(auto_scaling_max)
    
    # Set the Auto Scaling group configuration
    client = boto3.client('autoscaling', region_name=auto_scaling_group_region)
    client.update_auto_scaling_group(
        AutoScalingGroupName=auto_scaling_group_name,
        MinSize=instances_min,
        MaxSize=instances_max,
        DesiredCapacity=instances_desired
    )  

    # Lambda functions should return a result, even if it isn't used
    return result["unclaimed_task_count"]

在Lambda函数的配置标签下,导航到环境变量,然后编辑并添加以下键/值对。

  • SECRET_NAME:上面创建的秘密的名称。
  • SECRET_REGION:上面的秘密的区域。
  • AUTO_SCALING_MAX:要旋转的最大实例数,以整数表示。
    • 我们建议将最大值设置为您的CircleCI计划的自我托管运行器并发数限制
  • AUTO_SCALING_GROUP_NAME:自动扩展组的名称。
  • AUTO_SCALING_GROUP_REGION:自动缩放组的区域。

其他都保持默认值。

按计划触发Lambda函数

Lambda函数可以通过多种方式被触发。在这种情况下,它将在一个时间表上执行。我们建议每分钟调用该函数,以检查队列深度,并及时做出适当的调整。

要设置这一点,进入Lambda函数编辑界面,点击添加触发器。搜索并选择EventBridge(CloudWatch事件),然后选择创建一个新规则。填写以下细节。

  • 给你的规则起个合理的名字,比如cci-runner-scheduled-trigger
  • 将规则类型设置为时间表表达式
  • 输入值cron(0/1 * * * ? *) ,以便每分钟触发该功能。

点击"添加",完成预定触发器的设置。

测试和部署

就这样,你的自动缩放运行器解决方案的所有活动部件都到位了。现在你可以把它添加到你的CircleCI配置中并开始使用它了

在Lambda函数编辑界面,返回到代码选项卡并点击部署。这就是了!现在一切都在运行,可以使用了。

要测试,请到测试标签。让一切保持原样(以防止测试被保存)。点击 "测试"。

结果将是成功或失败,如果有必要,你将能够调试你的函数代码。如果一切显示为绿色,你就可以开始运行了。

如果你想监控你的函数,你可以使用Lambda中的监控选项卡,以确保你的函数是按照你在上一节中设置的时间表运行。

在自动扩展的自我托管的运行器上运行CircleCI作业

要在你新的自动缩放的资源类上运行CircleCI作业,你首先需要将资源类添加到你的CircleCI配置文件中。

样本库中的.circleci/config.yml文件使用machineresource_class 选项。

# .circleci/config.yml

version: 2.1

workflows:
  testing:
    jobs:
      - runner-test

jobs:
  runner-test:
    machine: true
    resource_class: NAMESPACE/-NAMENAME # Update this to reflect your self-hosted runner resource class details 
    steps:
      - run: echo "Hi I'm on Runners!"
      - run: node --version
      - checkout
      - run:
          command: npm install
          name: Install Node.js app dependencies
      - run:
          command: npm run test
          name: Test app

一旦你运行了一个作业,自我托管的运行器在启动时就会出现在CircleCI网络界面中。当一个作业出现在队列中时,AWS Lambda函数将触发自动扩展组来增加其容量。当实例准备好后,作业将从CircleCI发送到运行器上执行。

New runners resource class

您可以通过CircleCI的Web界面监控自我托管的运行器的状态。当它完成后,运行器将终止它所运行的实例,而自动扩展组将减少所需的实例数量,以匹配新的队列长度。

node.js-circleci-runner

您的流水线的结果将与您的CircleCI云作业一起被送回CircleCI UI。

Successful parallel test runs

在AWS方面,你将能够看到Lambda函数随着队列深度的变化和作业的完成而调整自动扩展组。

AWS - auto-scaling groups

CircleCI是一个灵活的CI/CD平台,以您的方式工作

通过自我托管的运行器,您可以完全控制您的CI/CD管道,包括执行环境和您的数据被存储和处理的地方。

CircleCI鼓励DevOps的最佳实践 - 但它不会规定您应该如何做事。您需要能够根据您的团队的优势,以您的工具链所允许的充分灵活性来工作,同时保持合规和安全。您可以开始使用预构建的执行环境,当您的需求变得更加专业时,可以部署您自己的定制的、可扩展的自我托管的运行器,并通过简单的配置更改开始使用它们--而无需彻底改变您的整个CI/CD工具链。

您今天就可以通过注册一个免费计划来开始使用CircleCI。您的免费计划提供了您所需要的一切,开始建立您自己的自动化CI/CD管道,以测试和部署您的代码。自我托管的运行器包含在所有计划中,当您准备开始实验CircleCI所提供的高级功能时,您可以使用。