Skip to content

Commit 62fe102

Browse files
committed
Add CloudWatch support
1 parent 3859903 commit 62fe102

7 files changed

Lines changed: 306 additions & 7 deletions

File tree

.github/workflows/runner.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ on:
3434
description: "AWS tags to apply to EC2 instances (JSON array format)"
3535
required: false
3636
type: string
37+
cloudwatch_logs_group:
38+
description: "CloudWatch Logs group name for streaming runner logs (leave empty to disable)"
39+
required: false
40+
type: string
3741
ec2_launch_role:
3842
description: "AWS role ARN to assume for EC2 operations (falls back to vars.EC2_LAUNCH_ROLE; one or the other is required)"
3943
required: false
@@ -49,7 +53,7 @@ on:
4953
type: string
5054
default: "g4dn.xlarge"
5155
ec2_instance_profile:
52-
description: "Instance profile name to attach to launched EC2 instance"
56+
description: "Instance profile name to attach to launched EC2 instance (required for CloudWatch logging)"
5357
required: false
5458
type: string
5559
ec2_home_dir:
@@ -141,6 +145,7 @@ jobs:
141145
with:
142146
aws_region: ${{ inputs.aws_region }}
143147
aws_tags: ${{ inputs.aws_tags }}
148+
cloudwatch_logs_group: ${{ inputs.cloudwatch_logs_group || vars.CLOUDWATCH_LOGS_GROUP }}
144149
ec2_image_id: ${{ inputs.ec2_image_id || vars.EC2_IMAGE_ID }}
145150
ec2_instance_type: ${{ inputs.ec2_instance_type || vars.EC2_INSTANCE_TYPE }}
146151
ec2_instance_profile: ${{ inputs.ec2_instance_profile || vars.EC2_INSTANCE_PROFILE }}

