Continuous Integration and Deployment (CI/CD): Learning Tutorial

Introduction

With 11 years specializing in Docker, Kubernetes, Terraform, and CI/CD automation, I ve worked on everything from single-repo MVPs to multi-team enterprise migrations. In this tutorial I share practical patterns, worked examples, and the exact commands and configurations I use in production to build reliable pipelines that reduce deployment risk and accelerate delivery.

This guide focuses on actionable techniques: concrete CI/CD YAML and build snippets, a JUnit/Testcontainers pattern to eliminate flaky integration tests, dependency-management checks with the Maven Enforcer plugin, containerized build examples, and a short Infrastructure-as-Code (IaC) primer using Terraform. Security, troubleshooting, and rollbacks are covered with pragmatic tips you can apply immediately.

By the end you'll have a clear checklist to implement a reproducible pipeline (build, test, package, deploy, monitor), plus sample code and configuration you can copy into your project and run locally or on CI.

Setting Up Your CI/CD Environment

Initial Setup and Configuration

Before diving into CI/CD, set up a reproducible environment. Use Git as your VCS (install from https://git-scm.com/) and create a repository with branch protections for main branches. Configure Git locally:


git config --global user.name 'Your Name'
git config --global user.email 'your.email@example.com'

What these commands do:

  • git config --global user.name: sets the author name used for commits
  • git config --global user.email: sets the email address used for commits

Choose a CI/CD tool that fits team skills and deployment targets. Example, supported versions used here as guidance: Jenkins 2.346.3 (requires a Java runtime), Docker 20.10.x for local builds, Kubernetes 1.24 for orchestration, and GitLab CI or GitHub Actions for hosted pipelines. Verify prerequisites such as Java with:

java -version
  • Choose a VCS (e.g., GitHub, GitLab)
  • Install Git: https://git-scm.com/
  • Set Git username and email
  • Select a CI/CD tool (e.g., Jenkins 2.346.3, GitLab CI)
  • Ensure Java (JDK appropriate for Jenkins) is installed

Key Takeaways

  • Pick tools aligned with team skills and deployment targets.
  • Verify and pin prerequisites; CI builds must run in repeatable environments.
  • Protect main branches and require pipeline success for merges.

Infrastructure as Code (IaC)

Given the importance of reproducible infrastructure alongside CI/CD, integrate IaC into your pipelines. Terraform is widely used and pairs well with pipelines: plan in CI, review plans, then apply from a controlled runner or CD job. Recommended versions for stability: Terraform 1.4+ (select a specific patch for your team), AWS provider v4+ where applicable.

Simple Terraform example: create an ECR repository and IAM role (HCL)

Keep Terraform state secure (remote state with locking) and run terraform fmt and terraform validate as part of CI.

provider "aws" {
  region = "us-east-1"
}

resource "aws_ecr_repository" "app" {
  name = "ci-cd-demo-app"
  image_tag_mutability = "MUTABLE"
}

resource "aws_iam_role" "codebuild_role" {
  name = "codebuild-ci-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = { Service = "codebuild.amazonaws.com" },
      Action = "sts:AssumeRole"
    }]
  })
}

IaC best practices (practical checklist)

  • Store remote state in a backend with locking (e.g., Terraform Cloud, S3 + DynamoDB locks).
  • Run terraform plan in CI and require human approval for destructive changes.
  • Use short-lived credentials for CI/CD runners when applying changes; avoid long-lived IAM keys.
  • Keep modules small and versioned; pin module versions in your required_providers block.
  • Scan plans for risky changes and include automated checks that fail the pipeline for unsafe operations.

Security & Troubleshooting for IaC

  • Encrypt secrets and state at rest; grant least privilege to state access.
  • If apply fails in CI, reproduce locally using the same Terraform and provider versions inside a container image.
  • Log apply output to an auditable store and keep a rollback plan (e.g., versioned module that can be re-applied to revert).

CI/CD Pipeline Architecture (Diagram)

This diagram shows a minimal, practical CI/CD flow: Commit -> Build -> Test -> Package -> Deploy -> Monitor. Use this as a baseline — each block maps to stages you'll configure in Jenkins, GitLab CI, or GitHub Actions.

CI/CD Pipeline Flow Developer commit to monitoring: build, test, deploy pipeline stages Commit Git Push Build Compile & Image Test Unit & Integration Deploy Canary/Prod
Figure: CI/CD pipeline flow from commit to deployment

Continuous Integration: Tools and Techniques

Popular CI Tools and Their Features

Pick tools that match team experience and target platform. Key versions used as examples in this article: Jenkins 2.346.3, Docker 20.10.x, Kubernetes 1.24, SonarQube 9.6. Validate plugin compatibility when upgrading.

Place CI configuration files at the repository root so the CI platform picks them up automatically (e.g., .gitlab-ci.yml, .travis.yml, .github/workflows/). After adding a pipeline configuration file, commit and push to trigger the first pipeline. Example workflow:

  1. Create the CI file in your project root (e.g., .gitlab-ci.yml).
  2. Commit: git add .gitlab-ci.yml && git commit -m "Add CI pipeline"
  3. Push: git push origin main
  4. Open your CI dashboard to see the pipeline run (GitLab/GitHub/Jenkins).

