feat: Add userDataCommands to Ec2RunnerProvider#954
Conversation
Allow running custom commands on EC2 runner instance start-up, before the runner registers with GitHub. This enables per-instance setup that can't be baked into the AMI, like security agents that need to register each instance (e.g. Lacework). Commands are inserted into the user data template with braces escaped so States.Format placeholders are unaffected, and each provider with custom commands gets a unique constant key in $.consts so multiple EC2 providers don't clash. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks. You should already be able to add commands that run before the job with something like: const builder = EcsRunnerProvider.imageBuilder(stack, 'builder');
builder.addComponent(
RunnerImageComponent.environmentVariables({
ACTIONS_RUNNER_HOOK_JOB_STARTED: '/home/runner/runner-started.sh',
}),
);
builder.addComponent(
RunnerImageComponent.custom({
name: 'runner-started.sh',
assets: [
{
source: 'INSERT_LOCAL_PATH_TO_STARTUP_SCRIPT_HERE', // TODO TODO TODO
target: '/home/runner/runner-started.sh',
},
],
}),
);
const provider = new EcsRunnerProvider(stack, 'ECS', {
labels: ['ecs'],
imageBuilder: builder,
});This method works on all types of providers and also has the added benefit of showing its output as part of the job for easier debugging. For things like security agents, I'd personally prefer having the AMI bake a systemd unit preconfigured to start with the system. That way it can monitor everything including runner setup and even the existing userdata itself. In general I worry about adding anything to our userdata. It's quite finicky with the braces and everything as you've mentioned. Is there a use-case I'm not thinking of that would require userdata modification? |
Make it easier to run a script before a job starts or after a job ends. This feature uses `ACTIONS_RUNNER_HOOK_JOB_STARTED` and `ACTIONS_RUNNER_HOOK_JOB_COMPLETED`. These are standard hooks available to GitHub self-hosted runners. They are environment variables pointing to a shell script. When set, the runner will execute those scripts as steps before a job starts and after it ends respectively. Output from the scrips will show up in "Set up runner" and "Complete runner" steps in the job itself. <img width="470" height="131" alt="image" src="https://github.com/user-attachments/assets/cf9d607d-2fa1-4e6d-ad05-e350633e0125" /> All of the runners we create are ephemeral so these scripts will only run once per runner. This PR makes this feature a bit more accessible by exposing it in `RunnerImageComponent.jobStartedHook()` and `RunnerImageComponent.jobCompletedHook()`. The scripts have access to all the usual [environment variables](https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables). More information about the feature is available in [GitHub's documentation](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/run-scripts). ```typescript const imageBuilder = Ec2RunnerProvider.imageBuilder(this, 'ImageBuilder'); imageBuilder.addComponent( RunnerImageComponent.jobStartedHook(path.join(__dirname, 'job-started.sh')), ); imageBuilder.addComponent( RunnerImageComponent.jobCompletedHook(path.join(__dirname, 'job-completed.sh')), ); const ec2Provider = new Ec2RunnerProvider(this, 'Ec2Provider', { labels: ['ec2', 'linux', 'x64'], imageBuilder: imageBuilder, }); // Create the GitHub runners infrastructure new GitHubRunners(this, 'GitHubRunners', { providers: [ec2Provider], }); ``` Related #762 Related #954
|
@kichik That's a good workaround for this. Thanks! I've also actually got another issue now around tags, so I'll open a PR for that soon. |
Summary
Adds a
userDataCommandsprop toEc2RunnerProviderthat runs custom commands on instance start-up, before the runner registers with GitHub and starts the job.Motivation: some software requires per-instance setup and can't simply be baked into the AMI with image builder components — for example security agents (Lacework, in my case) that need to register each instance. Today the EC2 user data template is hard-coded, so there's no supported way to do this.
Implementation notes
States.Format, braces in user commands are escaped so things like${VAR}are treated literally and never as placeholders. The replacement uses a replacer function so$sequences in commands aren't mangled byString.replacespecial patterns.$.constskey (suffixed withnode.addr) so multiple EC2 providers with different commands don't trip the duplicate-constants check. Providers without custom commands keep sharing the existingec2UserDataLinux/ec2UserDataWindowskeys.Testing
$escaping, placeholder preservation, and multiple providers with different commands merging into one state machine without duplicate-key errors.API.mdregenerated with docgen.Made with Cursor