Jenkins-pipeline

In a previous article, 5 principles for deploying your API from a CI/CD pipeline, we discovered the main steps required to deploy your API from a CI/CD pipeline and this can prove to be a tremendous amount of work. Hopefully, the latest release of Red Hat Integration greatly improved this situation by adding new capabilities to the 3scale CLI. In 3scale toolbox: Deploy an API from the CLI, we discovered how the 3scale toolbox strives to automate the delivery of APIs. In this article, we will discuss how the 3scale toolbox can help you deploy your API from a Jenkins pipeline on Red Hat OpenShift/Kubernetes.

Running the 3scale toolbox from Jenkins

When calling a CLI from within a Jenkins Pipeline, at least three different strategies are possible:

  • You can create a custom Jenkins Slave image containing the CLI.
  • You can install the CLI on the fly during the pipeline run.
  • Or, you can spin up a Kubernetes Job or Pod calling the CLI in a container.

The custom Jenkins Slave has the drawback of being yet another container image to build, manage, troubleshoot, secure, etc. It also means credentials need to be managed from within Jenkins which require a proprietary plugin to apply RBAC on them. Installing the CLI on-the-fly works well for static binaries, such as jq or the oc command, but not so well for a Ruby program. Also, credentials need to be managed from within Jenkins.

The Kubernetes Job/Pod solution has the advantage of removing any dependency/preparatory work. It allows credentials management in OpenShift/Kubernetes (and thus subject to RBAC) and is future proof because it's the way the Tekton pipelines are working.

To deploy your API from a Jenkins Pipeline, run the 3scale toolbox as Kubernetes Jobs.

Running the 3scale toolbox from Jenkins

To run the 3scale toolbox as Kubernetes Jobs from a Jenkins Pipeline, we need to write a small helper function:

def runToolbox(args) {
  def kubernetesJob = [
    "apiVersion": "batch/v1",
    "kind": "Job",
    "metadata": [
      "name": "toolbox"
    ],
    "spec": [
      "backoffLimit": 0,
      "activeDeadlineSeconds": 300,
      "template": [
        "spec": [
          "restartPolicy": "Never",
          "containers": [
            [
              "name": "job",
              "image": "quay.io/redhat/3scale-toolbox:master",
              "imagePullPolicy": "Always",
              "args": [ "3scale", "version" ],
              "env": [
                [ "name": "HOME", "value": "/config" ]
              ],
              "volumeMounts": [
                [ "mountPath": "/config", "name": "toolbox-config" ],
                [ "mountPath": "/artifacts", "name": "artifacts" ]
              ]
            ]
          ],
          "volumes": [
            [ "name": "toolbox-config", "secret": [ "secretName": "3scale-toolbox" ] ],
            [ "name": "artifacts", "configMap": [ "name": "openapi" ] ]
          ]
        ]
      ]
    ]
  ]

  kubernetesJob.spec.template.spec.containers[0].args = args

  sh "rm -f -- job.yaml"
  writeYaml file: "job.yaml", data: kubernetesJob
  sh """
  oc delete job toolbox --ignore-not-found
  sleep 2
  oc create -f job.yaml
  sleep 20 # Adjust the sleep duration to your server velocity
  """

  def logs = sh(script: "oc logs -f job/toolbox", returnStdout: true)
  echo logs
  return logs
}

This function starts by defining a Kubernetes Job template that you can customize to match your environments. In particular, you can adjust:

  • backoffLimit: Keep it at 0 during pipeline development and bump it to 2 once your pipeline is ready to use. The backoff limit determines how many attempts will be performed in case of error.
  • activeDeadlineSeconds: 300 seconds should be plenty to complete a toolbox execution, but you might need to set it to a higher value if your servers are slow, very loaded or if your API contains a lot of operations.
  • image: You can use the community image on quay.io or the official Red Hat image if you are a Red Hat customer (3scale-amp26/toolbox).

The Job template is then patched with the supplied command-line arguments. The resulting job is stored as a YAML file and deployed in OpenShift using the oc command. Just after the oc command, the pipeline waits a couple of seconds to allow the Kubernetes Job to create the container and let it transition from the "ContainerCreated" to "Running" state. You can adjust this wait time according to your server velocity or even better, replace it with a polling loop. Finally, once the container transitioned to the "Running" state, the function fetches its logs and returns them to the caller.

As you can see, the Job receives two mount points:

  • A Kubernetes Secret for the 3scale toolbox configuration file that contains the remote list.
  • A Kubernetes ConfigMap for the artefacts to provision (OpenAPI Specification file, Application Plan file, etc.).

The ConfigMap will be created by the Jenkins Pipeline. The secret is provisioned outside of the pipeline and subject to RBAC.

Provision the 3scale-toolbox secret in the same OpenShift project as your Jenkins Master:

3scale remote add 3scale-instance "https://123...456@MY-TENANT-admin.3scale.net/"
oc create secret generic 3scale-toolbox --from-file="$HOME/.3scalerc.yaml"

Note: In the rest of this article, we are showcasing a 3scale hosted instance with hosted APIcast. When using self-managed APIcast or on-premises instance, some minor adjustments might be needed.

Preparing a very simple Jenkins Pipeline to deploy your API