Example pipeline config files (place at repository root and commit to test):

Travis CI (.travis.yml) builds with OpenJDK 11 using Gradle:


language: java
jdk:
  - openjdk11
script:
  - ./gradlew build

GitLab CI (.gitlab-ci.yml) with simple build and test stages:


stages:
  - build
  - test

build:
  stage: build
  script:
    - echo Building the project

test:
  stage: test
  script:
    - echo Running tests

Note: 'echo Building the project' and 'echo Running tests' are placeholders. Replace them with your real build and test commands such as ./gradlew build, mvn -B package, pytest -q, or a containerized build step that builds and stores artifacts.

  • Define stages and let the CI-runner execute them sequentially by default.
  • Each job runs in a clean environment. Use artifacts to pass binaries between stages.
  • Tag runners and use tags in job definitions to target specific executors (e.g., Docker executor).

Key Takeaways

  • Keep pipeline configs in repo root for automatic detection.
  • Start simple; iterate and modularize pipelines as needs grow.
  • Use container images to standardize build/test environments.

Streamlining Deployment with Continuous Deployment

Automating Deployment Processes

Automate deployments to reduce manual steps and human error. Use progressive rollout strategies (canary, blue/green) and feature flags to limit blast radius. Integrate monitoring and automatic rollbacks when key metrics deviate.

Example Node.js deploy stage (add to .gitlab-ci.yml or equivalent):


stages:
  - build
  - test
  - deploy

deploy:
  stage: deploy
  script:
    - npm run deploy

The npm run deploy command usually executes a script defined in package.json to build, package, and push the application to a deployment target. Typical steps in such a script are: install dependencies, build the app, build and push a Docker image (e.g., docker build + docker push), and call deployment tools (kubectl, helm, or a cloud CLI) to update the cluster or service.

  1. Define deploy job that runs after build and test stages.
  2. Inject environment-specific secrets at runtime from your CI secret store or a managed secrets manager; never hardcode secrets in YAML.
  3. Run health checks and post-deploy smoke tests; fail the job and trigger rollback if checks fail.

Key Takeaways

  • Use progressive rollout strategies to reduce risk.
  • Include smoke tests and automated health checks in deployment jobs.
  • Automate rollback paths and practice them regularly.

Advanced Scenarios and Gotchas

This section provides concrete, real-world scenarios with resolutions and recommended tools/versions.

Flaky Tests Caused by Timing & Race Conditions

Issue: Integration tests fail intermittently in CI but pass locally. Causes include services not ready, state leakage, or parallel test interference.

  1. Run tests in isolated containers using Testcontainers (example stable release: Testcontainers 1.18.x) so each test gets a fresh DB and proper wait strategies.
  2. Use explicit wait strategies instead of blind sleeps; Testcontainers offers Wait.forListeningPort() and other ready checks.
  3. Reset DB schema between tests with migration tools or truncate tables in setup/teardown hooks.

Example JUnit 5 test using Testcontainers and PostgreSQL:

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class DbIntegrationTest {
  public static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:14.6")
    .withDatabaseName("testdb")
    .withUsername("test")
    .withPassword("test");

  @Test
  public void sampleTest() {
    postgres.start();
    // Run integration logic against postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()
    assertTrue(true);
    postgres.stop();
  }
}
  • Prefer class-level lifecycle management using @Testcontainers and @Container to avoid repeated container start/stop overhead.
  • Avoid Thread.sleep(); use container readiness checks.
  • Pin container image versions to avoid sudden upstream changes.

Dependency Hell: Conflicting Library Versions

Problem: Build fails in CI with NoSuchMethodError or ClassDefNotFoundError due to transitive dependency conflicts.

  1. Generate the dependency tree to inspect conflicts. For Maven: mvn dependency:tree -Dverbose. For Gradle: ./gradlew dependencies --configuration runtimeClasspath.
  2. Pin versions using <dependencyManagement> in Maven or force versions in Gradle.
  3. Use the Maven Enforcer plugin to fail builds when banned or conflicting dependencies are introduced.

Maven Enforcer plugin snippet:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
    <execution>
      <id>enforce-deps</id>
      <goals>
        <goal>enforce</goal>
      </goals>
      <configuration>
        <rules>
          <dependencyConvergence />
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>

Tip: Test dependency upgrades on a short-lived branch and run the full CI pipeline before merging.

Complex Rollbacks and Database Migrations

Problem: A DB migration breaks a deploy and rollback leaves schema and data inconsistent.

  1. Prefer backward-compatible, additive migrations.
  2. Gate schema-dependent features behind feature flags until migration completes.
  3. For destructive changes use a multi-step migration: compatibility layer -> migrate -> switch traffic -> cleanup.

Tools: Flyway or Liquibase for controlled, versioned migrations.