README.md

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Run GitHub Actions on ephemeral EC2 instances.
1111
- [Runner Lifecycle](#lifecycle)
1212
- [Multi-Job Workflows](#multi-job)
1313
- [How Termination Works](#termination)
14+
- [CloudWatch Logs Integration](#cloudwatch)
1415
- [Debugging and Troubleshooting](#debugging)
1516
- [SSH Access](#ssh)
1617
- [Important Log Files](#logs)
@@ -53,6 +54,10 @@ jobs:
5354
3. Configure an AWS IAM role with GitHub OIDC trust policy (see [below](#ec2-launch-role))
5455
4. Add the role ARN as a repository variable named `EC2_LAUNCH_ROLE`
5556

57+
**Optional Setup:**
58+
- `EC2_INSTANCE_PROFILE`: Only needed if you want CloudWatch Logs or need the EC2 instances to access AWS APIs
59+
- See [CloudWatch Logs Integration](#cloudwatch) for details
60+
5661
The `EC2_LAUNCH_ROLE` is passed to [aws-actions/configure-aws-credentials]; if you'd like to authenticate with AWS using other parameters, please [file an issue] to let us know.
5762

5863
### Configuring `EC2_LAUNCH_ROLE` <a id="ec2-launch-role"></a>
@@ -74,9 +79,10 @@ The reusable workflow ([`runner.yml`]) requires:
7479

7580
Optional inputs (all fall back to corresponding `vars.*` if not provided):
7681
- `action_ref` - ec2-gha Git ref to checkout (branch/tag/SHA); auto-detected if not specified
82+
- `cloudwatch_logs_group` - CloudWatch Logs group name for streaming logs (falls back to `vars.CLOUDWATCH_LOGS_GROUP`)
7783
- `ec2_home_dir` - Home directory (default: `/home/ubuntu`)
7884
- `ec2_image_id` - AMI ID (default: Deep Learning AMI)
79-
- `ec2_instance_profile` - IAM instance profile name for EC2 instances (useful for on-instance debugging [via SSH](#ssh))
85+
- `ec2_instance_profile` - IAM instance profile name for EC2 instances (useful for on-instance debugging [via SSH](#ssh), required for [CloudWatch logging](#cloudwatch))
8086
- `ec2_instance_type` - Instance type (default: `g4dn.xlarge`)
8187
- `ec2_key_name` - EC2 key pair name (required for [SSH access](#ssh))
8288
- `ec2_root_device_size` - Root device size in GB (default: 0 = use AMI default)
@@ -144,6 +150,45 @@ jobs:
144150
5. New jobs starting within the grace period cancel the termination
145151
6. Before shutdown, the runner process is gracefully stopped to remove itself from GitHub
146152

153+
### CloudWatch Logs Integration <a id="cloudwatch"></a>
154+
155+
CloudWatch Logs integration is optional. To stream runner logs to CloudWatch Logs:
156+
157+
1. **Create a CloudWatch Logs group**:
158+
```bash
159+
aws logs create-log-group --log-group-name /aws/ec2/github-runners
160+
```
161+
162+
2. **Create an IAM role for your EC2 instances** with CloudWatch Logs permissions:
163+
164+
**Important**: This is a separate role from your GitHub Actions launch role (`EC2_LAUNCH_ROLE`). The EC2 instances need their own IAM role to write logs. This role is only required if you want to use CloudWatch Logs.
165+
166+
See [Appendix: IAM Role Setup](#iam-setup-appendix) for detailed instructions on creating the `EC2_INSTANCE_PROFILE`.
167+
168+
3. **Configure the workflow** with the IAM role:
169+
```yaml
170+
jobs:
171+
ec2:
172+
uses: Open-Athena/ec2-gha/.github/workflows/runner.yml@main
173+
with:
174+
cloudwatch_logs_group: /aws/ec2/github-runners
175+
ec2_instance_profile: GitHubRunnerEC2Profile # The instance profile from step 2
176+
secrets: inherit
177+
```
178+
179+
Or set as a repository variable:
180+
```bash
181+
gh variable set EC2_INSTANCE_PROFILE --body "GitHubRunnerEC2Profile"
182+
```
183+
184+
The following logs will be streamed to CloudWatch:
185+
- `/var/log/runner-setup.log` - Runner installation and setup
186+
- `/tmp/job-started-hook.log` - Job start events with workflow/job details
187+
- `/tmp/job-completed-hook.log` - Job completion events with remaining job count
188+
- `/tmp/termination-check.log` - Instance termination checks every 30 seconds
189+
- `~/actions-runner/_diag/Runner_*.log` - GitHub runner diagnostic logs
190+
- `~/actions-runner/_diag/Worker_*.log` - GitHub runner worker process logs
191+
147192
### Debugging and Troubleshooting <a id="debugging"></a>
148193

149194
#### SSH Access <a id="ssh"></a>
@@ -157,11 +202,12 @@ To enable SSH debugging, provide:
157202
Once connected to the instance:
158203
- `/var/log/runner-setup.log` - Runner installation and registration
159204
- `/var/log/cloud-init-output.log` - Complete userdata execution
160-
- `/tmp/job-started-hook.log` - Job start tracking
161-
- `/tmp/job-completed-hook.log` - Job completion tracking
205+
- `/tmp/job-started-hook.log` - Job start tracking with detailed metadata
206+
- `/tmp/job-completed-hook.log` - Job completion tracking with job counts
162207
- `/tmp/termination-check.log` - Termination check logs (runs every 30 seconds)
163208
- `/var/run/github-runner-jobs/*.job` - Individual job status files
164-
- `~/actions-runner/_diag/*.log` - GitHub runner diagnostic logs
209+
- `~/actions-runner/_diag/Runner_*.log` - GitHub runner process logs (job scheduling, API calls)
210+
- `~/actions-runner/_diag/Worker_*.log` - Job execution logs
165211

166212
#### Common Issues <a id="issues"></a>
167213

@@ -257,6 +303,55 @@ ec2_launch_policy = aws.iam.Policy("github-actions-ec2-launch-policy",
257303
}"""
258304
)
259305
306+
# CloudWatch Logs policy for EC2 instances
307+
cloudwatch_logs_policy = aws.iam.Policy("ec2-instance-cloudwatch-policy",
308+
policy="""{
309+
"Version": "2012-10-17",
310+
"Statement": [
311+
{
312+
"Effect": "Allow",
313+
"Action": [
314+
"logs:CreateLogGroup",
315+
"logs:CreateLogStream",
316+
"logs:PutLogEvents",
317+
"logs:DescribeLogStreams"
318+
],
319+
"Resource": "arn:aws:logs:*:*:*"
320+
}
321+
]
322+
}"""
323+
)
324+
325+
# Create IAM role for EC2 instances (shared across all repos)
326+
ec2_instance_role = aws.iam.Role("github-runner-ec2-instance-role",
327+
assume_role_policy="""{
328+
"Version": "2012-10-17",
329+
"Statement": [
330+
{
331+
"Effect": "Allow",
332+
"Principal": {
333+
"Service": "ec2.amazonaws.com"
334+
},
335+
"Action": "sts:AssumeRole"
336+
}
337+
]
338+
}"""
339+
)
340+
341+
# Attach CloudWatch policy to instance role
342+
cloudwatch_policy_attachment = aws.iam.RolePolicyAttachment("ec2-instance-cloudwatch-attachment",
343+
role=ec2_instance_role.name,
344+
policy_arn=cloudwatch_logs_policy.arn
345+
)
346+
347+
# Create instance profile
348+
ec2_instance_profile = aws.iam.InstanceProfile("github-runner-ec2-profile",
349+
role=ec2_instance_role.name
350+
)
351+
352+
# Export the instance profile name
353+
pulumi.export("ec2_instance_profile_name", ec2_instance_profile.name)
354+
260355
# Configure which repos can use the launch role
261356
ORGS_REPOS = [
262357
"your-org/your-repo",
@@ -368,7 +463,56 @@ aws iam attach-role-policy \
368463
--role-name GitHubActionsEC2LaunchRole \
369464
--policy-arn arn:aws:iam::YOUR_ACCOUNT_ID:policy/GitHubActionsEC2LaunchPolicy
370465
371-
# 5. Configure repository variables
466+
# 5. Create CloudWatch Logs policy for EC2 instances
467+
aws iam create-policy \
468+
--policy-name GitHubRunnerCloudWatchPolicy \
469+
--policy-document '{
470+
"Version": "2012-10-17",
471+
"Statement": [
472+
{
473+
"Effect": "Allow",
474+
"Action": [
475+
"logs:CreateLogGroup",
476+
"logs:CreateLogStream",
477+
"logs:PutLogEvents",
478+
"logs:DescribeLogStreams"
479+
],
480+
"Resource": "arn:aws:logs:*:*:*"
481+
}
482+
]
483+
}'
484+
485+
# 6. Create EC2 instance role
486+
aws iam create-role \
487+
--role-name GitHubRunnerEC2InstanceRole \
488+
--assume-role-policy-document '{
489+
"Version": "2012-10-17",
490+
"Statement": [
491+
{
492+
"Effect": "Allow",
493+
"Principal": {
494+
"Service": "ec2.amazonaws.com"
495+
},
496+
"Action": "sts:AssumeRole"
497+
}
498+
]
499+
}'
500+
501+
# 7. Attach CloudWatch policy to instance role
502+
aws iam attach-role-policy \
503+
--role-name GitHubRunnerEC2InstanceRole \
504+
--policy-arn arn:aws:iam::YOUR_ACCOUNT_ID:policy/GitHubRunnerCloudWatchPolicy
505+
506+
# 8. Create instance profile
507+
aws iam create-instance-profile \
508+
--instance-profile-name GitHubRunnerEC2Profile
509+
510+
# 9. Add role to instance profile
511+
aws iam add-role-to-instance-profile \
512+
--instance-profile-name GitHubRunnerEC2Profile \
513+
--role-name GitHubRunnerEC2InstanceRole
514+
515+
# 10. Configure repository variables
372516
gh variable set EC2_LAUNCH_ROLE --body "arn:aws:iam::YOUR_ACCOUNT_ID:role/GitHubActionsEC2LaunchRole"
373517
gh variable set EC2_INSTANCE_PROFILE --body "GitHubRunnerEC2Profile"
374518
```

action.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ inputs:
1313
aws_tags:
1414
description: "The AWS tags to use for your runner, formatted as a JSON list. See `README` for more details."
1515
required: false
16+
cloudwatch_logs_group:
17+
description: "CloudWatch Logs group name for streaming runner logs. Leave empty to disable CloudWatch Logs."
18+
required: false
1619
ec2_home_dir:
1720
description: "The EC2 AMI home directory to use for your runner. Will not start if not specified. For example: `/home/ec2-user`"
1821
required: true
1922
ec2_image_id:
2023
description: "The machine AMI to use for your runner. This AMI can be a default but should have docker installed in the AMI. Will not start if not specified."
2124
required: true
2225
ec2_instance_profile:
23-
description: "Instance profile name to attach to launched EC2 instances"
26+
description: "Instance profile name to attach to launched EC2 instances (e.g. for CloudWatch Logs)"
2427
required: false
2528
ec2_instance_type:
2629
description: "The type of instance to use for your runner. For example: t2.micro, t4g.nano, etc. Will not start if not specified."

src/ec2_gha/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def main():
2020
EnvVarBuilder(env)
2121
.update_state("INPUT_AWS_SUBNET_ID", "subnet_id")
2222
.update_state("INPUT_AWS_TAGS", "tags", is_json=True)
23+
.update_state("INPUT_CLOUDWATCH_LOGS_GROUP", "cloudwatch_logs_group")
2324
.update_state("INPUT_EC2_HOME_DIR", "home_dir")
2425
.update_state("INPUT_EC2_IMAGE_ID", "image_id")
2526
.update_state("INPUT_EC2_INSTANCE_PROFILE", "iam_instance_profile")

src/ec2_gha/start.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class StartAWS(CreateCloudInstance):
2828
The name of the region to use.
2929
repo : str
3030
The repository to use.
31+
cloudwatch_logs_group : str
32+
CloudWatch Logs group name for streaming runner logs. Defaults to an empty string.
3133
gh_runner_tokens : list[str]
3234
A list of GitHub runner tokens. Defaults to an empty list.
3335
iam_instance_profile : str
@@ -64,6 +66,7 @@ class StartAWS(CreateCloudInstance):
6466
instance_type: str
6567
region_name: str
6668
repo: str
69+
cloudwatch_logs_group: str = ""
6770
gh_runner_tokens: list[str] = field(default_factory=list)
6871
iam_instance_profile: str = ""
6972
key_name: str = ""
@@ -263,6 +266,7 @@ def create_instances(self) -> dict[str, str]:
263266
labels = f"{self.labels},{label}" if self.labels else label
264267

265268
user_data_params = {
269+
"cloudwatch_logs_group": self.cloudwatch_logs_group,
266270
"github_workflow": environ.get("GITHUB_WORKFLOW", ""),
267271
"github_run_id": environ.get("GITHUB_RUN_ID", ""),
268272
"github_run_number": environ.get("GITHUB_RUN_NUMBER", ""),

0 commit comments

Comments
 (0)