This post will demonstrate how to setup Jenkins to install the necessary Pulumi Python dependencies, and show an example deployment pipeline to setup a couple of EC2 instances on AWS.

Here is an overview of the deployment process.

Note. As this is for demonstration purposes, the pipeline will also destroy the EC2 instances on AWS it initially created. This is executed via the stage: Pulumi Cleanup.

All the resources required can be found in the following Gitlab repository:

https://gitlab.com/spengler84/public-repo.git

Setup Jenkins

I’ll setup the Jenkins master inside a docker container for this demonstration.

Dockerfile

I’m using Ubuntu as the base image, then installing Jenkins along with additional dependencies.

  • entrypoint.sh - a shell script taken from here - this is used to launch Jenkins in the background.

  • Pulumi only supports Python versions >=3.6

    FROM ubuntu:latest
        
    ARG user=jenkins
    ARG group=jenkins
    ARG REF=/usr/share/jenkins/ref
    ARG JENKINS_HOME=/var/jenkins_home
    ARG PULUMI_VERSION=latest
    ARG uid=1000
    ARG gid=1000
    ARG http_port=8080
    ARG agent_port=50000
        
    ENV JENKINS_SLAVE_AGENT_PORT ${agent_port}
    ENV REF $REF
    ENV JENKINS_HOME $JENKINS_HOME
        
    ENV LC_ALL C.UTF-8
    ENV LANG C.UTF-8
        
    RUN mkdir -p /etc/pki/tls/certs
        
    RUN apt-get update -qq
        
    RUN apt-get install wget gnupg curl default-jdk python3-venv python3 python3-pip  -qq
        
    RUN wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | apt-key add -
        
    RUN sh -c 'echo deb https://pkg.jenkins.io/debian-stable binary/ > \
        /etc/apt/sources.list.d/jenkins.list'
        
    RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9B7D32F2D50582E6
        
    RUN apt-get update -qq
        
    RUN apt-get install jenkins -qq
        
    ENV PATH=$PATH:/root/.pulumi/bin
        
    RUN curl -fsSL https://get.pulumi.com/ | sh
        
    RUN pip3 install grpcio pipenv pulumi-aws
        
    RUN mkdir -p ${REF}/init.groovy.d ${JENKINS_HOME}
        
    RUN chown -R ${user}:${group} "$JENKINS_HOME" "$REF"
        
    RUN update-ca-certificates && cd /etc/pki/tls/certs/ && ln -s /etc/ssl/certs/ca-certificates.crt ca-bundle.crt
      
    COPY entrypoint.sh /
    ENTRYPOINT [ "/entrypoint.sh" ]    
    

Build the image by being in the same directory of your Dockerfile.

    docker build -t jenkins-ubuntu:0.1 .

docker-compose.yml

    version: '3.7'
    networks:
      default:
        external: 
          name: dev
    
    services:
      jenkins:
        restart: always
        ports:
          - 9090:8080
        image: jenkins-ubuntu:0.1
        container_name: jenkins-master
        volumes:
          - /volume1/docker-data/jenkins-master/:/var/jenkins_home
  • I’ve mapped an external volume to manage the Jenkins home directory. This way the directory is retained even if I destroy the container. * Port forwarding is also enabled * A separate network called ‘dev’ is used to isolate this container from accessing other Docker resources

Bring up Jenkins

    docker-compose up -d

Check the logs of the container for any problems related to the startup of Jenkins.

    docker logs -f jenkins-master

Create Jenkins credential

This will be used to store the AWS Access key ID & AWS Secret access key.

Navigate to https://jenkins_host**/credentials/store/system/domain/_/newCredentials**

This will open the following.

Username: Enter the AWS access Key Id

Password: Enter the AWS Secret access key

Description: Enter a meaningful description

Once you click on OK.

Jenkins will save this Credential and generate a hash for the ID. Copy this hash as you will need to enter into the Jenkins pipeline.

It will look similar to.

Jenkins pipeline

#!groovy

