DEV Community

CodeHiRise
CodeHiRise

Posted on

Stop Wasting Money 💰 on Idle Jenkins Instances: A Serverless 🚀 Solution to Slash Your AWS Bill

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:

  1. Stop Jenkins: Monitors activity and shuts down the EC2 instance when idle.
  2. 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.

Stop Jenkins workflow

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()
Enter fullscreen mode Exit fullscreen mode

🚀 Start Jenkins workflow

  1. 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.
  2. Once Jenkins is active, the Lambda forwards the webhook payload to /github-webhook/.

Start Jenkins workflow

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()}")

Enter fullscreen mode Exit fullscreen mode

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.

What do you think will you use this on your environment.

share your thoughts in the comment section.

Thank you for reading

Cheers 🥂

Top comments (0)