Hello DevOps Community,
In many environments, Jenkins instances sit idle between infrequent builds, accruing unnecessary EC2 costs. If your team uses Jenkins only periodically—for deployments, pull requests, or CI/CD pipelines you’re likely overpaying for compute resources.
In this tutorial, we’ll explore a serverless solution to stop the Jenkins instance during idle periods and start it automatically when a new build is triggered.
🚀 How It Works
This solution uses two AWS Lambda functions:
- Stop Jenkins: Monitors activity and shuts down the EC2 instance when idle.
- Start Jenkins: Starts the instance when a GitHub webhook triggers a build and forwards the request to Jenkins.
By running the EC2 instance only during active builds, you eliminate idle compute costs.
⚠️ Important Consideration:
This approach disrupts scheduled Jenkins jobs (e.g., nightly builds). Ensure your instance operates purely on-demand via webhook triggers and does not rely on schedules.
🔷 Note that This guide assumes you use the GitHub plugin to trigger builds on GitHub push events. Adjust the code if your setup differs.
🛑 Stop Jenkins workflow
stop Jenkins function will run periodically using event bridge scheduler to check whether Jenkins server is idle every 5 minutes and if it is idle this lambda function will stop the Jenkins server.
to check jenkins is idle we use Jenkins computer api/computer/api/json
.
import boto3
import urllib.request
import json
import os
import base64
ec2 = boto3.client('ec2')
jenkins_ip = os.environ['JENKINS_IP']
INSTANCE_ID = os.environ['EC2_INSTANCE_ID']
JENKINS_URL = f'http://{jenkins_ip}:8080'
JENKINS_USER = os.environ['JENKINS_USER']
JENKINS_TOKEN = os.environ['JENKINS_TOKEN']
def lambda_handler(event, context):
try:
state = get_instance_state()
print(f"Instance state: {state}")
if state == 'stopped':
return {'statusCode': 200, 'body': 'Instance is stopped'}
else:
if is_jenkins_idle():
ec2.stop_instances(InstanceIds=[INSTANCE_ID])
return {'statusCode': 200, 'body': 'Instance is idle initiated stop action'}
return {'statusCode': 200, 'body': 'Instance is running and Jenkins is busy'}
except Exception as e:
print(e)
return {'statusCode': 500, 'body': "error occurred"}
def get_instance_state():
response = ec2.describe_instances(InstanceIds=[INSTANCE_ID])
return response['Reservations'][0]['Instances'][0]['State']['Name']
def is_jenkins_idle():
try:
# Check running builds via executors
computer_url = f"{JENKINS_URL}/computer/api/json"
request = urllib.request.Request(computer_url)
request.add_header('Authorization', 'Basic ' +
base64.b64encode(f"{JENKINS_USER}:{JENKINS_TOKEN}".encode()).decode())
response = urllib.request.urlopen(request)
if response.status != 200:
print(f"Failed to get computer info: {response.status}")
return False
computer_data = json.loads(response.read().decode())
print(computer_data)
for computer in computer_data['computer']:
if not computer['idle']:
return False
print("Jenkins is idle")
return True
except Exception as e:
print(f"Error checking Jenkins status: {e}")
return False
finally:
response.close()
🚀 Start Jenkins workflow
- When a GitHub webhook triggers:
- The Start Jenkins Lambda checks if the instance is running.
- If stopped, it starts the instance and waits until it’s healthy.
- Once Jenkins is active, the Lambda forwards the webhook payload to
/github-webhook/
.
import boto3
import time
import urllib.request
import json
import os
ec2 = boto3.client('ec2')
jenkins_ip = os.environ['JENKINS_IP']
INSTANCE_ID = os.environ['EC2_INSTANCE_ID']
JENKINS_URL = f'http://{jenkins_ip}:8080'
def lambda_handler(event, context):
try:
payload = json.loads(event['body'])
print(payload)
state = get_instance_state()
print(f"Instance state: {state}")
if state == 'stopped':
ec2.start_instances(InstanceIds=[INSTANCE_ID])
wait_for_instance_running()
wait_for_jenkins_ready()
trigger_build(payload)
return {'statusCode': 200, 'body': 'Build triggered'}
except Exception as e:
print(e)
return {'statusCode': 500, 'body': "error occurred"}
def get_instance_state():
response = ec2.describe_instances(InstanceIds=[INSTANCE_ID])
return response['Reservations'][0]['Instances'][0]['State']['Name']
def wait_for_instance_running():
while get_instance_state() != 'running':
time.sleep(10)
def wait_for_jenkins_ready():
while True:
try:
req = urllib.request.Request(f"{JENKINS_URL}/login")
print(f"Attempting to connect to Jenkins at {JENKINS_URL}")
response = urllib.request.urlopen(req)
print(f"Jenkins is ready: {response.status}")
break
except Exception as e:
print(f"Jenkins is not ready error: {e}")
time.sleep(10)
def trigger_build(payload):
url = f"{JENKINS_URL}/github-webhook/"
try:
req = urllib.request.Request(url, method='POST', data=json.dumps(payload).encode(), headers={
'Content-Type': 'application/json',
'User-Agent': 'lambda-function',
'X-GitHub-Event': 'push'
})
response = urllib.request.urlopen(req)
print(f"Jenkins triggered build successfully with response: {response.status}")
except Exception as e:
print(f"Failed to trigger Jenkins build: {e}")
print(f"Error response: {e.read().decode()}")
Key Benefits
- 💸 Cost Savings: Pay only for active build time. For example, if you use the Jenkins instance for 2 hours daily for active builds, your monthly uptime totals 60 hours (versus 720 hours for 24/7 operation), resulting in over 90% cost savings.
- ✨ Automation: No manual intervention required.
Implement this solution to align your Jenkins costs with actual usage! 🚀💰📉
✋ If you need to use the Jenkins server for extended periods (e.g., debugging), you can either:
Temporarily disable the EventBridge scheduler to prevent automatic shutdowns.
Enable stop-protection on the EC2 instance until debugging is complete.
This ensures uninterrupted access while retaining cost optimization as the default behavior.
Top comments (0)