Skip to content

Commit 7e84a60

Browse files
ryan-williamsclaude
andcommitted
Add SSH private key support for connecting to Lambda instances
The Docker container running the action needs the SSH private key to connect to Lambda instances. Added: - `ssh_private_key` input to action.yml - `LAMBDA_SSH_PRIVATE_KEY` secret to runner.yml workflow - Code to write key to temp file and use `-i` flag in SSH commands - Better error logging for Lambda API errors (log response body) - Reverted e2e-test.yml to use `gpu_1x_a10` in `us-east-1` Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8aa2852 commit 7e84a60

File tree

5 files changed

+36
-3
lines changed

5 files changed

+36
-3
lines changed

.github/workflows/e2e-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ jobs:
88
uses: ./.github/workflows/runner.yml
99
secrets: inherit
1010
with:
11-
instance_type: gpu_1x_a100_sxm4
12-
region: us-west-2
11+
instance_type: gpu_1x_a10
12+
region: us-east-1
1313
runner_grace_period: "30"
1414
runner_initial_grace_period: "120"
1515
debug: "true"

.github/workflows/runner.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ on:
1616
LAMBDA_API_KEY:
1717
description: "Lambda Labs API key"
1818
required: true
19+
LAMBDA_SSH_PRIVATE_KEY:
20+
description: "SSH private key for connecting to Lambda instances"
21+
required: true
1922
inputs:
2023
action_ref:
2124
description: "lambda-gha Git ref (branch/tag/SHA) to checkout"
@@ -121,6 +124,7 @@ jobs:
121124
runner_poll_interval: ${{ inputs.runner_poll_interval || vars.RUNNER_POLL_INTERVAL }}
122125
runner_registration_timeout: ${{ inputs.runner_registration_timeout || vars.RUNNER_REGISTRATION_TIMEOUT }}
123126
ssh_key_names: ${{ inputs.ssh_key_names || vars.LAMBDA_SSH_KEY_NAMES }}
127+
ssh_private_key: ${{ secrets.LAMBDA_SSH_PRIVATE_KEY }}
124128
userdata: ${{ inputs.userdata }}
125129
env:
126130
GH_PAT: ${{ secrets.GH_SA_TOKEN }}

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ inputs:
4848
ssh_key_names:
4949
description: "SSH key names registered in Lambda Labs (comma-separated)"
5050
required: false
51+
ssh_private_key:
52+
description: "SSH private key for connecting to Lambda instances"
53+
required: false
5154
userdata:
5255
description: "Additional script to run before runner setup"
5356
required: false

src/lambda_gha/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def main():
5656
.update_state("INPUT_RUNNER_INITIAL_GRACE_PERIOD", "runner_initial_grace_period")
5757
.update_state("INPUT_RUNNER_POLL_INTERVAL", "runner_poll_interval")
5858
.update_state("INPUT_SSH_KEY_NAMES", "ssh_key_names")
59+
.update_state("INPUT_SSH_PRIVATE_KEY", "ssh_private_key")
5960
.update_state("INPUT_USERDATA", "userdata")
6061
.update_state("GITHUB_REPOSITORY", "repo")
6162
.update_state("INPUT_REPO", "repo")

src/lambda_gha/start.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class StartLambdaLabs:
9393
runner_initial_grace_period: str = "180"
9494
runner_poll_interval: str = "10"
9595
runner_release: str = ""
96+
ssh_private_key: str = ""
9697
userdata: str = ""
9798

9899
def _api_request(
@@ -106,7 +107,14 @@ def _api_request(
106107
headers = {"Authorization": f"Bearer {self.api_key}"}
107108

108109
resp = requests.request(method, url, headers=headers, json=json_data)
109-
resp.raise_for_status()
110+
if not resp.ok:
111+
# Log the actual error body before raising
112+
try:
113+
error_body = resp.json()
114+
print(f"Lambda API error: {error_body}")
115+
except Exception:
116+
print(f"Lambda API error (raw): {resp.text}")
117+
resp.raise_for_status()
110118
return resp.json()
111119

112120
def _get_template_vars(self, idx: int = None) -> dict:
@@ -341,15 +349,32 @@ def execute_setup_via_ssh(
341349
retry_delay : int
342350
Seconds between retry attempts.
343351
"""
352+
import os
353+
import stat
354+
import tempfile
355+
344356
print(f"Connecting to {ssh_user}@{ip} to execute setup...")
345357

358+
# Write SSH private key to temporary file if provided
359+
key_file = None
360+
if self.ssh_private_key:
361+
key_file = tempfile.NamedTemporaryFile(mode='w', suffix='_key', delete=False)
362+
key_file.write(self.ssh_private_key)
363+
if not self.ssh_private_key.endswith('\n'):
364+
key_file.write('\n')
365+
key_file.close()
366+
os.chmod(key_file.name, stat.S_IRUSR) # 0400
367+
print(f"Using SSH key from secret")
368+
346369
# SSH options for non-interactive, key-based auth
347370
ssh_opts = [
348371
"-o", "StrictHostKeyChecking=no",
349372
"-o", "UserKnownHostsFile=/dev/null",
350373
"-o", "ConnectTimeout=10",
351374
"-o", "BatchMode=yes",
352375
]
376+
if key_file:
377+
ssh_opts.extend(["-i", key_file.name])
353378

354379
# Wait for SSH to be available
355380
for attempt in range(1, max_retries + 1):

0 commit comments

Comments
 (0)