pipeline {
agent any
environment {
DEVOPS_WORKSPACE = "pulumi-automation"
PULUMI_STACK = "pulumi-aws-simple"
}
parameters {
string(defaultValue: '<replace with jenkins credential hash>', description: 'AWS credentials', name: 'AWS_CREDENTIAL')
string(defaultValue: '<replace with AWS region>', description: 'AWS Region', name: 'AWS_REGION')
password(defaultValue: '<replace with pulumi access token>', description: 'Pulumi Access token', name: 'PULUMI_ACCESS_TOKEN')


}
options {
// Only keep the 5 most recent builds
buildDiscarder(logRotator(numToKeepStr: '5'))
skipDefaultCheckout(true)
}
stages {

stage('Pulumi : Login') {
  steps {
  dir(DEVOPS_WORKSPACE) {
    timestamps {
    script {
      sh "export PULUMI_ACCESS_TOKEN=${PULUMI_ACCESS_TOKEN}; pulumi login"
    }
    }
  }
  }
}

stage('Pulumi : Initialize stack') {
  steps {
  dir(DEVOPS_WORKSPACE) {
    timestamps {
    script {
      sh ""
      "#!/bin/bash
      stack = \$(pulumi stack ls | grep "pulumi-aws-simple" | awk {
      'print \$1'
      })
      if [-z\ $stack];
      then
      pulumi stack init\ $PULUMI_STACK
      else
      pulumi stack select\ $PULUMI_STACK
      fi
      pulumi config set aws: region\ $AWS_REGION ""
      "
    }
    }
  }
  }

}

stage('Pulumi : Deployment') {
  steps {
  dir(DEVOPS_WORKSPACE) {
    withCredentials([
    [$class: 'UsernamePasswordMultiBinding', credentialsId: "${params.AWS_CREDENTIAL}", usernameVariable: 'AWS_ACCESS_KEY_ID', passwordVariable: 'AWS_SECRET_ACCESS_KEY']
    ]) {
    timestamps {
      script {
      sh ""
      "    #!/bin/bash
      echo "==> Cleanup Python temp files."
      rm - rf venv Pipfile.lock __pycache__

      python3 -m venv venv
      . venv/bin/activate
      set AWS_ACCESS_KEY_ID = $ {
        AWS_ACCESS_KEY_ID
      }
      set AWS_SECRET_ACCESS_KEY = $ {
        AWS_SECRET_ACCESS_KEY
      }

      echo "==> Install Python modules."
      pip3 install - r requirements.txt;
      echo "==> Pulumi deployment."
      pulumi up - y

      deactivate
        ""
      "
      }
    }
    }
  }
  }
}

stage('Pulumi : Cleanup') {
  steps {
  dir(DEVOPS_WORKSPACE) {
    withCredentials([
    [$class: 'UsernamePasswordMultiBinding', credentialsId: "${params.AWS_CREDENTIAL}", usernameVariable: 'AWS_ACCESS_KEY_ID', passwordVariable: 'AWS_SECRET_ACCESS_KEY']
    ]) {
    timestamps {
      script {
      sh ""
      "  #!/bin/bash
      echo "==> Destroy AWS EC2 instances."
      python3 - m venv venv
      . venv/bin/activate
      set AWS_ACCESS_KEY_ID = $ {
        AWS_ACCESS_KEY_ID
      }
      set AWS_SECRET_ACCESS_KEY = $ {
        AWS_SECRET_ACCESS_KEY
      }

      pulumi destroy - y


      pulumi stack rm $ {
        PULUMI_STACK
      } - y

      deactivate
      rm -rf venv
        ""
      "
      }
    }
    }

  }
  }

}

stage('Pulumi : Logout') {
  steps {
  dir(DEVOPS_WORKSPACE) {
    timestamps {
    script {
      sh "pulumi logout"
    }
    }
  }
  }
}
}
}

Jenkins pipeline stages

Pulumi: Login
  • Authenticate the user to make calls to the Pulumi service.

  • Setting the environment variable PULUMI_ACCESS_TOKEN allows login to be achieved via a script.

    export PULUMI_ACCESS_TOKEN=${PULUMI_ACCESS_TOKEN}; pulumi login
Pulumi: Initialize stack
  • Initilizes a stack else selects an existing one * Configures pulumi to set AWS region to deploy into
    stack=$(pulumi stack ls | grep "pulumi-aws-simple" | awk {'print $1'})
    if [ -z $stack  ]; then
     stack init $PULUMI_STACK
     stack select $PULUMI_STACK
    fi
    pulumi config set aws:region $AWS_REGION
Pulumi: Deployment
  • Cleans temporary files * Creates Python virtual environment * sets AWS access credentials environment variables * installs Pulumi dependencies * starts Pulumi deployment * Deactivates Python virtual environment
    echo "==> Cleanup Python temp files."
    rm -rf venv Pipfile.lock __pycache__
    python3 -m venv venv
     . venv/bin/activate
    set AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
    set AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
    
    echo "==> Install Python modules."                                
    pip3 install -r requirements.txt;
    echo "==> Pulumi deployment."
    pulumi up -y
                                         
    deactivate
    echo "==> Cleanup Python temp files."
    rm -rf venv Pipfile.lock __pycache__
Pulumi : Cleanup
  • Destroys recently deployed infrastructure * Removes Pulumi stack
    echo "==> Destroy AWS EC2 instances."
    python3 -m venv venv
    . venv/bin/activate
    set AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
    set AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
    
    pulumi destroy -y
    pulumi stack rm ${PULUMI_STACK} -y
    
    deactivate

Pulumi files

main.py

This is where you define what Pulumi will do.

  • Creates 2 EC2 instances in AWS * Creates 1 security group to allow port 22 for SSH access * this should not be open to the internet like I have done, but be confined to a range of IP-Addresses (for demo purposes I’ve left it open) - this is controlled by line 21 * Selects a centos image
    import pulumi
    import pulumi_aws as aws
    
    size = 't1.micro'
    no_servers = 2
    
    ami = aws.get_ami(most_recent="true",
                      owners=["247102896272"],
                      filters=[{
                          "name":
                          "name",
                          "values": ["aws-parallelcluster-2.0.0-centos7-hvm-*"]
                      }])
    
    group = aws.ec2.SecurityGroup('ssh-secgrp',
                                  description='Enable SSH access',
                                  ingress=[{
                                      'protocol': 'tcp',
                                      'from_port': 22,
                                      'to_port': 22,
                                      'cidr_blocks': ['0.0.0.0/0']
                                  }])
    
    for i in range(no_servers):
        server = aws.ec2.Instance('ssh-server-{}'.format(i),
                                instance_type=size,
                                vpc_security_group_ids=[group.id],
                                ami=ami.id)
    
        pulumi.export('public_ip', server.public_ip)
        pulumi.export('public_dns', server.public_dns)
Pulumi.yaml

Defines the Pulumi program.

    name: aws-py-simple
    runtime: python
    description: A minimal AWS Python Pulumi program
requirements.txt
    pulumi>=2.0.0,<3.0.0
    pulumi-aws>=2.0.0,<3.0.0

Running the Jenkins pipeline

Once you have setup the Jenkins pipeline, you will be presented with the following once you click on:

I’ve named the pipeline ‘PULUMI-AWS-CREATE-EC2’

Navigate to the Console Output to analyze the logs generated at runtime.