This week I picked up a story to automate the building of our Jenkins Linux agents. We currently run our Jenkins master server in AWS and launch the agents into AWS using the Amazon EC2 plugin. While writing the code to automate building a new agent AMI was pretty straightforward (I used Packer and Terraform), I did have one interesting challenge. How to update Jenkins to use the new node once it is built. I do not want to have to log into Jenkins and update the configuration manually with the new AMI ID every time a new agent is built. Instead, I wanted the Jenkins job that builds the image to also update itself to use the new image.

After doing some research on the internet, I found some references to this possibility in Stack Exchange and while I couldn’t get the answer that was posted to work, I was able to play around with the code until I got it working.

Looking up the Instance

The first thing I did was to create a function that looked up the AMI of the Jenkins agent that I just built. I do that with an awscli command that looks up the latest image from AWS and returns that ImageId.

def get_image_id() {
    def ami = sh (
        script: "aws ec2 describe-images --owners self \
                    --filters 'Name=name,Values=jenkins-agent-*' 'Name=state,Values=available' \
                    --query 'sort_by(Images, &CreationDate)[-1].ImageId' --output text",
        returnStdout: true
    ).trim()

    return ami
}

Next, I created a function that would update the jenkins server with the latest AMI if it has changed. It will require some In-process Script Approvals (and the job will fail each time until they are all approved) to get it to work properly, so if that is something that is not allowed in your environment, this will not work for you.

def update_instance(String ami) {
    def instance = Jenkins.getInstance()
    def ec2_instances = instance.clouds[0].getTemplates().each{
        if(it.labels == 'amazon-linux2'){
            if (it.getAmi() == ami) {
                println "Nothing to do"
            } else {
                println("Current AMI id: " + it.getAmi())
                println("New AMI id: " + ami)
                it.setAmi(ami)
            }
        }
    }
}

With the functions complete, the next step is to add a build stage to run the code.

stage('Update Jenkins AMI') {
    update_instance(get_image_id())
}

With that complete, now the Jenkins job will build the new image and then update its own configuration to use the that image. This saves the manual step of updating the Jenkins configuration to use the new AMI.