Your CI/CD pipeline can be entirely defined in code, not just scripts, but the entire workflow, stages, and even approvals, using Jenkinsfile and YAML.
Let’s see this in action. Imagine a simple Python web app. We want to build it, run tests, and if everything passes, deploy it to a staging environment.
Here’s a snippet of a Jenkinsfile that orchestrates this:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
script {
sh 'docker build -t my-python-app:latest .'
}
}
}
stage('Test') {
steps {
script {
sh 'docker run my-python-app:latest pytest'
}
}
}
stage('Deploy to Staging') {
when {
branch 'main'
}
steps {
script {
// In a real scenario, this would involve more complex deployment logic
// like pushing to a registry and triggering a deployment tool.
echo 'Deploying to staging...'
sh 'echo "Simulating staging deployment for commit ${env.GIT_COMMIT}"'
}
}
}
stage('Approve for Production') {
when {
branch 'main'
}
steps {
input message: 'Approve deployment to production?'
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
steps {
script {
echo 'Deploying to production...'
sh 'echo "Simulating production deployment for commit ${env.GIT_COMMIT}"'
}
}
}
}
post {
always {
echo 'Pipeline finished.'
}
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
}
}
This Jenkinsfile describes a declarative pipeline, a structured way to define your CI/CD process. It breaks down the workflow into distinct stages: Checkout, Build, Test, Deploy to Staging, Approve for Production, and Deploy to Production. Each stage contains steps that are executed. For instance, the 'Build' stage uses a sh step to execute a docker build command. The 'Deploy to Staging' and 'Deploy to Production' stages have when conditions, ensuring they only run for the main branch. The 'Approve for Production' stage introduces a manual input step, pausing the pipeline for human approval before proceeding to production. The post section defines actions to be taken regardless of the pipeline’s outcome, or specifically on success or failure.
The real power comes when you integrate this with YAML for configuration management and externalizing parameters. For example, you might have a config.yaml file that defines environment-specific variables or deployment targets.
environments:
staging:
url: "http://staging.myapp.com"
deploy_script: "./scripts/deploy_staging.sh"
production:
url: "http://myapp.com"
deploy_script: "./scripts/deploy_production.sh"
docker_image_name: "my-python-app"
Your Jenkinsfile can then read this YAML file using a plugin like "Pipeline Utility Steps" or by directly processing the file content.
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Load Config') {
steps {
script {
// Assuming config.yaml is at the root of the repo
def config = readYaml file: 'config.yaml'
env.STAGING_URL = config.environments.staging.url
env.PROD_URL = config.environments.production.url
env.DOCKER_IMAGE = config.docker_image_name
}
}
}
stage('Build') {
steps {
script {
sh "docker build -t ${env.DOCKER_IMAGE}:latest ."
}
}
}
stage('Test') {
steps {
script {
sh "docker run ${env.DOCKER_IMAGE}:latest pytest"
}
}
}
stage('Deploy to Staging') {
when { branch 'main' }
steps {
script {
echo "Deploying to staging at ${env.STAGING_URL}..."
// sh "sh ${config.environments.staging.deploy_script} ${env.DOCKER_IMAGE}:latest"
sh 'echo "Simulating staging deployment to ${env.STAGING_URL}"'
}
}
}
stage('Approve for Production') {
when { branch 'main' }
steps {
input message: 'Approve deployment to production?'
}
}
stage('Deploy to Production') {
when { branch 'main' }
steps {
script {
echo "Deploying to production at ${env.PROD_URL}..."
// sh "sh ${config.environments.production.deploy_script} ${env.DOCKER_IMAGE}:latest"
sh 'echo "Simulating production deployment to ${env.PROD_URL}"'
}
}
}
}
post {
always {
echo 'Pipeline finished.'
}
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
}
}
This approach allows you to manage your entire CI/CD workflow, from code checkout to production deployment, as code. This means version control for your pipelines, easier collaboration, and the ability to test and roll back pipeline changes just like application code. The Jenkinsfile defines the what and the when, while external configuration files like config.yaml define the how and the where for different environments. This separation of concerns makes pipelines more maintainable and adaptable.
A common pattern is to use Jenkins Shared Libraries to house reusable pipeline logic, such as common build steps, deployment functions, or notification handlers. These libraries are essentially Groovy code stored in a separate Git repository, and they can be included in any Jenkinsfile, promoting DRY (Don’t Repeat Yourself) principles across your organization’s pipelines.
The most surprising aspect of pipeline-as-code is how it blurs the lines between infrastructure configuration and application development. You’re not just deploying applications; you’re deploying the process by which applications are deployed. This means your pipeline’s stability and correctness are as critical as your application’s. It also enables sophisticated strategies like blue-green deployments or canary releases to be defined declaratively within the Jenkinsfile itself, rather than being bolted on with ad-hoc scripting.
This shift allows for a much more robust and auditable CI/CD system, where every change to the deployment process is tracked, reviewed, and tested. The next step in mastering pipeline-as-code is often exploring advanced Jenkins features like parameterized builds and pipeline orchestration across multiple Jenkins instances.