On your Jenkins Master, create a new Pipeline and initiate the Pipeline scripts with some global variables (that you can later promote as pipeline parameters):

def targetSystemName = "saas-usecase-apikey"
def targetInstance = "3scale-instance"
def privateBaseURL = "http://echo-api.3scale.net"
def testUserKey = "azerty1234567890"
def developerAccountId = "john"
def publicStagingBaseURL = null
def publicProductionBaseURL = null

Those variables map directly to the well-known concepts of 3scale:

  • targetSystemName is the identifier (system_name) of the service to be created.
  • targetInstance is the name of the toolbox remote that you created above.
  • privateBaseURL is the Private Base URL of the service.
  • testUserKey is the User Key/API Key that will be used to perform an end-to-end test.
  • developerAccountId is the identifier of the default "Developer" account (or the username of its admin: john).
  • publicStagingBaseURL is the Public Staging Base URL of the service.
  • publicProductionBaseURL is the Public Production Base URL of the service.

Note: If you are using self-managed APIcast or an on-premise installation of 3scale, you would need to give a proper value to the publicStagingBaseURL and publicProductionBaseURL variables.

Jenkins Pipeline stages to deploy your API

In this section, we will implement the pipeline stages described in the previous article: 5 principles for deploying your API from a CI/CD pipeline.

Fetch the OpenAPI specification file

Add a pipeline stage to fetch your OpenAPI specification file and provide it as a ConfigMap on OpenShift:

node() {
  stage("Fetch OpenAPI") {
    sh """
    curl -sfk -o swagger.json https://raw.githubusercontent.com/microcks/api-lifecycle/master/beer-catalog-demo/api-contracts/beer-catalog-api-swagger.json
    oc delete configmap openapi --ignore-not-found
    oc create configmap openapi --from-file="swagger.json"
    """
  }

The OpenAPI specification contains the specifications needed to provision the API contract in 3scale:

  • Service metadata such as the name, version, maintainer, etc.
  • The payload schema.
  • The list of operations.
  • The security scheme used to secure the service.

Import the OpenAPI Specification file

Add a pipeline stage that uses 3scale toolbox to import the OpenAPI specification file into 3scale:

  stage("Import OpenAPI") {
    def tooboxArgs = [ "3scale", "import", "openapi", "-d", targetInstance, "/artifacts/swagger.json", "--override-private-base-url=${privateBaseURL}", "-t", targetSystemName ]
    if (publicStagingBaseURL !=null) {
      tooboxArgs += "--staging-public-base-url=${publicStagingBaseURL}"
    }
    if (publicProductionBaseURL !=null) {
      tooboxArgs += "--production-public-base-url=${publicProductionBaseURL}"
    }
    runToolbox(tooboxArgs)
  }

This stage uses some groovy sugar to dynamically set the toolbox command-line arguments if the Public Staging or Production Base URLs are set.

Create an application plan and an application

Add pipeline stages that use toolbox to create a 3scale application plan and an application:

  stage("Create an Application Plan") {
    runToolbox([ "3scale", "application-plan", "apply", targetInstance, targetSystemName, "test", "-n", "Test Plan", "--default" ])
  }

  stage("Create an Application") {
    runToolbox([ "3scale", "application", "apply", targetInstance, testUserKey, "--account=${developerAccountId}", "--name=Test Application", "--description=Created by Jenkins", "--plan=test", "--service=${targetSystemName}" ])
  }

Run the end-to-end tests

Add a stage that uses toolbox to run your end-to-end tests. To run the end-to-end tests when using 3scale hosted instances, you must fetch the proxy definition to extract the staging public URL; otherwise, you can directly reuse the publicStagingBaseURL variable defined above:

  stage("Run integration tests") {
    if (publicStagingBaseURL == null) {
      def proxyDefinition = runToolbox([ "3scale", "proxy", "show", targetInstance, targetSystemName, "sandbox" ])
      def proxy = readJSON text: proxyDefinition
      publicStagingBaseURL = proxy.content.proxy.sandbox_endpoint
    }

    sh """
    echo "Public Staging Base URL is ${publicStagingBaseURL}"
    echo "userkey is ${testUserKey}"
    curl -vfk ${publicStagingBaseURL}/beer -H 'api-key: ${testUserKey}'
    curl -vfk ${publicStagingBaseURL}/beer/Weissbier -H 'api-key: ${testUserKey}'
    curl -vfk ${publicStagingBaseURL}/beer/findByStatus/available -H 'api-key: ${testUserKey}'
    """
  }

Promote the configuration to the production gateway

Each 3scale service has two public URLs: one named "staging" and one named "production." The staging URL is used to test the settings before actually applying the configuration. When the configuration is promoted, it is applied to the "production" gateway atomically.

  stage("Promote to production") {
   runToolbox([ "3scale", "proxy", "promote", targetInstance,  targetSystemName ])
  }
}

Use the 3scale toolbox to deploy your API from a Jenkins Pipeline

Congratulations! You just deployed your API from a Jenkins Pipeline. The complete pipeline can be found here for reference.

This pipeline is far from perfect; nevertheless, it provides a solid basis for use in your pipelines. In the next article of this blog post series, we will discuss how some shortcomings of this pipeline can be enhanced: read Using the 3scale toolbox Jenkins Shared Library.

Read more

Last updated: January 14, 2022