Security Insights for Advanced Workflows

  • Never store secrets in plaintext in repositories or pipeline logs; use HashiCorp Vault or cloud secret managers and inject secrets at runtime.
  • Enable secret masking in CI systems to prevent accidental exposure in logs.
  • Use least-privilege roles for CI service accounts and rotate tokens regularly.

Overcoming Common CI/CD Challenges

Addressing Integration and Testing Issues

Mitigate flaky tests via deterministic test design, containerized environments (Docker 20.10.x), and robust readiness checks. Use retries only when you have a documented, temporary flake and monitor the retry rate to avoid masking real issues.

Dependency management: use Maven, Gradle, npm, or yarn with lockfiles to ensure reproducible installs in CI and local environments.

Docker example (build and run locally):


docker build -t myapp:latest . && docker run myapp:latest
  1. docker build -t myapp:latest . builds the Docker image using the Dockerfile in the current directory and tags it myapp:latest.
  2. docker run myapp:latest starts a container from the built image.
  3. Use docker logs <container-id> and docker ps to inspect running containers and troubleshoot locally before changing CI.

Key Takeaways

  • Build deterministic tests and standardize environments with containers.
  • Pin and audit dependencies to avoid pipeline surprises.
  • Use local container tooling to reproduce CI failures.

Common Issues and Troubleshooting

Pipeline failed due to missing environment variables

Why: Required environment variables are not set in CI or differ between local and remote environments.

Solution:

  1. List required env variables in pipeline docs and set them in the CI provider's secret store.
  2. Do not commit .env files; include .env.example for local development.
  3. Add a validation job at pipeline start that checks for required variables and fails fast with a clear diagnostic.

Build failure due to dependency version conflict

Why: Differences in dependency resolution or transitive conflicts between environments.

Solution:

  1. Run dependency tree commands (mvn dependency:tree, ./gradlew dependencies).
  2. Pin versions via dependency management and add enforcement (Maven Enforcer).
  3. Run builds inside containers that mirror CI to reproduce issues locally.

Companion Examples Repository

To make this tutorial actionable, collect all snippets (JUnit/Testcontainers, Maven Enforcer pom excerpt, .gitlab-ci.yml examples, Dockerfile, pytest sample, and Terraform HCL) into a single repository so you can clone and run the examples end-to-end in CI. If you prefer, create a new repository locally and push the files shown in this article; the structure below is a recommended layout:

  • /java-testcontainers/: JUnit + Testcontainers sample
  • /maven-enforcer/: pom.xml with enforcer plugin
  • /gitlab-ci/: .gitlab-ci.yml examples
  • /docker/: Dockerfile and build scripts
  • /python-pytest/: pytest example
  • /terraform/: Terraform examples and backend config

Quick commands to initialize and push a repo with these examples:

git init
git add .
git commit -m "Add CI/CD examples"
# Create a GitHub repository manually and then:
# git remote add origin git@github.com:YOUR_USERNAME/ci-cd-examples.git
# git push -u origin main

Note: I do maintain companion examples in a public repository. To obtain the exact runnable files, check the author's GitHub profile or create the repository locally using the layout above and copy the snippets from this article into the appropriate folders.

Security tip: Do not commit secrets or state files; add .gitignore entries for local credentials and Terraform state.

Frequently Asked Questions

What is the best CI/CD tool for beginners?

Jenkins is popular and extensible; GitHub Actions and GitLab CI provide lower-friction onboarding for teams already using those platforms. Start with a single pipeline that builds and tests, then iterate.

How do I secure my CI/CD pipeline?

Use a secrets manager (HashiCorp Vault, AWS Secrets Manager), rotate credentials, enable least-privilege IAM, and mask secrets in logs. Add SAST and dependency scanning early in the pipeline.

How can I improve pipeline performance?

Parallelize independent tests, cache dependency artifacts (Maven/Gradle, npm), and measure stage durations to target slow steps for optimization.

Summary of Tools Mentioned

Tool Primary Function
Jenkins CI/CD server for automating builds and deployments
GitHub Actions Workflow automation directly within GitHub
Travis CI Continuous integration for GitHub projects
Docker Containerization for consistent application environments
Kubernetes Container orchestration for scalable deployments
AWS CodePipeline Automated release management for AWS applications
SonarQube Static code analysis for improving code quality
LaunchDarkly Feature flag management and experimentation
HashiCorp Vault Secret management for secure data handling

Conclusion

CI/CD is foundational to modern software delivery. With reproducible builds, automated tests, and controlled deployments (backed by IaC and monitoring), teams ship faster and with more confidence. Practice by assembling a small project with a Dockerfile, pipeline configuration, Terraform infra, and an automated smoke test stage; run it locally and then in a CI runner.

Further Resources

Ahmed Khalil

Ahmed Khalil is a DevOps Engineering Manager with 11 years of experience specializing in Docker, Kubernetes, Terraform, Jenkins, GitLab CI, AWS, and monitoring. He has led teams implementing CI/CD automation and cloud migrations to improve deployment frequency and reliability.


Published: Sep 02, 2025 | Updated: Jan 05, 2026