cdk-github-runners

repository·main·Indexed 18 days ago

https://github.com/cloudsnorkel/cdk-github-runners

AWS CDK constructs for deploying ephemeral, on-demand GitHub self-hosted runners using EC2, CodeBuild, ECS, Fargate, and Lambda. Features include composite providers with fallback and weighted distribution strategies, custom image components for pre-installed software, per-job runner hooks, and support for GitHub Enterprise Server (GHES) in a VPC.

Tokens
77.2K
Snippets
253
Records
332
Agent score
63%

What's inside cdk-github-runners

  1. Overview of GitHub Self-Hosted Runners CDK Constructs

    main

    This project provides AWS CDK constructs to create ephemeral, on-demand self-hosted GitHub runners within your AWS account. These runners are started only when a job is running, ensuring you only pay for active compute time.

    Key features include:

    • Web-based interface for easy GitHub integration.
    • Customizable runner environments with sensible defaults.
    • Support for multiple runner configurations via labels.
    • Fully hosted within your own AWS account.
    • Automatic updates to the latest runner versions.

    Ephemeral runners are recommended by GitHub for auto-scaling and ensuring every job runs in a clean, isolated environment.

  2. Use GitHub Actions runner hooks for per-job tasks

    main

    GitHub Actions self-hosted runners support running scripts before or after every job via environment variables. This is a portable mechanism supported across all providers (EC2, ECS, Fargate, CodeBuild, Lambda).

    Supported Hooks

    • Before a job: Set the ACTIONS_RUNNER_HOOK_JOB_STARTED environment variable to the path of your script.
    • After a job: Set the ACTIONS_RUNNER_HOOK_JOB_COMPLETED environment variable to the path of your script.

    Use Cases

    Use hooks for tasks that must occur once per job, such as:

    • Logging job metadata.
    • Sending notifications.
    • Preparing or cleaning the workspace.
    • Gating jobs.

    Note: If you need to install software or run a persistent service on the runner itself, do not use hooks. Instead, bake the software into the runner image or configure a systemd unit during the image build.

  3. Configure ICompositeProvider labels for runner selection

    main
    The labels property on an ICompositeProvider defines the GitHub Actions labels used by that provider. When a GitHub Actions job sends a webhook with runs-on labels, the system matches those against the labels specified here. If all labels in the provider's list are present in the job's labels, this provider is selected to spawn a new runner.
  4. Avoid using `instanceof` for Construct type-testing

    main

    In JavaScript environments where multiple copies of the constructs library might exist (e.g., due to manual symlinking or certain monorepo tool configurations), instanceof checks against the Construct class may fail even if the object is a valid construct.

    To ensure reliability, avoid using instanceof and instead use the project's provided type-testing methods (like isConstruct) to verify objects.

  5. Storage volume types for EC2 runners

    main

    When configuring EC2 runners, you can choose between different EBS volume types to balance cost and performance:

    • GP3: General purpose SSD. Offers configurable IOPS and throughput; provides the best price/performance.
    • GP2: General purpose SSD. Provides baseline performance.
    • IO1: Provisioned IOPS SSD. Designed for the highest performance in I/O-intensive workloads.
  6. Customize images with CodeBuildImageBuilder

    main

    The CodeBuildImageBuilder is an implementation of IRunnerImageBuilder that uses AWS CodeBuild to build Docker images pre-baked with GitHub Actions runner requirements. These builders can be passed to runner providers to customize the runner's environment. Builders automatically re-run at a specified rebuildInterval to keep images up to date.

    const builder = new CodeBuildImageBuilder(this, 'Builder', {
        dockerfilePath: FargateRunnerProvider.LINUX_X64_DOCKERFILE_PATH,
        runnerVersion: RunnerVersion.specific('2.293.0'),
        rebuildInterval: Duration.days(14),
    });
    builder.setBuildArg('EXTRA_PACKAGES', 'nginx xz-utils');
    new FargateRunnerProvider(this, 'Fargate provider', {
        labels: ['customized-fargate'],
        imageBuilder: builder,
    });
  7. Use Ec2RunnerProvider to provide EC2-based runners

    main

    The Ec2RunnerProvider is a GitHub Actions runner provider that uses EC2 instances to execute jobs.

    Important: This construct is not intended to be used in isolation. It must be passed into the providers property of a GitHubRunners construct.

    It implements the IRunnerProvider interface and provides the necessary logic to generate Step Function tasks for starting new runners.

    import { Ec2RunnerProvider } from '@cloudsnorkel/cdk-github-runners'
    
    // Note: This must be passed to GitHubRunners providers property
    new Ec2RunnerProvider(scope, id, props);
  8. Choose a Runner Registration Level

    main

    The registration level determines where on-demand runners are dynamically registered during provisioning. This is independent of where the GitHub App is installed.

    • Repository-level (Recommended): Runners are registered to specific repositories. This provides better isolation and reduces the risk of jobs being assigned to the wrong runners. Requires the administration permission.
    • Organization-level: Runners are registered to the entire organization and are available to all repositories in the organization, regardless of app installation. This requires the organization_self_hosted_runners permission but carries a higher risk of accidental job routing.

    Decision Guide:

    • Use Repository-level for better security, isolation, and control.
    • Use Organization-level if you want to minimize permissions and fully trust all repositories in your organization.
  9. Use AmiBuilder to create custom runner images

    main
    The AmiBuilder construct allows you to automate the creation of Amazon Machine Images (AMIs) specifically tailored for your GitHub runners. This ensures that your runners boot up with all necessary software, dependencies, and configurations pre-installed.
  10. Understand the ICompositeProvider interface

    main

    The ICompositeProvider interface is used for composite runner providers that interact with multiple sub-providers. Unlike a standard IRunnerProvider, a composite provider delegates connections, capabilities, log groups, and retryable errors to its underlying sub-providers.

    Key responsibilities include:

    • Generating Step Function tasks via getStepFunctionTask (called internally by GithubRunners).
    • Merging constants from all sub-providers via stepFunctionConstants for the orchestrator's $.consts pass. Note: Duplicate keys across sub-providers must be avoided.
    • Providing status information for all sub-providers via status.
    • Optionally modifying the state machine role via grantStateMachine to add additional policy statements.
    public getStepFunctionTask(parameters: IRunnerRuntimeParameters): IChainable
    public grantStateMachine(stateMachineRole: IGrantable): void
    public status(statusFunctionRole: IGrantable): IRunnerProviderStatus[]
    public stepFunctionConstants(): {[ key: string ]: string}
  11. Implement GitHub Actions runner hooks

    main

    GitHub Actions self-hosted runners support running scripts before or after every job using specific environment variables. This is a portable mechanism that works across all providers (EC2, ECS, Fargate, CodeBuild, Lambda).

    To implement this using @cloudsnorkel/cdk-github-runners, you use the RunnerImageComponent to bake your scripts into the runner image and automatically configure the required environment variables:

    • ACTIONS_RUNNER_HOOK_JOB_STARTED: Executes a script before every job starts.
    • ACTIONS_RUNNER_HOOK_JOB_COMPLETED: Executes a script after every job completes.

    GitHub passes job context to these hooks via environment variables such as $GITHUB_REPOSITORY and $GITHUB_RUN_ID.

    Note: Use hooks for per-job tasks (e.g., logging metadata, notifications, workspace cleanup). For permanent software installation or long-running services, add them directly to the image or configure a systemd unit instead.

    // Conceptual usage of RunnerImageComponent hooks
    // The component copies the local script into the image and sets the env var
    runnerImageComponent.jobStartedHook('path/to/job-started.sh');
    runnerImageComponent.jobCompletedHook('path/to/job-completed.sh');