Skip to content

Commit 02b4a04

Browse files
neethu-pNeethu Pandhaplaviljervinpalm
authored
feat: Support GitHub Enterprise Cloud with Data Residency (github-aws-runners#4390)
Opening up the PR again to address the CI issue - github-aws-runners#4389 --------- Co-authored-by: Neethu Pandhaplavil <[email protected]> Co-authored-by: Jørgen Jervidalo <[email protected]> Co-authored-by: Niek Palm <[email protected]>
1 parent bbf8a4e commit 02b4a04

File tree

14 files changed

+360
-26
lines changed

14 files changed

+360
-26
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ This [Terraform](https://www.terraform.io/) module creates the required infrastr
2525
- Tailored software, hardware and network configuration: Bring your own AMI, define the instance types and subnets to use.
2626
- OS support: Linux (x64/arm64) and Windows
2727
- Multi-Runner: Create multiple runner configurations with a single deployment
28-
- GitHub cloud and GitHub Enterprise Server (GHES) support.
28+
- GitHub cloud, Github Cloud with Data Residency and GitHub Enterprise Server (GHES) support.
2929
- Org and repo level runners. enterprise level runners are not supported (yet).
3030

3131

@@ -140,7 +140,7 @@ Join our discord community via [this invite link](https://discord.gg/bxgXW8jJGh)
140140
| <a name="input_enable_userdata"></a> [enable\_userdata](#input\_enable\_userdata) | Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI. | `bool` | `true` | no |
141141
| <a name="input_eventbridge"></a> [eventbridge](#input\_eventbridge) | Enable the use of EventBridge by the module. By enabling this feature events will be put on the EventBridge by the webhook instead of directly dispatching to queues for scaling.<br/><br/> `enable`: Enable the EventBridge feature.<br/> `accept_events`: List can be used to only allow specific events to be putted on the EventBridge. By default all events, empty list will be be interpreted as all events. | <pre>object({<br/> enable = optional(bool, true)<br/> accept_events = optional(list(string), null)<br/> })</pre> | `{}` | no |
142142
| <a name="input_ghes_ssl_verify"></a> [ghes\_ssl\_verify](#input\_ghes\_ssl\_verify) | GitHub Enterprise SSL verification. Set to 'false' when custom certificate (chains) is used for GitHub Enterprise Server (insecure). | `bool` | `true` | no |
143-
| <a name="input_ghes_url"></a> [ghes\_url](#input\_ghes\_url) | GitHub Enterprise Server URL. Example: https://github.internal.co - DO NOT SET IF USING PUBLIC GITHUB | `string` | `null` | no |
143+
| <a name="input_ghes_url"></a> [ghes\_url](#input\_ghes\_url) | GitHub Enterprise Server URL. Example: https://github.internal.co - DO NOT SET IF USING PUBLIC GITHUB - github.com. However if you are using Github Enterprise Cloud with data-residency (ghe.com), set the endpoint here. Example - https://companyname.ghe.com | `string` | `null` | no |
144144
| <a name="input_github_app"></a> [github\_app](#input\_github\_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). | <pre>object({<br/> key_base64 = string<br/> id = string<br/> webhook_secret = string<br/> })</pre> | n/a | yes |
145145
| <a name="input_idle_config"></a> [idle\_config](#input\_idle\_config) | List of time periods, defined as a cron expression, to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle. | <pre>list(object({<br/> cron = string<br/> timeZone = string<br/> idleCount = number<br/> evictionStrategy = optional(string, "oldest_first")<br/> }))</pre> | `[]` | no |
146146
| <a name="input_instance_allocation_strategy"></a> [instance\_allocation\_strategy](#input\_instance\_allocation\_strategy) | The allocation strategy for spot instances. AWS recommends using `price-capacity-optimized` however the AWS default is `lowest-price`. | `string` | `"lowest-price"` | no |

docs/configuration.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ To be able to support a number of use-cases, the module has quite a lot of confi
1010
- Linux vs Windows. You can configure the OS types linux and win. Linux will be used by default.
1111
- Re-use vs Ephemeral. By default runners are re-used, until detected idle. Once idle they will be removed from the pool. To improve security we are introducing ephemeral runners. Those runners are only used for one job. Ephemeral runners only work in combination with the workflow job event. For ephemeral runners the lambda requests a JIT (just in time) configuration via the GitHub API to register the runner. [JIT configuration](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-just-in-time-runners) is limited to ephemeral runners (and currently not supported by GHES). For non-ephemeral runners, a registration token is always requested. In both cases the configuration is made available to the instance via the same SSM parameter. To disable JIT configuration for ephemeral runners set `enable_jit_config` to `false`. We also suggest using a pre-build AMI to improve the start time of jobs for ephemeral runners.
1212
- Job retry (**Beta**). By default the scale-up lambda will discard the message when it is handled. Meaning in the ephemeral use-case an instance is created. The created runner will ask GitHub for a job, no guarantee it will run the job for which it was scaling. Result could be that with small system hick-up the job is keeping waiting for a runner. Enable a pool (org runners) is one option to avoid this problem. Another option is to enable the job retry function. Which will retry the job after a delay for a configured number of times.
13-
- GitHub Cloud vs GitHub Enterprise Server (GHES). The runners support GitHub Cloud as well GitHub Enterprise Server. For GHES, we rely on our community for support and testing. We have no capability to test GHES ourselves.
13+
- GitHub Cloud vs GitHub Enterprise Server (GHES). The runners support GitHub Cloud (Public GitHub - github.com), GitHub Data Residency instances (ghe.com), and GitHub Enterprise Server. For GHES, we rely on our community for support and testing. We have no capability to test GHES ourselves.
1414
- Spot vs on-demand. The runners use either the EC2 spot or on-demand life cycle. Runners will be created via the AWS [CreateFleet API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateFleet.html). The module (scale up lambda) will request via the CreateFleet API to create instances in one of the subnets and of the specified instance types.
1515
- ARM64 support via Graviton/Graviton2 instance-types. When using the default example or top-level module, specifying `instance_types` that match a Graviton/Graviton 2 (ARM64) architecture (e.g. a1, t4g or any 6th-gen `g` or `gd` type), you must also specify `runner_architecture = "arm64"` and the sub-modules will be automatically configured to provision with ARM64 AMIs and leverage GitHub's ARM64 action runner. See below for more details.
1616
- Disable default labels for the runners (os, architecture and `self-hosted`) can achieve by setting `runner_disable_default_labels` = true. If enabled, the runner will only have the extra labels provided in `runner_extra_labels`. In case you on own start script is used, this configuration parameter needs to be parsed via SSM.

lambdas/functions/control-plane/src/pool/pool.test.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import nock from 'nock';
55

66
import { listEC2Runners } from '../aws/runners';
77
import * as ghAuth from '../github/auth';
8-
import { createRunners } from '../scale-runners/scale-up';
8+
import { createRunners, getGitHubEnterpriseApiUrl } from '../scale-runners/scale-up';
99
import { adjust } from './pool';
1010

1111
const mockOctokit = {
@@ -28,7 +28,7 @@ jest.mock('./../aws/runners', () => ({
2828
listEC2Runners: jest.fn(),
2929
}));
3030
jest.mock('./../github/auth');
31-
jest.mock('./../scale-runners/scale-up');
31+
jest.mock('../scale-runners/scale-up');
3232

3333
const mocktokit = Octokit as jest.MockedClass<typeof Octokit>;
3434
const mockedAppAuth = mocked(ghAuth.createGithubAppAuth, {
@@ -167,6 +167,12 @@ beforeEach(() => {
167167

168168
describe('Test simple pool.', () => {
169169
describe('With GitHub Cloud', () => {
170+
beforeEach(() => {
171+
(getGitHubEnterpriseApiUrl as jest.Mock).mockReturnValue({
172+
ghesApiUrl: '',
173+
ghesBaseUrl: '',
174+
});
175+
});
170176
it('Top up pool with pool size 2 registered.', async () => {
171177
await expect(await adjust({ poolSize: 3 })).resolves;
172178
expect(createRunners).toHaveBeenCalledTimes(1);
@@ -240,7 +246,29 @@ describe('Test simple pool.', () => {
240246

241247
describe('With GHES', () => {
242248
beforeEach(() => {
243-
process.env.GHES_URL = 'https://github.enterprise.something';
249+
(getGitHubEnterpriseApiUrl as jest.Mock).mockReturnValue({
250+
ghesApiUrl: 'https://api.github.enterprise.something',
251+
ghesBaseUrl: 'https://github.enterprise.something',
252+
});
253+
});
254+
255+
it('Top up if the pool size is set to 5', async () => {
256+
await expect(await adjust({ poolSize: 5 })).resolves;
257+
// 2 idle, top up with 3 to match a pool of 5
258+
expect(createRunners).toHaveBeenCalledWith(
259+
expect.anything(),
260+
expect.objectContaining({ numberOfRunners: 3 }),
261+
expect.anything(),
262+
);
263+
});
264+
});
265+
266+
describe('With Github Data Residency', () => {
267+
beforeEach(() => {
268+
(getGitHubEnterpriseApiUrl as jest.Mock).mockReturnValue({
269+
ghesApiUrl: 'https://api.companyname.ghe.com',
270+
ghesBaseUrl: 'https://companyname.ghe.com',
271+
});
244272
});
245273

246274
it('Top up if the pool size is set to 5', async () => {

lambdas/functions/control-plane/src/pool/pool.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import yn from 'yn';
55
import { bootTimeExceeded, listEC2Runners } from '../aws/runners';
66
import { RunnerList } from '../aws/runners.d';
77
import { createGithubAppAuth, createGithubInstallationAuth, createOctokitClient } from '../github/auth';
8-
import { createRunners } from '../scale-runners/scale-up';
8+
import { createRunners, getGitHubEnterpriseApiUrl } from '../scale-runners/scale-up';
99

1010
const logger = createChildLogger('pool');
1111

@@ -24,7 +24,6 @@ export async function adjust(event: PoolEvent): Promise<void> {
2424
const runnerGroup = process.env.RUNNER_GROUP_NAME || '';
2525
const runnerNamePrefix = process.env.RUNNER_NAME_PREFIX || '';
2626
const environment = process.env.ENVIRONMENT;
27-
const ghesBaseUrl = process.env.GHES_URL;
2827
const ssmTokenPath = process.env.SSM_TOKEN_PATH;
2928
const ssmConfigPath = process.env.SSM_CONFIG_PATH || '';
3029
const subnets = process.env.SUBNET_IDS.split(',');
@@ -43,10 +42,7 @@ export async function adjust(event: PoolEvent): Promise<void> {
4342
? (JSON.parse(process.env.ENABLE_ON_DEMAND_FAILOVER_FOR_ERRORS) as [string])
4443
: [];
4544

46-
let ghesApiUrl = '';
47-
if (ghesBaseUrl) {
48-
ghesApiUrl = `${ghesBaseUrl}/api/v3`;
49-
}
45+
const { ghesApiUrl, ghesBaseUrl } = getGitHubEnterpriseApiUrl();
5046

5147
const installationId = await getInstallationId(ghesApiUrl, runnerOwner);
5248
const ghAuth = await createGithubInstallationAuth(installationId, ghesApiUrl);

lambdas/functions/control-plane/src/scale-runners/job-retry.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { addPersistentContextToChildLogger, createSingleMetric, logger } from '@aws-github-runner/aws-powertools-util';
22
import { publishMessage } from '../aws/sqs';
3-
import { ActionRequestMessage, ActionRequestMessageRetry, getGitHubEnterpriseApiUrl, isJobQueued } from './scale-up';
3+
import { ActionRequestMessage, ActionRequestMessageRetry, isJobQueued, getGitHubEnterpriseApiUrl } from './scale-up';
44
import { getOctokit } from '../github/octokit';
55
import { MetricUnit } from '@aws-lambda-powertools/metrics';
66
import yn from 'yn';

lambdas/functions/control-plane/src/scale-runners/scale-down.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ describe('Scale down runners', () => {
159159
mockCreateClient.mockResolvedValue(new mocktokit());
160160
});
161161

162-
const endpoints = ['https://api.github.com', 'https://github.enterprise.something'];
162+
const endpoints = ['https://api.github.com', 'https://github.enterprise.something', 'https://companyname.ghe.com'];
163163

164164
describe.each(endpoints)('for %s', (endpoint) => {
165165
beforeEach(() => {
166-
if (endpoint.includes('enterprise')) {
166+
if (endpoint.includes('enterprise') || endpoint.endsWith('.ghe.com')) {
167167
process.env.GHES_URL = endpoint;
168168
}
169169
});

lambdas/functions/control-plane/src/scale-runners/scale-down.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { RunnerInfo, RunnerList } from './../aws/runners.d';
88
import { GhRunners, githubCache } from './cache';
99
import { ScalingDownConfig, getEvictionStrategy, getIdleRunnerCount } from './scale-down-config';
1010
import { metricGitHubAppRateLimit } from '../github/rate-limit';
11+
import { getGitHubEnterpriseApiUrl } from './scale-up';
1112

1213
const logger = createChildLogger('scale-down');
1314

@@ -21,11 +22,7 @@ async function getOrCreateOctokit(runner: RunnerInfo): Promise<Octokit> {
2122
}
2223

2324
logger.debug(`[createGitHubClientForRunner] Cache miss for ${key}`);
24-
const ghesBaseUrl = process.env.GHES_URL;
25-
let ghesApiUrl = '';
26-
if (ghesBaseUrl) {
27-
ghesApiUrl = `${ghesBaseUrl}/api/v3`;
28-
}
25+
const { ghesApiUrl } = getGitHubEnterpriseApiUrl();
2926
const ghAuthPre = await createGithubAppAuth(undefined, ghesApiUrl);
3027
const githubClientPre = await createOctokitClient(ghAuthPre.token, ghesApiUrl);
3128

0 commit comments

Comments
 (0)