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)