<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Jenkins - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Jenkins - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 14 Jun 2026 14:44:56 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/jenkins/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How I Built a Production-Ready CI/CD Pipeline for a Monorepo-Based Microservices System with Jenkins, Docker Compose, and Traefik ]]>
                </title>
                <description>
                    <![CDATA[ This tutorial is a complete, real-world guide to building a production-ready CI/CD pipeline using Jenkins, Docker Compose, and Traefik on a single Linux server. You’ll learn how to expose services on  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-production-ready-ci-cd-pipeline-for-monorepo-based-microservices-system/</link>
                <guid isPermaLink="false">69ea60c8904b915438a58ca2</guid>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ci-cd ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Traefik ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Md Tarikul Islam ]]>
                </dc:creator>
                <pubDate>Thu, 23 Apr 2026 18:11:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/66cb39fcaa2a09f9a8d691c1/d59c62f5-e376-4f09-851f-83e437f9960a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>This tutorial is a complete, real-world guide to building a production-ready CI/CD pipeline using Jenkins, Docker Compose, and Traefik on a single Linux server.</p>
<p>You’ll learn how to expose services on a custom domain with auto-renewing HTTPS, and implement a smart deployment strategy that detects changes and redeploys only the affected microservices. This helps avoid unnecessary full-stack redeploys. We'll also cover real production issues and the exact fixes for each one.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-1-what-youll-build">1. What you'll build</a></p>
</li>
<li><p><a href="#heading-2-architecture">2. Architecture</a></p>
</li>
<li><p><a href="#heading-3-server-prerequisites">3. Server prerequisites</a></p>
</li>
<li><p><a href="#heading-4-traefik-the-reverse-proxy">4. Traefik — the reverse proxy</a></p>
</li>
<li><p><a href="#heading-5-run-jenkins-in-docker">5. Run Jenkins in Docker</a></p>
</li>
<li><p><a href="#heading-6-expose-jenkins-on-a-domain-via-traefik">6. Expose Jenkins on a domain via Traefik</a></p>
</li>
<li><p><a href="#heading-7-first-time-jenkins-setup">7. First-time Jenkins setup</a></p>
</li>
<li><p><a href="#heading-8-add-the-github-credential">8. Add the GitHub credential</a></p>
</li>
<li><p><a href="#heading-9-create-the-pipeline-job">9. Create the pipeline job</a></p>
</li>
<li><p><a href="#heading-10-the-jenkinsfile-deploy-only-what-changed">10. The Jenkinsfile (deploy only what changed)</a></p>
</li>
<li><p><a href="#heading-11-end-to-end-test">11. End-to-end test</a></p>
</li>
<li><p><a href="#heading-12-troubleshooting-every-error-we-hit">12. Troubleshooting — every error we hit</a></p>
</li>
<li><p><a href="#heading-13-mental-model-host-vs-container">13. Mental model: host vs. container</a></p>
</li>
<li><p><a href="#heading-14-daily-operations-cheat-sheet">14. Daily operations cheat sheet</a></p>
</li>
<li><p><a href="#heading-15-what-id-do-differently-next-time">15. What I'd do differently next time</a></p>
</li>
<li><p><a href="#heading-closing-thoughts">Closing thoughts</a></p>
</li>
</ul>
<h2 id="heading-1-what-youll-build">1. What You'll Build</h2>
<p>In this tutorial, you'll build a Jenkins instance running inside Docker on the same Linux server as your application stack.</p>
<p>Traefik will act as a reverse proxy in front of Jenkins, exposing it via a clean URL (<a href="https://jenkins.example.com"><code>https://jenkins.example.com</code></a>) with <strong>auto-renewing Let's Encrypt certificates</strong>.</p>
<p>You'll also create a Jenkinsfile in your application repository that:</p>
<ul>
<li><p>Automatically triggers on every push to the <code>staging</code> branch,</p>
</li>
<li><p>Detects which microservices changed in each commit,</p>
</li>
<li><p>Pulls the latest code on the host machine,</p>
</li>
<li><p>Rebuilds and restarts <strong>only the affected services</strong>.</p>
</li>
</ul>
<p>On every push, only the relevant services are redeployed.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before jumping in, this guide assumes you’re already comfortable with a few core concepts and tools.</p>
<p>This isn't a beginner-level tutorial — we’ll be working directly with infrastructure, containers, and CI/CD pipelines.</p>
<p>You should be familiar with:</p>
<ul>
<li><p>Basic Linux commands (SSH, file system navigation, permissions)</p>
</li>
<li><p>Docker fundamentals (images, containers, volumes, networks)</p>
</li>
<li><p>Git workflows (clone, pull, branches)</p>
</li>
<li><p>General idea of CI/CD pipelines</p>
</li>
</ul>
<p>Tools and environment required:</p>
<ul>
<li><p>A Linux server (Ubuntu recommended)</p>
</li>
<li><p>Docker Engine + Docker Compose (v2)</p>
</li>
<li><p>A domain name (for Traefik + HTTPS)</p>
</li>
<li><p>GitHub repository (for your backend project)</p>
</li>
<li><p>Basic understanding of microservices architecture</p>
</li>
</ul>
<p>If you’re comfortable with the above, you’re ready to follow along.</p>
<h2 id="heading-2-architecture">2. Architecture</h2>
<p>Here's an overview of the architecture:</p>
<pre><code class="language-plaintext">┌──────────────────────────── Linux server (Ubuntu) ────────────────────────────┐
│                                                                               │
│   /home/developer/projects/                                                  │
│       └── project-prod-configs/             ← infra repo (compose, Traefik) │
│              ├── docker-compose.staging.yml                                   │
│              ├── traefik.staging.yml                                          │
│              └── project-backend/          ← app repo (services, gateways) │
│                     ├── Jenkinsfile                                           │
│                     ├── docker-compose.staging.yml                            │
│                     └── apps/                                                 │
│                            ├── services/&lt;name&gt;/                               │
│                            ├── gateways/&lt;name&gt;/                               │
│                            └── core/&lt;name&gt;/                                   │
│                                                                               │
│   ┌─────────────────────── Docker network: proxy ──────────────────────┐      │
│   │  traefik (80, 443)                                                 │      │
│   │     │                                                              │      │
│   │     ├──► jenkins  (projects-jenkins-staging)                     │      │
│   │     │      ↳ /projects  ← bind-mount of the host project tree     │      │
│   │     │      ↳ /var/run/docker.sock ← controls host Docker           │      │
│   │     │                                                              │      │
│   │     └──► your services &amp; gateways (built by the pipeline)          │      │
│   └────────────────────────────────────────────────────────────────────┘      │
│                                                                               │
└───────────────────────────────────────────────────────────────────────────────┘
            ▲
            │  webhook on push
            │
   GitHub: &lt;org&gt;/project-backend (branch: staging)
</code></pre>
<p>There are two key ideas here:</p>
<ol>
<li><p><strong>Jenkins runs in a container</strong>, but it controls the <strong>host's</strong> Docker by mounting <code>/var/run/docker.sock</code>. It also bind-mounts the project folder as <code>/projects/...</code>, so it can <code>cd</code> into the real code on the host and run <code>docker compose</code> there.</p>
</li>
<li><p>The <strong>Jenkinsfile lives inside the app repo</strong>, so the pipeline definition is versioned with the code. Jenkins simply points at it.</p>
</li>
</ol>
<h3 id="heading-3-server-prerequisites">3. Server Prerequisites</h3>
<p>Before we start configuring Jenkins or Traefik, we need to prepare the server properly.</p>
<p>In this step, we’ll:</p>
<ul>
<li><p>Create a dedicated Linux user for managing the project</p>
</li>
<li><p>Install Docker and Docker Compose</p>
</li>
<li><p>Set up the folder structure for our repositories</p>
</li>
</ul>
<p>This ensures our CI/CD pipeline runs in a clean and predictable environment.</p>
<pre><code class="language-bash"># Linux user that owns the project tree
sudo adduser developer

# Docker engine + Compose plugin
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker developer

# Sanity check Compose v2
docker compose version
# -&gt; Docker Compose version v2.x.y

# Find where the Compose plugin binary lives — write it down, you'll need it
ls /usr/libexec/docker/cli-plugins/docker-compose
# (some distros use /usr/lib/docker/cli-plugins/docker-compose)

# Project layout
sudo mkdir -p /home/developer/project
sudo chown -R developer:developer /home/developer/project

# Clone both repos in the right place
cd /home/developer/projects
git clone https://github.com/&lt;org&gt;/projects-prod-configs.git
cd projects-prod-configs
git clone -b staging https://github.com/&lt;org&gt;/projects-backend.git
</code></pre>
<p>You should now have:</p>
<pre><code class="language-plaintext">/home/developer/projects/projects-prod-configs/projects-backend
</code></pre>
<p>Memorize this path — your Jenkinsfile references it.</p>
<h3 id="heading-dns">DNS</h3>
<p>Point an A-record for your Jenkins subdomain to the server's public IP <strong>before</strong> the next steps so Let's Encrypt can validate via HTTP challenge:</p>
<pre><code class="language-plaintext">jenkins.example.com   A   &lt;server-public-ip&gt;
</code></pre>
<h2 id="heading-4-traefik-the-reverse-proxy">4. Traefik — the Reverse Proxy</h2>
<p>Traefik acts as the entry point to your entire system. Instead of exposing each service manually with ports, Traefik automatically:</p>
<ul>
<li><p>Routes traffic based on domain names</p>
</li>
<li><p>Generates and renews HTTPS certificates using Let’s Encrypt</p>
</li>
<li><p>Connects to Docker and detects services dynamically</p>
</li>
</ul>
<p>In simple terms, Traefik lets you access services like:</p>
<p><a href="https://jenkins.example.com">https://jenkins.example.com</a><br><a href="https://api.example.com">https://api.example.com</a></p>
<p>…without manually configuring NGINX or managing SSL certificates.</p>
<p>In this setup, Traefik watches Docker containers and routes traffic using labels we'll define later.</p>
<p>Traefik gives every container a real domain and a real cert with <strong>zero per-service config</strong> — you just add a few labels.</p>
<h3 id="heading-traefikstagingyml-static-config"><code>traefik.staging.yml</code> (static config)</h3>
<p>Put this at the root of your infra repo:</p>
<pre><code class="language-yaml">api:
  dashboard: true

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      httpChallenge:
        entryPoint: web
      email: admin@example.com           # ← change me
      storage: /etc/traefik/acme.json

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false              # only containers with traefik.enable=true
    network: proxy
  file:
    directory: /etc/traefik/dynamic
    watch: true

log:
  level: INFO

accessLog: {}
</code></pre>
<h3 id="heading-the-traefik-service-in-docker-composestagingyml">The Traefik service in <code>docker-compose.staging.yml</code></h3>
<pre><code class="language-yaml">networks:
  proxy:
    name: proxy
    driver: bridge
  internal:
    name: internal
    driver: bridge

volumes:
  acme-data:
  traefik-logs:
  jenkins-data:

services:
  traefik:
    image: traefik:v2.11
    container_name: projects-traefik-staging
    restart: unless-stopped
    ports:
      - "80:80"        # HTTP (auto-redirects to HTTPS)
      - "443:443"      # HTTPS
      - "8080:8080"    # Traefik dashboard (internal only — protect via firewall)
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.staging.yml:/etc/traefik/traefik.yml:ro
      - ./dynamic:/etc/traefik/dynamic:ro
      - acme-data:/etc/traefik           # persists Let's Encrypt certs
      - traefik-logs:/var/log/traefik
    networks:
      - proxy
    command:
      - '--api.insecure=false'
      - '--api.dashboard=true'
      - '--providers.docker=true'
      - '--providers.docker.exposedbydefault=false'
      - '--providers.docker.network=proxy'
      - '--entrypoints.web.address=:80'
      - '--entrypoints.websecure.address=:443'
      - '--entrypoints.web.http.redirections.entryPoint.to=websecure'
      - '--entrypoints.web.http.redirections.entryPoint.scheme=https'
      - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
      - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
      - '--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-admin@example.com}'
      - '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'
      - '--log.level=INFO'
      - '--accesslog=true'
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      # Traefik's own dashboard
      - "traefik.http.routers.traefik-dash.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik-dash.entrypoints=websecure"
      - "traefik.http.routers.traefik-dash.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik-dash.service=api@internal"
</code></pre>
<p>Bring it up:</p>
<pre><code class="language-bash">cd /home/developer/projects/projects-prod-configs
docker compose -f docker-compose.staging.yml up -d traefik
</code></pre>
<p>Watch the logs the first time — Traefik will request a cert for the dashboard host as soon as DNS resolves.</p>
<pre><code class="language-bash">docker logs -f projects-traefik-staging
</code></pre>
<p><strong>Tip.</strong> While testing, switch ACME to staging endpoint (<code>acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory</code>) so you don't burn through Let's Encrypt's rate limits if you misconfigure DNS. Remove that flag before going live.</p>
<h2 id="heading-5-run-jenkins-in-docker">5. Run Jenkins in Docker</h2>
<p>Add this Jenkins service to the same <code>docker-compose.staging.yml</code>. Every line matters (and the comments explain why).</p>
<pre><code class="language-yaml">  jenkins:
    image: jenkins/jenkins:lts
    container_name: projects-jenkins-staging
    restart: unless-stopped
    user: root                           # to use host docker.sock without UID juggling
    environment:
      - JAVA_OPTS=-Xmx1g -Xms512m -Duser.timezone=Asia/Dhaka
      - TZ=Asia/Dhaka                    # OS-level timezone inside container
      - JENKINS_OPTS=--prefix=/
    ports:
      - "3095:8080"                      # web UI (also reachable directly if needed)
      - "50000:50000"                    # inbound agent port
    volumes:
      - jenkins-data:/var/jenkins_home   # Jenkins config/jobs/secrets persistence
      - /var/run/docker.sock:/var/run/docker.sock                          # control host Docker
      - /usr/bin/docker:/usr/bin/docker                                     # docker CLI from host
      - /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro  # docker compose plugin
      - /home/developer/projects:/projects                                # project tree
      - /etc/localtime:/etc/localtime:ro                                    # match host clock
      - /etc/timezone:/etc/timezone:ro
    networks:
      - proxy
      - internal
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8080/login']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 120s
    deploy:
      resources:
        limits:
          memory: 1024M
</code></pre>
<p><strong>Why</strong> <code>user: root</code><strong>?</strong> It's the simplest way to share <code>docker.sock</code> and the project bind-mount without UID/GID gymnastics. If you prefer an unprivileged user, you'll need to set <code>group: docker</code> and align UIDs/perms on host folders — possible but out of scope here.</p>
<h2 id="heading-6-expose-jenkins-on-a-domain-via-traefik">6. Expose Jenkins on a Domain via Traefik</h2>
<p>This is the section many guides skip. We'll add <strong>labels</strong> to the Jenkins service so Traefik picks it up automatically. No editing of Traefik config required.</p>
<pre><code class="language-yaml">  jenkins:
    # ... everything above ...
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"

      # 1) Router — match incoming Host
      - "traefik.http.routers.jenkins.rule=Host(`jenkins.example.com`)"
      - "traefik.http.routers.jenkins.entrypoints=websecure"
      - "traefik.http.routers.jenkins.tls.certresolver=letsencrypt"
      - "traefik.http.routers.jenkins.service=jenkins"

      # 2) Service — tell Traefik which container port is the app
      - "traefik.http.services.jenkins.loadbalancer.server.port=8080"

      # 3) Middleware — Jenkins needs X-Forwarded-Proto so it knows it's behind HTTPS
      - "traefik.http.middlewares.jenkins-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.routers.jenkins.middlewares=jenkins-headers"
</code></pre>
<p>What each line does:</p>
<table>
<thead>
<tr>
<th>Label</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>traefik.enable=true</code></td>
<td>Opts this container in (we set <code>exposedByDefault=false</code>).</td>
</tr>
<tr>
<td><code>traefik.docker.network=proxy</code></td>
<td>Tells Traefik which network to talk to Jenkins on (Jenkins is on both <code>proxy</code> and <code>internal</code>).</td>
</tr>
<tr>
<td><code>routers.jenkins.rule=Host(...)</code></td>
<td>Forwards only this hostname to Jenkins.</td>
</tr>
<tr>
<td><code>routers.jenkins.entrypoints=websecure</code></td>
<td>Listens only on 443. (HTTP redirect was set up in section 4.)</td>
</tr>
<tr>
<td><code>routers.jenkins.tls.certresolver=letsencrypt</code></td>
<td>Auto-issues + renews the cert.</td>
</tr>
<tr>
<td><code>services.jenkins.loadbalancer.server.port=8080</code></td>
<td>Jenkins listens on 8080 inside the container.</td>
</tr>
<tr>
<td><code>customrequestheaders.X-Forwarded-Proto=https</code></td>
<td>Without this, Jenkins generates <code>http://</code> URLs in webhooks/links and breaks.</td>
</tr>
</tbody></table>
<p>Bring Jenkins up:</p>
<pre><code class="language-bash">cd /home/developer/projects/projects-prod-configs
docker compose -f docker-compose.staging.yml up -d jenkins

# Watch Traefik issue the certificate
docker logs -f projects-traefik-staging | grep -i acme
</code></pre>
<p>After 10–60 seconds you should be able to open <code>https://jenkins.example.com</code> and see Jenkins's setup wizard with a valid lock icon.</p>
<p>Inside Jenkins (after first login):</p>
<p>Manage Jenkins → System → Jenkins URL → set this to: <a href="https://jenkins.example.com/">https://jenkins.example.com/</a></p>
<p>This is important because Jenkins uses this base URL to generate:</p>
<ul>
<li><p>Webhook endpoints (for GitHub triggers)</p>
</li>
<li><p>Links inside emails and build logs</p>
</li>
</ul>
<p>If this isn't set correctly, GitHub webhooks may fail, and any links Jenkins generates will point to the wrong address (often localhost or internal IPs).</p>
<h2 id="heading-7-first-time-jenkins-setup">7. First-Time Jenkins Setup</h2>
<p>If you're running Jenkins for the first time on this server, follow this section to complete the initial setup.</p>
<p>If you already have Jenkins configured, you can skip this section — but make sure the required plugins and settings match what we use later in this guide.</p>
<ol>
<li><p>Open <code>https://jenkins.example.com</code>. Get the initial admin password:</p>
<pre><code class="language-bash">docker exec projects-jenkins-staging cat /var/jenkins_home/secrets/initialAdminPassword
</code></pre>
</li>
<li><p>Paste it, choose Install suggested plugins.</p>
</li>
<li><p>Create your admin user.</p>
</li>
<li><p>Manage Jenkins → Plugins → Available and install:</p>
<ul>
<li><p>GitHub (and GitHub Branch Source)</p>
</li>
<li><p>Pipeline: GitHub</p>
</li>
<li><p>Credentials Binding (usually preinstalled)</p>
</li>
</ul>
</li>
</ol>
<p>That's all the plugins you need for the rest of this guide.</p>
<h2 id="heading-8-add-the-github-credential">8. Add the GitHub Credential</h2>
<p>Jenkins needs permission to access your GitHub repository.</p>
<p>This is done using a GitHub Personal Access Token (PAT), which acts like a password for secure API and Git operations.</p>
<p>We’ll store this token inside Jenkins as a credential so it can pull code during pipeline execution and authenticate securely without exposing secrets in code.</p>
<p>This single credential is used both for the SCM checkout and for the deploy-time <code>git pull</code>.</p>
<ol>
<li><p>Create a Personal Access Token (classic) on GitHub with <code>repo</code> scope.</p>
</li>
<li><p>In Jenkins: Manage Jenkins → Credentials → System → Global → Add Credentials.</p>
</li>
<li><p>Fill in:</p>
<ul>
<li><p>Kind: Username with password</p>
</li>
<li><p>Username: your GitHub username</p>
</li>
<li><p>Password: the token</p>
</li>
<li><p><strong>ID:</strong> <code>github_classic_token</code> <em>(the Jenkinsfile references this exact ID)</em></p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-9-create-the-pipeline-job">9. Create the Pipeline Job</h2>
<p>Now that Jenkins has access to your repository, the next step is to define how deployments should run.</p>
<p>A pipeline job tells Jenkins:</p>
<ul>
<li><p>where your code lives,</p>
</li>
<li><p>which branch to monitor,</p>
</li>
<li><p>and how to execute your deployment process.</p>
</li>
</ul>
<p>In Jenkins, create a new Pipeline job and connect it to your GitHub repository. Once this is set up, Jenkins will automatically trigger deployments whenever you push to the <code>staging</code> branch.</p>
<p>Start by creating a new job:</p>
<p>New Item → Pipeline → name it <code>projects-staging</code> → OK</p>
<p>Then configure the job:</p>
<ul>
<li><p>Under <strong>Build Triggers</strong>, enable:<br><strong>GitHub hook trigger for GITScm polling</strong></p>
</li>
<li><p>Under <strong>Pipeline</strong>:</p>
<ul>
<li><p>Definition: Pipeline script from SCM</p>
</li>
<li><p>SCM: Git</p>
</li>
<li><p>Repository URL: <code>https://github.com/&lt;org&gt;/projects-backend.git</code></p>
</li>
<li><p>Credentials: <code>github_classic_token</code></p>
</li>
<li><p>Branch: <code>*/staging</code></p>
</li>
<li><p>Script Path: <code>Jenkinsfile</code></p>
</li>
</ul>
</li>
</ul>
<p>Save the configuration.</p>
<p>At this point, Jenkins is fully connected to your repository and ready to run your deployment pipeline automatically.</p>
<h2 id="heading-10-the-jenkinsfile-deploy-only-what-changed">10. The Jenkinsfile (Deploy Only What Changed)</h2>
<p>Place this at the root of the <strong>app</strong> repo (<code>projects-backend/Jenkinsfile</code>), branch <code>staging</code>.</p>
<pre><code class="language-groovy">pipeline {
  agent any

  environment {
    PROJECT_PATH = "/projects/projects-prod-configs/projects-backend"
    COMPOSE_FILE = "docker-compose.staging.yml"
  }

  stages {

    stage('Checkout') {
      steps {
        checkout scm
        echo "Checkout completed for branch: ${env.BRANCH_NAME ?: 'staging'}"
      }
    }

    stage('Detect Changes') {
      steps {
        script {
          def changedFiles = sh(
            script: "git diff --name-only HEAD~1 HEAD",
            returnStdout: true
          ).trim()

          echo "Changed files:\n${changedFiles}"

          def services = [] as Set
          changedFiles.split('\n').each { file -&gt;
            def svc  = file =~ /^apps\/services\/([a-z0-9-]+)\//
            def gw   = file =~ /^apps\/gateways\/([a-z0-9-]+)\//
            def core = file =~ /^apps\/core\/([a-z0-9-]+)\//
            if (svc)  { services &lt;&lt; svc[0][1]  }
            if (gw)   { services &lt;&lt; gw[0][1]   }
            if (core) { services &lt;&lt; core[0][1] }
          }
          services = services.findAll { !it.endsWith('-e2e') }
          env.CHANGED_SERVICES = services.join(' ')

          echo "Services to deploy: ${env.CHANGED_SERVICES ?: '(none)'}"
        }
      }
    }

    stage('Deploy') {
      when { expression { return env.CHANGED_SERVICES?.trim() } }
      steps {
        withCredentials([usernamePassword(
          credentialsId: 'github_classic_token',
          usernameVariable: 'GIT_USER',
          passwordVariable: 'GIT_TOKEN'
        )]) {
          sh '''
            set -eu
            git config --global --add safe.directory "${PROJECT_PATH}"
            cd "${PROJECT_PATH}"
            git remote set-url origin "https://github.com/&lt;org&gt;/projects-backend.git"
            git -c credential.helper= \
                -c "credential.helper=!f() { echo username=\({GIT_USER}; echo password=\){GIT_TOKEN}; }; f" \
                pull origin staging
            docker compose -f "\({COMPOSE_FILE}" up -d --build \){CHANGED_SERVICES}
          '''
        }
        echo "Deployed: ${env.CHANGED_SERVICES}"
      }
    }

    stage('Skip Deployment') {
      when { expression { return !env.CHANGED_SERVICES?.trim() } }
      steps { echo "No service changes detected — nothing to deploy." }
    }
  }
}
</code></pre>
<p>Why each tricky line is there:</p>
<ul>
<li><p><code>git config --global --add safe.directory ...</code> — git refuses to operate on a repo whose owner UID differs from the current user's. The repo on disk is owned by <code>developer</code>, but Git inside the container runs as <code>root</code>. This whitelists the path.</p>
</li>
<li><p><code>git remote set-url origin "https://..."</code> — flips the on-disk remote to HTTPS so the <strong>token can be used</strong>. (A PAT can't authenticate <code>git@github.com:</code> URLs — those use SSH.) Idempotent — safe to re-run.</p>
</li>
<li><p><code>git -c credential.helper="!f() { echo username=...; echo password=...; }; f"</code> — feeds the username/token to git for that one command without writing the token to disk and without exposing it on the process command line.</p>
</li>
<li><p><code>${CHANGED_SERVICES}</code> is unquoted on purpose so multiple service names expand as separate args.</p>
</li>
</ul>
<h2 id="heading-11-end-to-end-test">11. End-to-End Test</h2>
<p>Before considering the setup complete, we need to verify that the entire pipeline works as expected.</p>
<p>This end-to-end test ensures that:</p>
<ul>
<li><p>GitHub webhooks are triggering Jenkins correctly,</p>
</li>
<li><p>Jenkins can detect which services changed,</p>
</li>
<li><p>and only the affected services are rebuilt and deployed.</p>
</li>
</ul>
<p>In other words, this simulates a real production deployment.</p>
<p>Start by making a small change in your repository. For example, modify a file inside:</p>
<p>apps/gateways/student-apigw/</p>
<p>Then push the change to the <code>staging</code> branch.</p>
<p>Once pushed, Jenkins should automatically trigger via the webhook. If not, you can manually click <strong>Build Now</strong>.</p>
<p>Now open the build’s <strong>Console Output</strong> and verify the flow. You should see something like:</p>
<ul>
<li><p>Checkout completed for branch: staging</p>
</li>
<li><p>Services to deploy: student-apigw</p>
</li>
<li><p>git pull origin staging (successful)</p>
</li>
<li><p>docker compose ... up -d --build student-apigw</p>
</li>
<li><p>Deployed: student-apigw</p>
</li>
</ul>
<p>If you see this sequence, your pipeline is working correctly.</p>
<p>If anything fails, don’t worry — jump to Section 12 where every common issue and its fix is documented.</p>
<h2 id="heading-12-troubleshooting-every-error-we-hit">12. Troubleshooting — Every Error We Hit</h2>
<p>This section covers real issues we faced while setting up this pipeline — and more importantly, <em>why each fix works</em>. Understanding the “why” will help you debug similar problems in your own setup.</p>
<h3 id="heading-cd-cant-cd-to-projectsprojects-prod-configsprojects-backend">cd: can't cd to /projects/projects-prod-configs/projects-backend</h3>
<p><strong>Cause:</strong><br>The Jenkinsfile runs <code>cd $PROJECT_PATH</code>, but inside the container that path doesn’t exist. This usually happens when:</p>
<ul>
<li><p>the project wasn’t cloned on the host, or</p>
</li>
<li><p>the bind mount isn’t configured correctly.</p>
</li>
</ul>
<p><strong>Fix:</strong></p>
<pre><code class="language-bash">ls /home/developer/projects/projects-prod-configs/projects-backend
# If missing: git clone -b staging &lt;url&gt; there.
</code></pre>
<p>Confirm the bind mount:</p>
<pre><code class="language-plaintext">docker inspect projects-jenkins-staging --format '{{range .Mounts}}{{.Source}} -&gt; {{.Destination}}{{println}}{{end}}'
</code></pre>
<p>If missing, recreate the container:</p>
<pre><code class="language-plaintext">docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins
</code></pre>
<p><strong>Why this works:</strong></p>
<p>Jenkins runs inside a container, but your code lives on the host. The bind mount connects them. Without it, Jenkins cannot access your project directory.</p>
<h3 id="heading-fatal-detected-dubious-ownership-in-repository">fatal: detected dubious ownership in repository</h3>
<p><strong>Cause:</strong><br>Git blocks access when the repository owner differs from the current user.</p>
<ul>
<li><p>Repo owner: <code>developer</code> (host)</p>
</li>
<li><p>Git runs as: <code>root</code> (inside container)</p>
</li>
</ul>
<p><strong>Fix:</strong></p>
<pre><code class="language-plaintext">git config --global --add safe.directory "${PROJECT_PATH}"
</code></pre>
<p><strong>Why this works:</strong></p>
<p>This explicitly tells Git that the directory is trusted, bypassing ownership mismatch security restrictions.</p>
<h3 id="heading-host-key-verification-failed-could-not-read-from-remote-repository"><code>Host key verification failed</code> / <code>Could not read from remote repository</code></h3>
<h4 id="heading-cause">Cause:</h4>
<p>The repository uses SSH (<code>git@github.com:...</code>), but:</p>
<ul>
<li><p>the container has no SSH keys</p>
</li>
<li><p>no known_hosts file exists</p>
</li>
</ul>
<p>Also, GitHub tokens cannot authenticate over SSH.</p>
<p><strong>Fix (recommended):</strong></p>
<pre><code class="language-plaintext">git remote set-url origin "https://github.com/&lt;org&gt;/projects-backend.git"
</code></pre>
<p><strong>Why this works:</strong></p>
<p>HTTPS uses token-based authentication (PAT), which works inside containers without SSH configuration.</p>
<h3 id="heading-unknown-shorthand-flag-f-in-f-docker-compose"><code>unknown shorthand flag: 'f' in -f</code> ( <code>docker compose</code>)</h3>
<p><strong>Cause:</strong><br>The Docker CLI exists, but the Docker Compose plugin is missing inside the container.</p>
<p><strong>Fix:</strong></p>
<pre><code class="language-plaintext">volumes:
  - /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro
</code></pre>
<p>Find your path if needed:</p>
<pre><code class="language-plaintext">find /usr -name docker-compose -type f 2&gt;/dev/null
</code></pre>
<p>Verify:</p>
<pre><code class="language-plaintext">docker exec projects-jenkins-staging docker compose version
</code></pre>
<p><strong>Why this works:</strong></p>
<p>Docker Compose v2 is a CLI plugin. Mounting this directory makes the <code>docker compose</code> command available inside the container.</p>
<h3 id="heading-wrong-timezone-in-build-timestamps-and-jenkins-ui">Wrong timezone in build timestamps and Jenkins UI</h3>
<p><strong>Fix:</strong> Set both env var and JVM flag, and bind-mount the host's clock files:</p>
<pre><code class="language-yaml">environment:
  - TZ=Asia/Dhaka
  - JAVA_OPTS=... -Duser.timezone=Asia/Dhaka
volumes:
  - /etc/localtime:/etc/localtime:ro
  - /etc/timezone:/etc/timezone:ro
</code></pre>
<p>You <strong>must</strong> recreate the container for env-var changes to take effect:</p>
<pre><code class="language-bash">docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins
</code></pre>
<p><strong>Why this works:</strong><br>Jenkins runs on Java, which uses its own timezone separate from the OS.<br>By aligning OS timezone, JVM timezone, and host clock, you ensure consistent timestamps everywhere.</p>
<h3 id="heading-errsockettimeout-pnpm-install-fails">ERR_SOCKET_TIMEOUT (pnpm install fails)</h3>
<h4 id="heading-cause">Cause:</h4>
<p>If you have multiple services building in parallel and each runs pnpm install with ~1500 packages, the network gets saturated and a timeout occurs.</p>
<h4 id="heading-fixes">Fixes:</h4>
<p>a) Increase timeout + control concurrency</p>
<pre><code class="language-xml">RUN pnpm install --frozen-lockfile --ignore-scripts 
--network-timeout 600000 
--network-concurrency 8
</code></pre>
<p>Why: Gives pnpm more time and reduces network overload.</p>
<p>b) Enable pnpm cache (BuildKit)</p>
<pre><code class="language-xml">RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store 
pnpm install --frozen-lockfile --ignore-scripts
</code></pre>
<p>Why: Dependencies are cached and reused instead of downloading every time.</p>
<p>c) Avoid unnecessary rebuilds</p>
<pre><code class="language-xml">docker compose -f \(COMPOSE_FILE build \)CHANGED_SERVICES docker compose -f \(COMPOSE_FILE up -d --no-build \)CHANGED_SERVICES
</code></pre>
<p>Why: Only changed services are rebuilt → less network load → fewer failures.</p>
<h3 id="heading-container-changes-dont-apply-after-editing-docker-composeyml">Container changes don’t apply after editing docker-compose.yml</h3>
<h4 id="heading-cause">Cause:</h4>
<p>Docker compose up -d does not update running containers.</p>
<h4 id="heading-fix">Fix:</h4>
<pre><code class="language-xml">docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins
</code></pre>
<p><strong>Why this works:</strong></p>
<p>This forces Docker to recreate the container with updated configuration (env, volumes, labels).</p>
<h3 id="heading-traefik-shows-default-certificate-no-https">Traefik shows default certificate (no HTTPS)</h3>
<h4 id="heading-common-causes">Common causes:</h4>
<p>DNS not pointing to server Port 80 blocked Wrong Docker network</p>
<h4 id="heading-check">Check:</h4>
<pre><code class="language-xml">dig +short jenkins.example.com docker logs projects-traefik-staging 2&gt;&amp;1 | grep -i acme
</code></pre>
<p><strong>Why this works:</strong></p>
<p>Let’s Encrypt uses HTTP-01 challenge, so it must reach your server via port 80. If DNS or networking is wrong, certificate issuance fails.</p>
<h3 id="heading-jenkins-reverse-proxy-setup-is-broken">Jenkins: "Reverse proxy setup is broken"</h3>
<h4 id="heading-fix">Fix:</h4>
<p>Set the Jenkins URL to <a href="https://jenkins.example.com/">https://jenkins.example.com/</a><br>Ensure header:</p>
<pre><code class="language-xml">X-Forwarded-Proto: https
</code></pre>
<p><strong>Why this works:</strong></p>
<p>Jenkins needs to know it's behind HTTPS. Without this, it generates incorrect URLs (http instead of https), breaking redirects and webhooks.</p>
<h2 id="heading-13-mental-model-host-vs-container">13. Mental Model: Host vs. Container</h2>
<p>Many setup mistakes come from confusing the <strong>host</strong> filesystem with the <strong>container</strong> filesystem. This table makes it explicit:</p>
<table>
<thead>
<tr>
<th>Inside the Jenkins container</th>
<th>Comes from on the host</th>
</tr>
</thead>
<tbody><tr>
<td><code>/var/jenkins_home</code></td>
<td>docker volume <code>jenkins-data</code> (Jenkins config, jobs, secrets)</td>
</tr>
<tr>
<td><code>/projects/...</code></td>
<td><code>/home/developer/projects/...</code> (your project tree)</td>
</tr>
<tr>
<td><code>/usr/bin/docker</code></td>
<td>host's <code>/usr/bin/docker</code></td>
</tr>
<tr>
<td><code>/usr/libexec/docker/cli-plugins/docker-compose</code></td>
<td>host plugin (lets <code>docker compose</code> work)</td>
</tr>
<tr>
<td><code>/var/run/docker.sock</code></td>
<td>host Docker daemon (so builds happen on the host's engine)</td>
</tr>
<tr>
<td><code>/etc/localtime</code>, <code>/etc/timezone</code></td>
<td>host clock</td>
</tr>
<tr>
<td><code>~/.ssh</code></td>
<td><strong>nothing</strong> — that's why SSH-to-GitHub doesn't work without extra setup</td>
</tr>
</tbody></table>
<p>When debugging, always ask: <em>"Inside which filesystem is this command running, and does the file/folder it's looking for exist there?"</em></p>
<h2 id="heading-14-daily-operations-cheat-sheet">14. Daily Operations Cheat Sheet</h2>
<pre><code class="language-bash"># Recreate Jenkins after changing compose
cd /home/developer/Projects/projects-prod-configs
docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins

# Tail Jenkins logs
docker logs -f projects-jenkins-staging

# Open a shell inside the Jenkins container
docker exec -it projects-jenkins-staging bash

# From inside the container — sanity checks
docker compose version
ls /projects/projects-prod-configs/projects-backend
git -C /projects/projects-prod-configs/projects-backend remote -v

# Manually trigger the same deploy the pipeline does
cd /projects/projects-configs/projects-backend
git pull origin staging
docker compose -f docker-compose.staging.yml up -d --build student-apigw

# Inspect Traefik routing decisions
docker logs projects-traefik-staging 2&gt;&amp;1 | grep -i jenkins

# Check renewed certs
docker exec projects-traefik-staging cat /etc/traefik/acme.json | head -50
</code></pre>
<h2 id="heading-15-what-id-do-differently-next-time">15. What I'd Do Differently Next Time</h2>
<ul>
<li><p><strong>Pre-build a base image</strong> with all node_modules baked in. With ~1500 packages × 15 services, every clean build re-downloads ~22k tarballs. A shared base cuts that 90%.</p>
</li>
<li><p><strong>Run a private npm proxy</strong> (Verdaccio / Nexus / GitHub Packages) on the same Docker network — eliminates flaky <code>npmjs.org</code> timeouts entirely.</p>
</li>
<li><p><strong>Per-service Jenkinsfile</strong> if your services drift apart in tooling. With one Jenkinsfile, every team contends for the same pipeline definition.</p>
</li>
<li><p><strong>Replace</strong> <code>git diff HEAD~1 HEAD</code> with <code>git diff $(git merge-base HEAD origin/staging~1) HEAD</code> so squash-merges and force-pushes don't accidentally skip services.</p>
</li>
<li><p><strong>Move secrets to a vault</strong> (HashiCorp Vault / AWS Secrets Manager / Doppler). PATs in Jenkins work, but rotation across many jobs is painful.</p>
</li>
<li><p><strong>Use Jenkins' Configuration-as-Code (JCasC)</strong> so the entire Jenkins setup (jobs, credentials definitions, plugins) is in git. Then a server rebuild is a one-command operation.</p>
</li>
</ul>
<h2 id="heading-closing-thoughts">Closing Thoughts</h2>
<p>The pipeline itself is just three stages: <strong>Checkout → Detect Changes → Deploy</strong> — but a real production setup is mostly about <strong>plumbing</strong>: reverse proxy, certificates, bind-mounts, credentials, timezones, build caches. None of these are exotic. Together they decide whether your Friday-afternoon deploy goes silently green or eats your weekend.</p>
<p>Follow sections 1–11 to get a working pipeline. Bookmark section 12 to keep it working.</p>
<p>Happy shipping.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ CI/CD in Production with Jenkins ]]>
                </title>
                <description>
                    <![CDATA[ Automation is no longer just a "nice-to-have" skill, it powers modern software development. To help you master automation and CI/CD, we’ve just released a massive 17-hour Jenkins course on the freeCod ]]>
                </description>
                <link>https://www.freecodecamp.org/news/ci-cd-in-production-with-jenkins/</link>
                <guid isPermaLink="false">69b199496c896b0519a8964c</guid>
                
                    <category>
                        <![CDATA[ ci-cd ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Wed, 11 Mar 2026 16:33:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5f68e7df6dfc523d0a894e7c/8a580ac3-b25c-40ba-a1cc-1787c4aaf899.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Automation is no longer just a "nice-to-have" skill, it powers modern software development. To help you master automation and CI/CD, we’ve just released a massive 17-hour Jenkins course on the <a href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel. This course is a complete journey from the core foundations of the Modern SDLC to the complex world of production-grade DevSecOps.</p>
<p>Here are the key sections in this course:</p>
<ul>
<li><p>Modern SDLC Explained</p>
</li>
<li><p>CI/CD Concepts &amp; Branching Strategies</p>
</li>
<li><p>Jenkins Basics &amp; Installation</p>
</li>
<li><p>Jenkins Freestyle Jobs Deep Dive</p>
</li>
<li><p>CI/CD Project | Dockerized Flask App</p>
</li>
<li><p>Jenkins Pipelines (Build, Push, Deploy)</p>
</li>
<li><p>Transition to Multibranch Pipelines</p>
</li>
<li><p>Maven for DevOps</p>
</li>
<li><p>DevSecOps Explained</p>
</li>
<li><p>DevSecOps Mega Project</p>
</li>
<li><p>Jenkins Shared Libraries</p>
</li>
</ul>
<p>Watch the full course on <a href="https://youtu.be/uHNOqKdqQas">the freeCodeCamp.org YouTube channel</a> (17-hour watch).</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/uHNOqKdqQas" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ MERN App Development – How to Build a CI/CD Pipeline with Jenkins ]]>
                </title>
                <description>
                    <![CDATA[ By Rakesh Potnuru As you continue to develop your software, you must also continue to integrate it with previous code and deploy it to servers.  Manually doing this is a time-consuming process that can occasionally result in errors. So we need to do ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/automate-mern-app-deployment-with-jenkins/</link>
                <guid isPermaLink="false">66d460c5d14641365a05095d</guid>
                
                    <category>
                        <![CDATA[ continuous deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Continuous Integration ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mongo ]]>
                    </category>
                
                    <category>
                        <![CDATA[ node ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Wed, 08 Mar 2023 17:42:07 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/03/CICD-Pipeline-with-Jenkins.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Rakesh Potnuru</p>
<p>As you continue to develop your software, you must also continue to integrate it with previous code and deploy it to servers. </p>
<p>Manually doing this is a time-consuming process that can occasionally result in errors. So we need to do this in a continuous and automated manner – which is what you will learn in this article.</p>
<p>We'll go over how you can improve your MERN (MongoDB, Express, React, and NodeJs) app development process by setting up a CI/CD pipeline with Jenkins. You'll see how to automate deployment for faster, more efficient releases.</p>
<h2 id="heading-lets-get-started">Let's Get Started</h2>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li>Basic understanding of MERN stack technologies.</li>
<li>Basic understanding of Docker.</li>
<li>Get source code from <a target="_blank" href="https://github.com/itsrakeshhq/productivity-app">GitHub</a></li>
</ul>
<h2 id="heading-the-problem">The Problem</h2>
<p>Consider this <a target="_blank" href="https://github.com/itsrakeshhq/productivity-app">productivity app</a> – it's a MERN project that we are going to use in this article. There are numerous steps we must complete, from building the application to pushing it to the Docker hub. </p>
<p>First, we must run tests with a command to determine whether all tests pass or not. If all tests pass, we build the Docker images and then push those images to Docker Hub. If your application is extremely complex, you may need to take additional steps. </p>
<p>Now, imagine that we're doing everything manually, which takes time and can lead to mistakes.</p>
<p><img src="https://i.imgur.com/iWAmMm4.jpg" alt="Waiting for deployment without devops meme" width="1600" height="840" loading="lazy">
<em>Waiting for deployment without devops meme</em></p>
<h2 id="heading-the-solution">The Solution</h2>
<p>To address this problem, we can create a CI/CD <strong>Pipeline</strong>. So, whenever you add a feature or fix a bug, this pipeline gets triggered. This automatically performs all of the steps from testing to deploying.</p>
<h2 id="heading-what-is-cicd-and-why-is-it-important">What is CI/CD and Why is it Important?</h2>
<p><strong>C</strong>ontinuous <strong>I</strong>ntegration and <strong>C</strong>ontinuous <strong>D</strong>eployment is a series of steps performed to automate software integration and deployment. CI/CD is the heart of DevOps.</p>
<p><img src="https://i.imgur.com/uMFtPwJ.png" alt="ci cd steps" width="1920" height="1080" loading="lazy">
<em>CI/CD steps</em></p>
<p>From development to deployment, our MERN app goes through four major stages: testing, building Docker images, pushing to a registry, and deploying to a cloud provider. All of this is done manually by running various commands. And we need to do this every time a new feature is added or a bug is fixed. </p>
<p>But this will significantly reduce developer productivity, which is why CI/CD can be so helpful in automating this process. In this article, we will cover the steps up until pushing to the registry.</p>
<p><img src="https://i.imgur.com/g2omESy.png" alt="ci cd meme" width="1600" height="840" loading="lazy">
<em>CI/CD meme</em></p>
<h2 id="heading-the-project">The Project</h2>
<p>The project we are going to use in this tutorial is a very simple full-stack MERN application.</p>
<p><img src="https://i.imgur.com/GSvRlQ0.gif" alt="project demo" width="600" height="338" loading="lazy">
<em>Project demo</em></p>
<p>It contains two microservices.</p>
<ol>
<li>Frontend</li>
<li>Backend</li>
</ol>
<p>You can learn more about the project <a target="_blank" href="https://blog.itsrakesh.co/lets-build-and-deploy-a-full-stack-mern-web-application">here</a>.</p>
<p>Both of these applications contains a Dockerfile. You can learn how to dockerize a MERN application <a target="_blank" href="https://blog.itsrakesh.co/dockerizing-your-mern-stack-app-a-step-by-step-guide">here</a>.</p>
<h2 id="heading-what-is-jenkins">What is Jenkins?</h2>
<p>To run a CI/CD pipeline, we need a CI/CD server. This is where all of the steps written in a pipeline run. </p>
<p>There are numerous services available on the market, including GitHub Actions, Travis CI, Circle CI, GitLab CI/CD, AWS CodePipeline, Azure DevOps, and Google Cloud Build. Jenkins is one of the popular CI/CD tools, and it's what we'll use here.</p>
<h2 id="heading-how-to-set-up-jenkins-server-on-azure">How to Set Up Jenkins Server on Azure</h2>
<p>Because Jenkins is open source and it doesn't provide a cloud solution, we must either run it locally or self-host on a cloud provider. Now, running locally can be difficult, particularly for Windows users. As a result, I've chosen to self-host it on Azure for this demo.</p>
<p>If you want to run locally or self-host somewhere other than Azure (follow <a target="_blank" href="https://www.jenkins.io/doc/book/installing/">these</a> guides by Jenkins), skip this section and proceed to the <strong>How to Configure Jenkins</strong> section.</p>
<p>First, you'll need to sign in to your <a target="_blank" href="https://Azure.microsoft.com?wt.mc_id=studentamb_90351">Azure</a> account (Create one if you don't have one already). Open Azure Cloud Shell.</p>
<p><img src="https://i.imgur.com/IN6RXAe.png" alt="opening azure cloud shell" width="2120" height="467" loading="lazy">
<em>Opening Azure Cloud Shell</em></p>
<p>Then create a directory called <code>jenkins</code> to store all the Jenkins config, and switch to that directory:</p>
<pre><code class="lang-bash">mkdir jenkins
<span class="hljs-built_in">cd</span> jenkins
</code></pre>
<p>Create a file called <code>cloud-init-jenkins.txt</code>. Open with nano or vim,</p>
<pre><code class="lang-bash">touch cloud-init-jenkins.txt
nano cloud-init-jenkins.txt
</code></pre>
<p>and paste this code into it:</p>
<pre><code class="lang-bash"><span class="hljs-comment">#cloud-config</span>
package_upgrade: <span class="hljs-literal">true</span>
runcmd:
  - sudo apt install openjdk-11-jre -y
  - wget -qO - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo apt-key add -
  - sh -c <span class="hljs-string">'echo deb https://pkg.jenkins.io/debian-stable binary/ &gt; /etc/apt/sources.list.d/jenkins.list'</span>
  - sudo apt-get update &amp;&amp; sudo apt-get install jenkins -y
  - sudo service jenkins restart
</code></pre>
<p>Here, we'll use this file to install Jenkins after creating a virtual machine. First, we install openjdk, which is required for Jenkins to function. The Jenkins service is then restarted after we install it.</p>
<p>Next, create a resource group. (A resource group in Azure is like a container that holds all the related resources of a project in one group. Learn more about resource groups <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal#what-is-a-resource-group?wt.mc_id=studentamb_90351">here</a>.)</p>
<pre><code class="lang-bash">az group create --name jenkins-rg --location centralindia
</code></pre>
<p><strong>Note:</strong> make sure to change the location to the one closest to you.</p>
<p>Now, create a virtual machine.</p>
<pre><code class="lang-bash">az vm create \
--resource-group jenkins-rg \
--name jenkins-vm \
--image UbuntuLTS \
--admin-username <span class="hljs-string">"azureuser"</span> \
--generate-ssh-keys \
--public-ip-sku Standard \
--custom-data cloud-init-jenkins.txt
</code></pre>
<p>You can verify the VM installation with this command:</p>
<pre><code class="lang-bash">az vm list -d -o table --query <span class="hljs-string">"[?name=='jenkins-vm']"</span>
</code></pre>
<p>Don't be confused. This command simply displays JSON data in a tabular format for easy verification.</p>
<p>Jenkins server runs on port <code>8080</code>, so we need to expose this port on our VM. You can do that like this:</p>
<pre><code class="lang-bash">az vm open-port \
--resource-group jenkins-rg \
--name jenkins-vm  \
--port 8080 --priority 1010
</code></pre>
<p>Now we can access the Jenkins dashboard in the browser with the URL <code>http://&lt;your-vm-ip&gt;:8080</code>. Use this command to get the VM IP address:</p>
<pre><code class="lang-bash">az vm show \
--resource-group jenkins-rg \
--name jenkins-vm -d \
--query [publicIps] \
--output tsv
</code></pre>
<p>You can now see the Jenkins application in your browser.</p>
<p><img src="https://i.imgur.com/Sy1Glar.png" alt="jenkins dashboard" width="2007" height="1025" loading="lazy">
<em>Jenkins dashboard</em></p>
<p>As you'll notice, Jenkins is asking us to provide an admin password which is automatically generated during its installation.</p>
<p>But first let's SSH into our virtual machine where Jenkins is installed.</p>
<pre><code class="lang-bash">ssh azureuser@&lt;ip_address&gt;
</code></pre>
<p>Now, type in the below command to get the password:</p>
<pre><code class="lang-bash">sudo cat /var/lib/jenkins/secrets/initialAdminPassword
</code></pre>
<p>Copy and paste it. Then click <strong>Continue</strong>.</p>
<h2 id="heading-how-to-configure-jenkins">How to Configure Jenkins</h2>
<p>First, you'll need to click <strong>Install suggested plugins</strong>. It will take some time to install all the plugins.</p>
<p><img src="https://i.imgur.com/vDaaqE3.png" alt="installing suggested plugins" width="2010" height="1095" loading="lazy">
<em>Installing suggested plugins</em></p>
<p>An admin user is needed to restrict access to Jenkins. So go ahead and create one. After finishing, click <strong>Save and continue</strong>.</p>
<p><img src="https://i.imgur.com/qqkwQN6.png" alt="create an admin user" width="2010" height="1036" loading="lazy">
<em>Create an admin user</em></p>
<p>Now you will be presented with the Jenkins dashboard.</p>
<p>The first step is to install the "Blue Ocean" plugin. Jenkins has a very old interface, which may make it difficult for some people to use. This blue ocean plugin provides a modern interface for some Jenkins components (like creating a pipeline).</p>
<p>To install plugins, go to <strong>Manage Jenkins</strong> -&gt; click <strong>Manage Plugins</strong> under "System Configuration" -&gt; <strong>Available plugins</strong>. Search for "Blue Ocean" -&gt; check the box and click <strong>Download now and install after restart</strong>.</p>
<p><img src="https://i.imgur.com/dAKBLiq.png" alt="blue ocean" width="1920" height="1080" loading="lazy">
<em>Blue ocean</em></p>
<p>Great, we're all set. Now let's create a pipeline.</p>
<h2 id="heading-how-to-write-a-jenkinsfile">How to Write a Jenkinsfile</h2>
<p>To create a pipeline, we need a <strong>Jenkinsfile</strong>. This file contains all the pipeline configurations – stages, steps, and so on. Jenkinsfile is to Jenkins as a Dockerfile is to Docker.</p>
<p>Jenkinsfile uses the <strong>Groovy</strong> syntax. The syntax is very simple. You can understand everything by just looking at it.</p>
<p>Let's start by writing:</p>
<pre><code class="lang-groovy">pipeline {

}
</code></pre>
<p>The word 'agent' should be the first thing you mention in the pipeline. An agent is similar to a container or environment in which jobs run. You can use multiple agents to run jobs in parallel. You can find more information about Jenkins agents can <a target="_blank" href="https://www.jenkins.io/doc/book/using/using-agents/">here</a>.</p>
<pre><code class="lang-groovy">pipeline {
    agent any
}
</code></pre>
<p>Here we are telling Jenkins to use any available agent.</p>
<p>We have a total of 5 stages in our pipeline:</p>
<p><img src="https://i.imgur.com/ezvdElo.png" alt="ci cd pipeline stages" width="1920" height="1080" loading="lazy">
<em>CI/CD pipeline stages</em></p>
<h3 id="heading-stage-1-checkout-code">Stage 1: Checkout code</h3>
<p>Different CI/CD tools use different naming conventions. In Jenkins, these are referred to as stages. In each stage we write various steps.</p>
<p>Our first stage is checking out code from a source code management system (in our case, GitHub).</p>
<pre><code class="lang-groovy">pipeline {
    agent any

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
    }
}
</code></pre>
<p>Commit the changes and push to your GitHub repo.</p>
<p>Since we haven't created any pipelines yet, let's do that now.</p>
<p>Before we begin, we must ensure that Git is installed on our system. If you followed my previous steps to install Jenkins on an Azure VM, Git is already installed. </p>
<p>You can test it by running the following command (make you are still SSHed into the VM):</p>
<pre><code class="lang-bash">git --version
</code></pre>
<p>If it isn't already installed, you can do so with:</p>
<pre><code class="lang-bash">sudo apt install git
</code></pre>
<p>Open blue ocean. Click <strong>Create new pipeline</strong>.</p>
<p><img src="https://i.imgur.com/FNffT6p.png" alt="creating new pipeline" width="2010" height="1036" loading="lazy">
<em>Creating new pipeline</em></p>
<p>Then select your source code management system. If you chose GitHub, you must provide an access token for Jenkins to access your repository. I recommend clicking on <strong>Create an access token here</strong> because it is a template with all of the necessary permissions. Then click <strong>Connect</strong>.</p>
<p><img src="https://i.imgur.com/H9TUsHV.png" alt="selecting scm" width="2010" height="1036" loading="lazy">
<em>Selecting scm</em></p>
<p>After that, a pipeline will be created. Since our repository already contains a Jenkinsfile, Jenkins automatically detects it and runs the stages and steps we mentioned in the pipeline.</p>
<p>If everything went well, the entire page will turn green. (Other colors: <strong>blue</strong> indicates that the pipeline is running, <strong>red</strong> indicates that something went wrong in the pipeline, and <strong>gray</strong> indicates that we stopped the pipeline.)</p>
<p><img src="https://i.imgur.com/FtvJlND.png" alt="stage one successful" width="1884" height="486" loading="lazy">
<em>Stage one successful</em></p>
<h3 id="heading-stage-2-run-frontend-tests">Stage 2: Run frontend tests</h3>
<p>In general, all the CI/CD pipelines contains some tests that needs to be run before deploying. So I added simple tests to both the frontend and backend. Let's start with the frontend tests.</p>
<pre><code class="lang-groovy">stage('Client Tests') {
    steps {
        dir('client') {
            sh 'npm install'
            sh 'npm test'
        }
    }
}
</code></pre>
<p>We're changing the directory to <code>client/</code> because that's where the frontend code is. And then install the dependencies with <code>npm install</code> and run the tests with <code>npm test</code> in a shell.</p>
<p>Again, before we restart the pipeline, we have to make sure node and npm are installed or not. Install node and npm with these commands in the virtual machine:</p>
<pre><code class="lang-bash">curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
</code></pre>
<p>After that, run the following:</p>
<pre><code class="lang-bash">sudo apt-get install -y nodejs
</code></pre>
<p>Now, commit the code and restart the pipeline.</p>
<p><img src="https://i.imgur.com/OWYcdDu.png" alt="run client tests" width="1886" height="438" loading="lazy">
<em>Run client tests</em></p>
<h3 id="heading-stage-3-run-backend-tests">Stage 3: Run backend tests</h3>
<p>Now do the same thing for the backend tests.</p>
<p>But there is one thing we need to do before we proceed. If you take a look at the codebase and <code>activity.test.js</code>, we are using a few environment variables. So let's add these environment varibales in Jenkins.</p>
<h4 id="heading-how-to-add-environment-variables-in-jenkins">How to add environment variables in Jenkins</h4>
<p>To add environment variables, go to <strong>Manage Jenkins</strong> -&gt; click <strong>Manage Credentials</strong> under "Security" -&gt;  <strong>System</strong> -&gt; <strong>Global credentials (unrestricted)</strong> -&gt; click <strong>+ Add Credentials</strong>.</p>
<p>For <strong>Kind</strong> select "Secret text", leave <strong>Scope</strong> default, and for <strong>Secret</strong> write the secret value and <strong>ID</strong>. This is what we use when using these environment variables in the Jenkinsfile.</p>
<p>Add the following env variables:</p>
<p><img src="https://i.imgur.com/xGjg2mG.png" alt="environment variables" width="2091" height="796" loading="lazy">
<em>Environment variables</em></p>
<p>Then in the Jenkinsfile, use these env variables:</p>
<pre><code class="lang-groovy">environment {
    MONGODB_URI = credentials('mongodb-uri')
    TOKEN_KEY = credentials('token-key')
    EMAIL = credentials('email')
    PASSWORD = credentials('password')
}
</code></pre>
<p>Add a stage to install dependencies, set these variables in the Jenkins environment, and run the tests:</p>
<pre><code class="lang-groovy">stage('Server Tests') {
    steps {
        dir('server') {
            sh 'npm install'
            sh 'export MONGODB_URI=$MONGODB_URI'
            sh 'export TOKEN_KEY=$TOKEN_KEY'
            sh 'export EMAIL=$EMAIL'
            sh 'export PASSWORD=$PASSWORD'
            sh 'npm test'
        }
    }
}
</code></pre>
<p>Again, commit the code and restart the pipeline.</p>
<p><img src="https://i.imgur.com/hpjMUyT.png" alt="run server tests" width="1059" height="595" loading="lazy">
<em>Run server tests</em></p>
<h3 id="heading-stage-4-build-docker-images">Stage 4: Build Docker images</h3>
<p>Now, we have to specify a step to build the Docker images from the Dockerfiles.</p>
<p>Before we proceed, install Docker in the VM (if you don't already have it installed).</p>
<p>To install Docker:</p>
<pre><code class="lang-bash">sudo apt install docker.io
</code></pre>
<p>Add the user <code>jenkins</code> to the <code>docker</code> group so that Jenkins can access the Docker daemon – otherwise you'll get a permission denied error.</p>
<pre><code class="lang-bash">sudo usermod -a -G docker jenkins
</code></pre>
<p>Then restart the <code>jenkins</code> service.</p>
<pre><code class="lang-bash">sudo systemctl restart jenkins
</code></pre>
<p>Add a stage in the Jenkinsfile.</p>
<pre><code class="lang-groovy">stage('Build Images') {
    steps {
        sh 'docker build -t rakeshpotnuru/productivity-app:client-latest client'
        sh 'docker build -t rakeshpotnuru/productivity-app:server-latest server'
    }
}
</code></pre>
<p>Commit the code and restart the pipeline.</p>
<p><img src="https://i.imgur.com/USh63SD.png" alt="build docker images" width="1984" height="914" loading="lazy">
<em>Build docker images</em></p>
<h3 id="heading-stage-5-push-images-to-the-registry">Stage 5: Push images to the registry</h3>
<p>As a final stage, we will push the images to Docker hub.</p>
<p>Before that, add your docker hub username and password to the Jenkins credentials manager, but for <strong>Kind</strong> choose "Username with password".</p>
<p><img src="https://i.imgur.com/ue0MMKM.png" alt="username with password type credential" width="2010" height="1095" loading="lazy">
<em>Username with password type credential</em></p>
<p>Add the final stage where we login and push images to Docker hub.</p>
<pre><code class="lang-groovy">stage('Push Images to DockerHub') {
    steps {
        withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
            sh 'docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD'
            sh 'docker push rakeshpotnuru/productivity-app:client-latest'
            sh 'docker push rakeshpotnuru/productivity-app:server-latest'
        }
    }
}
</code></pre>
<p><img src="https://i.imgur.com/copfIou.png" alt="push images to dockerhub" width="1991" height="919" loading="lazy">
<em>Push images to dockerhub</em></p>
<p>Here is the complete Jenkinsfile:</p>
<pre><code class="lang-groovy">// This is a Jenkinsfile. It is a script that Jenkins will run when a build is triggered.
pipeline {
    // Telling Jenkins to run the pipeline on any available agent.
    agent any

    // Setting environment variables for the build.
    environment {
        MONGODB_URI = credentials('mongodb-uri')
        TOKEN_KEY = credentials('token-key')
        EMAIL = credentials('email')
        PASSWORD = credentials('password')
    }

    // This is the pipeline. It is a series of stages that Jenkins will run.
    stages {
        // This state is telling Jenkins to checkout the source code from the source control management system.
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        // This stage is telling Jenkins to run the tests in the client directory.
        stage('Client Tests') {
            steps {
                dir('client') {
                    sh 'npm install'
                    sh 'npm test'
                }
            }
        }

        // This stage is telling Jenkins to run the tests in the server directory.
        stage('Server Tests') {
            steps {
                dir('server') {
                    sh 'npm install'
                    sh 'export MONGODB_URI=$MONGODB_URI'
                    sh 'export TOKEN_KEY=$TOKEN_KEY'
                    sh 'export EMAIL=$EMAIL'
                    sh 'export PASSWORD=$PASSWORD'
                    sh 'npm test'
                }
            }
        }

        // This stage is telling Jenkins to build the images for the client and server.
        stage('Build Images') {
            steps {
                sh 'docker build -t rakeshpotnuru/productivity-app:client-latest client'
                sh 'docker build -t rakeshpotnuru/productivity-app:server-latest server'
            }
        }

        // This stage is telling Jenkins to push the images to DockerHub.
        stage('Push Images to DockerHub') {
            steps {
                withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
                    sh 'docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD'
                    sh 'docker push rakeshpotnuru/productivity-app:client-latest'
                    sh 'docker push rakeshpotnuru/productivity-app:server-latest'
                }
            }
        }
    }
}
</code></pre>
<p><img src="https://i.imgur.com/NQxFXhO.png" alt="pipeline ran successfully" width="2120" height="1155" loading="lazy">
<em>Pipeline ran successfully</em></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In summary, let's review what we've covered:</p>
<ul>
<li>We explored the significance of implementing Continuous Integration and Continuous Deployment (CI/CD) in software development.</li>
<li>We delved into the fundamentals of Jenkins and acquired knowledge on how to deploy a Jenkins server on the Azure cloud platform.</li>
<li>We customized Jenkins to meet our specific requirements.</li>
<li>Lastly, we wrote a Jenkinsfile and built a pipeline utilizing the user-friendly interface of Jenkins Blue Ocean.</li>
</ul>
<p>That's all for now! Thanks for reading 🙂.</p>
<p>Connect with me on <a target="_blank" href="https://twitter.com/rakesh_at_tweet">twitter</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn Jenkins by Building a CI/CD Pipeline – Full Course ]]>
                </title>
                <description>
                    <![CDATA[ Jenkins is an open source automation server which makes it easier to build, test, and deploy software. We just published a video course on the freeCodeCamp.org YouTube channel that will teach you about Jenkins by showing you how to build a CI/CD pipe... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-jenkins-by-building-a-ci-cd-pipeline/</link>
                <guid isPermaLink="false">66b204a74b36d956a7749bf4</guid>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Fri, 16 Sep 2022 14:10:10 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/09/jenkins.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Jenkins is an open source automation server which makes it easier to build, test, and deploy software.</p>
<p>We just published a video course on the freeCodeCamp.org YouTube channel that will teach you about Jenkins by showing you how to build a CI/CD pipeline for a web application. </p>
<p>Gwendolyn Faraday developed this course. Gwen is an experienced software developer and she has created a bunch of popular courses on both her own channel and the freeCodeCamp channel.</p>
<p>Besides Jenkins, the project in this course also uses these other technologies:</p>
<ul>
<li>Debian servers running on Linode</li>
<li>Docker &amp; Dockerhub</li>
<li>Github</li>
<li>Some command line for settings things up</li>
</ul>
<p>Jenkins can help developers automate their software development process and improve their productivity. It can also help users obtain a fresh build of their software project more easily. Jenkins is an important tool for creating a DevOps pipeline.</p>
<p>A DevOps pipeline is a set of processes and tools that enable the continuous delivery of software applications. The term "DevOps" is a combination of the words "development" and "operations." DevOps pipelines are used to automate the build, test, and deploy phases of the software development life cycle.</p>
<p>The project in this course features the following architecture.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/09/image-372.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Here are all the sections covered in this course:</p>
<ul>
<li>Course Overview</li>
<li>What is Jenkins?</li>
<li>Terms &amp; Definitions</li>
<li>Project Architecture</li>
<li>Linode Intro</li>
<li>Setting Up Jenkins</li>
<li>Tour of Jenkins Interface</li>
<li>Installing Plugins</li>
<li>Blue Ocean</li>
<li>Creating a Pipeline</li>
<li>Installing Git</li>
<li>Jenkinsfile</li>
<li>Updating a Pipeline</li>
<li>Jenkins with nom</li>
<li>Docker &amp; Dockerhub</li>
<li>Closing Remarks</li>
</ul>
<p>Watch the full course below or on <a target="_blank" href="https://www.youtube.com/watch?v=f4idgaq2VqA">the freeCodeCamp.org YouTube channel</a> (1-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/f4idgaq2VqA" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn Terraform by Deploying a Jenkins Server on AWS ]]>
                </title>
                <description>
                    <![CDATA[ Hello, everyone! Today we're going to learn about Terraform by building a project. Terraform is more than just a tool to boost the productivity of operations teams. You have the chance to transform your developers into operators by implementing Terra... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-terraform-by-deploying-jenkins-server-on-aws/</link>
                <guid isPermaLink="false">66b906b7e8c4e204927a90f3</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Terraform ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Destiny Erhabor ]]>
                </dc:creator>
                <pubDate>Tue, 26 Jul 2022 18:03:50 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/07/pexels-pok-rie-2003885.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Hello, everyone! Today we're going to learn about Terraform by building a project.</p>
<p>Terraform is more than just a tool to boost the productivity of operations teams. You have the chance to transform your developers into operators by implementing Terraform.</p>
<p>This can help increase the efficiency of your entire engineering team and improve communication between developers and operators.</p>
<p>In this article, I'll show you how to fully automate the deployment of your Jenkins services on the AWS cloud using Terraform with a custom baked image.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#heading-what-is-terraform">What is Terraform?</a></li>
<li><a class="post-section-overview" href="#heading-why-should-you-use-terraform">Why Should You Use Terraform?</a></li>
<li><a class="post-section-overview" href="#heading-how-terraform-works">How Terraform works</a></li>
<li><a class="post-section-overview" href="#heading-what-is-a-procedural-language-vs-a-declarative-language">What is a Procedural Language vs a Declarative Language?</a></li>
<li><a class="post-section-overview" href="#heading-prerequisites-and-installation">Prerequisites and Installation</a></li>
<li><a class="post-section-overview" href="#heading-filefolder-structure-of-our-project">File/Folder Structure of Our Project</a></li>
<li><a class="post-section-overview" href="#heading-how-to-first-initialize-terraform-state">How to First Initialize Terraform State</a></li>
<li><a class="post-section-overview" href="#heading-how-to-provision-an-aws-virtual-private-cloud">How to Provision an AWS Virtual Private Cloud</a></li>
<li><a class="post-section-overview" href="#heading-how-to-work-with-terraform-modules">How to Work with Terraform Modules</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-a-vpc-subnet">How to Create a VPC Subnet</a></li>
<li><a class="post-section-overview" href="#heading-how-to-setup-vpc-route-tables">How to Setup VPC Route Tables</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-a-public-route-table">How to Create a Public Route Table</a></li>
<li><a class="post-section-overview" href="#how-to-create-a-private-route-table">How to Create a Private Route Table</a></li>
<li><a class="post-section-overview" href="#heading-how-to-setup-a-vpc-bastion-host">How to Setup a VPC Bastion Host</a></li>
<li><a class="post-section-overview" href="#heading-how-to-provision-our-compute-service">How to Provision our Compute Service</a></li>
<li><a class="post-section-overview" href="#heading-jenkins-master-instance">Jenkins master instance</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-the-load-balancer">How to Create the Load Balancer</a></li>
<li><a class="post-section-overview" href="#heading-cleaning-up">Cleaning Up</a></li>
<li><a class="post-section-overview" href="#heading-summary">Summary</a></li>
</ul>
<h1 id="heading-what-is-terraform">What is Terraform?</h1>
<p>Terraform by HashiCorp is an infrastructure as code solution. It lets you specify cloud and on-premise resources in human-readable configuration files that you can reuse and share. It is a powerful DevOps provisioning tool.</p>
<h1 id="heading-why-should-you-use-terraform">Why Should You Use Terraform?</h1>
<p>Terraform has a number of use-cases, including the capacity to:</p>
<ul>
<li>Specify infrastructure in config/code and easily rebuild/change and track changes to infrastructure.</li>
<li>Support different cloud platforms</li>
<li>Perform incremental resource modifications</li>
<li>Support software-defined networking</li>
</ul>
<h1 id="heading-how-terraform-works">How Terraform works</h1>
<p>Let's have a look at how Terraform works at a high level.</p>
<p>Terraform is developed in the Go programing language. The Go code is compiled into <strong>terraform,</strong> a single binary. You can use this binary to deploy infrastructure from your laptop, a build server, or just about any other computer, and you won't need to run any additional infrastructure to do so.</p>
<p>This is because the Terraform binary makes API calls on your behalf to one or more providers, which include Azure, AWS, Google Cloud, DigitalOcean, and others. This allows Terraform to take advantage of the infrastructure that those providers already have in place for their API servers, as well as the authentication processes they require.</p>
<p><strong>But Terraform doesn't know what API requests to make – so how does it know?</strong> Terraform configurations, which are text files in <strong>declarative language</strong> that specify what infrastructure you want to generate, are the answer. The "code" in "infrastructure as code" is these setups.</p>
<p>You have complete control over your infrastructure, including servers, databases, load balancers, network topology, and more. On your behalf, the Terraform binary parses your code and converts it into a series of API calls as quickly as possible.</p>
<h1 id="heading-what-is-a-procedural-language-vs-a-declarative-language">What is a Procedural Language vs a Declarative Language?</h1>
<p>A procedural language allows you to specify the entire process and list the steps necessary to complete it. You merely give instructions and specify how the process will be carried out. Chef and Ansible encourage this method.</p>
<p>Declarative languages, on the other hand, allow you to simply set the command or order and leave it up to the system to carry it out. You don't need to go into the process; you just need the result. Examples are Terraform, cloudFormation, and Puppeteer.</p>
<p>Enough of the theory...</p>
<p>Now is the moment to put Terraform's high availability, security, performance, and dependability into action.</p>
<p>Here, we're talking about a Terraform-based Jenkins server on Amazon Web Services. We are setting up the networking from the ground-up, so let's get started.</p>
<h2 id="heading-prerequisites-and-installation">Prerequisites and Installation</h2>
<p>There are a few things you'll need to have setup and installed to follow along with this tutorial:</p>
<ul>
<li><a target="_blank" href="https://aws.amazon.com/console/">Create an AWS account</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console">Create an IAM user</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey">Create and Download your user secret and access key</a></li>
<li><a target="_blank" href="https://learn.hashicorp.com/tutorials/terraform/install-cli">Install Terraform from the Terraform - HashiCorp Learn page</a></li>
</ul>
<h2 id="heading-filefolder-structure-of-our-project">File/Folder Structure of Our Project</h2>
<p>We'll use a modular development strategy to separate our Jenkins cluster deployment into numerous template files (rather than developing one large template file).</p>
<p>Each file is in charge of executing a target infrastructure component or AWS resource.</p>
<p>For creating and enforcing infrastructure settings, Terraform leverages the syntax of a JSON-like configuration language called HCL (HashiCorp Configuration Language).</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/structure.PNG" alt="files/folders structures" width="600" height="400" loading="lazy">
<em>files/folders structures</em></p>
<h2 id="heading-how-to-first-initialize-terraform-state">How to First Initialize Terraform State</h2>
<p>To follow best practices, we will be storing our Terraform state files in our cloud storage. This is essential especially for team collaboration.</p>
<p>Terraform state files are files that contain Terraform resources on the projects.</p>
<p>Inside the main.tf file in the backend-state folder, add the following code:</p>
<pre><code class="lang-json">variable <span class="hljs-string">"aws_region"</span> { 
    default = <span class="hljs-attr">"us-east-1"</span> 
 } 
variable <span class="hljs-string">"aws_secret_key"</span> {} 
variable <span class="hljs-string">"aws_access_key"</span> {} 

provider <span class="hljs-string">"aws"</span> { 
    region = var.aws_region 
    access_key = var.aws_access_key 
    secret_key = var.aws_secret_key 
} 

resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"terraform_state"</span> { 
    bucket = <span class="hljs-attr">"terraform-state-caesar-tutorial-jenkins"</span> 

    lifecycle { 
        prevent_destroy = true 
    } 

    versioning { 
        enabled = true 
   } 

   server_side_encryption_configuration { 
           rule { 
            apply_server_side_encryption_by_default { 
                sse_algorithm = <span class="hljs-attr">"AES256"</span> 
            } 
        } 
   } 
}
</code></pre>
<p>Let's make sure we know what's going on in the above code.</p>
<p>We use <strong>variables</strong> to store data, and in Terraform you declare a variable with the variable keyword followed by the name. The variable block can either take some properties such as default, description, type, and so on or none. You will be seeing this a lot.</p>
<p>Now we are declaring the variables as <code>variable "variable_name"{}</code> and <strong>using them in any resources/data block as</strong> <code>var.variable_name</code>. Later you'll see how we will be assigning values to those variables in our secrets.tfvars file.</p>
<p>To use Terraform, you need to tell it the <strong>provider</strong> it will be communicating with and pass in its required properties for authentication. Here we have the AWS region, access, and secret key (you should have these downloaded on your system from the prerequisites).</p>
<p>In terraform, each <strong>resource</strong> we need is defined in the resource block. Resources is the underlined infrastructure that creates our cloud service. It follows the syntax <code>resource "terraform-resource-name" "custom-name" {}</code>.</p>
<p>Terraform has a lot of resources for particular providers in the terraform docs (always refer to the docs if you have questions).</p>
<p>Next, we are creating the aws_s3_bucket. This will store our remote state. It takes the following properties:</p>
<ul>
<li><strong>bucket</strong> → This has to be globally unique</li>
<li><strong>lifecycle</strong> → If you need to destroy your Terraform resources, you might want to prevent destroying the state as it is shared across teams</li>
<li><strong>versioning</strong> → Helps provide some version control over the states</li>
<li><strong>server_side_encryption_configuration</strong> → Provides encryption.</li>
</ul>
<p>Our state backend is ready. But before we initialize it, plan, and apply it with Terraform, let’s <strong>assign our variable to its values</strong>.</p>
<p>In secrets.tfvars, add the following info from your AWS account:</p>
<pre><code class="lang-json">  aws_region = <span class="hljs-string">"us-east-1 
  aws_secret_key = "</span>enter-your-secret<span class="hljs-string">" 
  aws_access_key = "</span>enter-your-access
</code></pre>
<p>In your terminal in the same backend-state folder, run <code>terraform init</code>.</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1655827434318_terminal-state-1.PNG" alt="terraform state on terminal" width="600" height="400" loading="lazy">
<em>terraform state on terminal</em></p>
<p>Then <code>terraform apply -var-file=secrets.tfvars</code>:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1655827520492_terminal-state-2.PNG" alt="terraform state on terminal" width="600" height="400" loading="lazy">
<em>terraform state on terminal</em></p>
<p>In your <strong>AWS console</strong>, here's what you'll see:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1655827945123_aws-console.PNG" alt="terraform state on aws s3 bucket" width="600" height="400" loading="lazy">
<em>terraform state on aws s3 bucket</em></p>
<p>‌‌Now that our state is ready, let’s move to the next part.</p>
<h2 id="heading-how-to-provision-an-aws-virtual-private-cloud">How to Provision an AWS Virtual Private Cloud</h2>
<p>To secure our Jenkins cluster, we will deploy the architecture within a virtual private cloud (VPC) and private subnet. You can deploy the cluster in the AWS default VPC.</p>
<p>To have complete control over the network topology, we will create a VPC from scratch.</p>
<pre><code class="lang-json"> variable <span class="hljs-string">"cidr_block"</span> {} 
 variable <span class="hljs-string">"aws_access_key"</span> {} 
 variable <span class="hljs-string">"aws_secret_key"</span> {} 
 variable <span class="hljs-string">"aws_region"</span> {} 

 provider <span class="hljs-string">"aws"</span> { 
     region = var.aws_region 
    access_key = var.aws_access_key 
    secret_key = var.aws_secret_key 
} 

terraform { 
    backend <span class="hljs-attr">"s3"</span> { 
        bucket     = <span class="hljs-attr">"terraform-state-caesar-tutorial-jenkins"</span> 
        key        = <span class="hljs-attr">"tutorial-jenkins/development/network/terraform.tfstate"</span> 
        region     = <span class="hljs-attr">"us-east-1"</span> 
        encrypt    = true 
   }
} 

resource <span class="hljs-string">"aws_vpc"</span> <span class="hljs-string">"main_vpc"</span> { 
    cidr_block           = var.cidr_block 
    enable_dns_support   = true 
    enable_dns_hostnames = true 

    tags = { 
        Name        = <span class="hljs-attr">"jenkins-instance-main_vpc"</span> 
    } 
}
</code></pre>
<pre><code class="lang-json">output <span class="hljs-string">"vpc_id"</span> { 
    value = aws_vpc.main_vpc.id 
} 

output <span class="hljs-string">"vpc_cidr_block"</span> { 
    value = aws_vpc.main_vpc.cidr_block 
}
</code></pre>
<pre><code class="lang-json">cidr_block            = <span class="hljs-string">"172.0.0.0/16"</span> 
aws_region = <span class="hljs-string">"us-east-1"</span> 
aws_secret_key = <span class="hljs-string">"enter-your-secret"</span> 
aws_access_key = <span class="hljs-string">"enter-your-access"</span>
</code></pre>
<ul>
<li><strong>cidr_block →</strong> Classless Inter-Domain Routing is referred to as CIDR. A CIDR block is an IP address range, to put it simply. This defines what range we are working in.</li>
<li><strong>output →</strong> The output block in Terraform is used to export resource values to other modules. This is another important term when transferring a resource data in one module to another resource in a separate module. (You will learn what modules are soon) Here's its syntax: <code>output "custom_output_name" {  value = "resource-name"}</code>. It takes in a <strong>value</strong> key that takes the resource passed. Here we are output vpc_id and cidr_block.</li>
</ul>
<p>Now, in the terminal, run <code>terraform init</code> and <code>terraform apply</code> to create the resources. You can run <code>terraform plan</code> before to see what resources you are actually creating. Here's the command: <code>terraform apply -var-file=secrets.tfvars</code>, and the output:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1655907973370_vpc.PNG" alt="Image" width="600" height="400" loading="lazy"></p>
<p>You should see your <strong>vpc_id and vpc_cidr_block</strong> in your <strong>AWS Console</strong>:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1656907111688_vpc-aws.PNG" alt="vpc resource on aws" width="600" height="400" loading="lazy">
<em>vpc on aws</em></p>
<h2 id="heading-how-to-work-with-terraform-modules">How to Work with Terraform Modules</h2>
<p>A group of typical configuration files in a specific directory make up a Terraform module. Terraform modules put together resources that are used for a single operation. This cuts down on the amount of code you need to create identical infrastructure components.</p>
<p>Using the syntax below, you can transfer one Terraform module resource to another to be used.</p>
<pre><code class="lang-json">module <span class="hljs-string">"custom-module-name"</span> { 
    source     = <span class="hljs-attr">"path-to-modules-resources"</span> 
}
</code></pre>
<p>And to used the module resource output inside another resource module, this is the command: <code>module.custom-module-name.resource-output-value</code>.</p>
<h2 id="heading-how-to-create-a-vpc-subnet">How to Create a VPC Subnet</h2>
<p>Creating a VPC isn't enough – we also need a subnet to be able to install Jenkins instances on this isolated network. We must pass the VPC ID we output before, since this subnet belongs to a previously constructed VPC.</p>
<p>For resilience, we'll use two public subnets and two private subnets in distinct availability zones. Each subnet has its own CIDR block, which is a subset of the VPC CIDR block, which we got from the VPC resource.</p>
<pre><code class="lang-json">resource <span class="hljs-string">"aws_subnet"</span> <span class="hljs-string">"public_subnets"</span> { 
    vpc_id         = var.vpc_id 
    cidr_block     = cidrsubnet(var.vpc_cidr_block, 8, 2 + count.index)  
       availability_zone   = element(var.availability_zones, count.index)      
    map_public_ip_on_launch = true 
    count                   = var.public_subnets_count 

    tags = { 
        Name        = <span class="hljs-attr">"jenkins-instance-public-subnet"</span> 
   } 
} 

resource <span class="hljs-string">"aws_subnet"</span> <span class="hljs-string">"private_subnets"</span> { 
    vpc_id     = var.vpc_id 
    cidr_block = cidrsubnet(var.vpc_cidr_block, 8, count.index)              
    availability_zone    = element(var.availability_zones, count.index)  
    map_public_ip_on_launch = false 
    count                   = var.private_subnets_count 

    tags = { 
        Name        = <span class="hljs-attr">"jenkins-instance-private-subnet"</span> 
    } 
 }
</code></pre>
<p>Alright, what's going on in this code?</p>
<ul>
<li><a target="_blank" href="https://www.terraform.io/language/meta-arguments/count"><strong>count</strong></a> <strong>→</strong> The count meta-argument accepts a whole number, and creates that many instances of the resource or module. Here we are specifying 2 each to the variables private_subnets_count and public_subnets_count.</li>
<li><strong>map_public_ip_on_launch →</strong> Specify true to indicate that instances launched into the subnet should be assigned a public IP address.</li>
<li><a target="_blank" href="https://www.terraform.io/language/functions/cidrsubnet"><strong>cidrsubnet()</strong></a> <strong>→</strong> cidrsubnet calculates a subnet address within a given IP network address prefix.</li>
<li><a target="_blank" href="https://www.terraform.io/language/functions/element"><strong>element()</strong></a> <strong>→</strong> element retrieves a single element from a list.</li>
</ul>
<p>Now let’s update our modules variables:</p>
<pre><code class="lang-json">variable <span class="hljs-string">"vpc_id"</span> {} 
variable <span class="hljs-string">"vpc_cidr_block"</span> {} 
variable <span class="hljs-string">"private_subnets_count"</span> {} 
variable <span class="hljs-string">"public_subnets_count"</span> {} 
variable <span class="hljs-string">"availability_zones"</span> {}
</code></pre>
<p>Update the secrets.tfvars like this:</p>
<pre><code class="lang-json">private_subnets_count = <span class="hljs-number">2</span> 
public_subnets_count  = <span class="hljs-number">2</span>
</code></pre>
<p>You must establish private and public route tables to specify the traffic-routing method in VPC subnets. Let’s do that before we execute <strong>terraform apply</strong> on our resources.</p>
<h2 id="heading-how-to-setup-vpc-route-tables">How to Setup VPC Route Tables</h2>
<p>We will develop private and public route tables for fine-grained traffic management. This will enable instances deployed in private subnets to access the internet without being exposed to the general public.</p>
<h3 id="heading-how-to-create-a-public-route-table">How to create a public route table</h3>
<p>First we need to establish an <strong>Internet gateway resource</strong> and link it to the VPC we generated previously. Then we need to define a <strong>public route table</strong> and a route that points all traffic (0.0.0.0/0) to the internet gateway. And lastly we need to link it with public subnets in our VPC so that traffic flowing from those subnets is routed to the internet gateway by creating a <strong>route table association.</strong></p>
<pre><code class="lang-json"><span class="hljs-comment">/*** Internet Gateway - Provides a connection between the VPC and the public internet, allowing traffic to flow in and out of the VPC and translating IP addresses to public* addresses.*/</span> 
resource <span class="hljs-string">"aws_internet_gateway"</span> <span class="hljs-string">"igw"</span> { 
    vpc_id = var.vpc_id 

    tags = { 
        Name = <span class="hljs-attr">"igw_jenkins"</span> 
   } 
} 

<span class="hljs-comment">/*** A route from the public route table out to the internet through the internet* gateway.*/</span> 
resource <span class="hljs-string">"aws_route_table"</span> <span class="hljs-string">"public_rt"</span> { 
    vpc_id = var.vpc_id 

    route { 
        cidr_block = <span class="hljs-attr">"0.0.0.0/0"</span> 
        gateway_id = aws_internet_gateway.igw.id 
   } 

   tags = { 
           Name = <span class="hljs-attr">"public_rt_jenkins"</span> 
   } 
} 
<span class="hljs-comment">/*** Associate the public route table with the public subnets.*/</span> 
resource <span class="hljs-string">"aws_route_table_association"</span> <span class="hljs-string">"public"</span> { 
    count     = var.public_subnets_count 
    subnet_id = element(var.public_subnets.*.id, count.index) 
    route_table_id = aws_route_table.public_rt.id 
}
</code></pre>
<h3 id="heading-how-to-create-a-private-route-table">‌How to create a private route table</h3>
<p>Now that our public route table is finished, let’s create the private route table.</p>
<p>To allow our Jenkins instances to connect to the internet as it is deployed on the private subnet, we will construct a <strong>NAT gateway resource</strong> inside a public subnet.</p>
<p>Add an <strong>Elastic IP</strong> address to the NAT gateway after that and a private route table with a route (0.0.0.0/0) that directs all traffic to the ID of the NAT gateway you established. Then we attach private subnets to the private route table by creating the <strong>route table association</strong>.</p>
<pre><code class="lang-json"> <span class="hljs-comment">/*** An elastic IP address to be used by the NAT Gateway defined below.  The NAT* gateway acts as a gateway between our private subnets and the public* internet, providing access out to the internet from within those subnets,* while denying access to them from the public internet.  This IP address* acts as the IP address from which all the outbound traffic from the private* subnets will originate.*/</span> 

 resource <span class="hljs-string">"aws_eip"</span> <span class="hljs-string">"eip_for_the_nat_gateway"</span> { 
     vpc = true 

    tags = { 
        Name = <span class="hljs-attr">"jenkins-tutoral-eip_for_the_nat_gateway"</span> 
    } 
} 

<span class="hljs-comment">/*** A NAT Gateway that lives in our public subnet and provides an interface* between our private subnets and the public internet.  It allows traffic to* exit our private subnets, but prevents traffic from entering them.*/</span> 

resource <span class="hljs-string">"aws_nat_gateway"</span> <span class="hljs-string">"nat_gateway"</span> { 
    allocation_id = aws_eip.eip_for_the_nat_gateway.id 
    subnet_id     = element(var.public_subnets.*.id, 0) 

    tags = { 
        Name = <span class="hljs-attr">"jenkins-tutorial-nat_gateway"</span> 
    } 
} 
<span class="hljs-comment">/*** A route from the private route table out to the internet through the NAT * Gateway.*/</span> 

resource <span class="hljs-string">"aws_route_table"</span> <span class="hljs-string">"private_rt"</span> { 
    vpc_id = var.vpc_id 

    route { 
        cidr_block     = <span class="hljs-attr">"0.0.0.0/0"</span> 
        nat_gateway_id = aws_nat_gateway.nat_gateway.id } 

        tags = { 
            Name   = <span class="hljs-attr">"private_rt_${var.vpc_name}"</span> 
            Author = var.author 
        } 
} 
<span class="hljs-comment">/*** Associate the private route table with the private subnet.*/</span> 
resource <span class="hljs-string">"aws_route_table_association"</span> <span class="hljs-string">"private"</span> { 
    count = var.private_subnets_count 
    subnet_id = element(aws_subnet.private_subnets.*.id, count.index) 
    route_table_id = aws_route_table.private_rt.id 
}
</code></pre>
<p>‌Now let's run <code>terraform apply</code>. But we need to <strong>update our main.tf</strong> files (as this is our entry terraform file) to be aware of our subnets and module variables and <strong>secrets.tfvars</strong> (for our variables).</p>
<pre><code class="lang-json">variable <span class="hljs-string">"vpc_id"</span> {} 
variable <span class="hljs-string">"vpc_cidr_block"</span> {} 
variable <span class="hljs-string">"private_subnets_count"</span> {} 
variable <span class="hljs-string">"public_subnets_count"</span> {} 
variable <span class="hljs-string">"availability_zones"</span> {} 
variable <span class="hljs-string">"public_subnets"</span> {}
</code></pre>
<pre><code class="lang-json">variable <span class="hljs-string">"private_subnets_count"</span> {} 
variable <span class="hljs-string">"public_subnets_count"</span> {} 
variable <span class="hljs-string">"availability_zones"</span> {} 

module <span class="hljs-string">"subnet_module"</span> { 
    source     = <span class="hljs-attr">"./modules"</span> 
    vpc_id     = aws_vpc.main_vpc.id 
    vpc_cidr_block = aws_vpc.main_vpc.cidr_block 
    availability_zones = var.availability_zones 
    public_subnets_count = var.public_subnets_count 
    private_subnets_count = var.private_subnets_count 
 }
</code></pre>
<pre><code class="lang-json"> availability_zones    = [<span class="hljs-string">"us-east-1a"</span>, <span class="hljs-string">"us-east-1b"</span>, <span class="hljs-string">"us-east-1c"</span>, <span class="hljs-string">"us-east-1d"</span>, <span class="hljs-string">"us-east-1e"</span>]
</code></pre>
<p>Our subnets and respective securities are ready. Now we can initialize it, plan, and apply with Terraform.</p>
<p>We will run <strong>terraform apply</strong> to create the resources. You can run terraform plan before to see what resources you are actually creating.</p>
<p>In the terminal run <code>terraform apply -var-file=secrets.tfvars</code>.</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1656906570979_terminal-state-3.PNG" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Just keep in mind that the number of resources added here might defer from yours.</p>
<p>Here's the AWS Console (subnets, elastic address, route_tables):</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1656907223081_image.png" alt="subnets" width="600" height="400" loading="lazy">
<em>subnets</em></p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1656907210658_elastic-ip.PNG" alt="elastic ip" width="600" height="400" loading="lazy">
<em>elastic ip</em></p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1656907210672_rt.PNG" alt="route tables" width="600" height="400" loading="lazy">
<em>route tables</em></p>
<h2 id="heading-how-to-setup-a-vpc-bastion-host">How to Setup a VPC Bastion Host</h2>
<p>We deployed our Jenkins cluster inside the private subnets. Because the cluster lacks a public IP, instances won't be publicly available via the internet. So to take care of this, we'll set up a bastion host so that we can access Jenkins instances safely.</p>
<p>Add the following resources and security group in the bastion.tf file:</p>
<pre><code class="lang-json"><span class="hljs-comment">/*** A security group to allow SSH access into our bastion instance.*/</span> 
resource <span class="hljs-string">"aws_security_group"</span> <span class="hljs-string">"bastion"</span> { 
    name   = <span class="hljs-attr">"bastion-security-group"</span> 
    vpc_id = var.vpc_id 

    ingress { 
        protocol    = <span class="hljs-attr">"tcp"</span> 
        from_port   = 22 
        to_port     = 22 
        cidr_blocks = [<span class="hljs-attr">"0.0.0.0/0"</span>] 
    } 
    egress { 
        protocol    = -1 
        from_port   = 0 
        to_port     = 0 
        cidr_blocks = [<span class="hljs-attr">"0.0.0.0/0"</span>] 
   } 

   tags = { 
           Name = <span class="hljs-attr">"aws_security_group.bastion_jenkins"</span> 
   } 
} 

<span class="hljs-comment">/*** The public key for the key pair we'll use to ssh into our bastion instance.*/</span> 

resource <span class="hljs-string">"aws_key_pair"</span> <span class="hljs-string">"bastion"</span> { 
    key_name   = <span class="hljs-attr">"bastion-key-jenkins"</span> 
    public_key = var.public_key 
 } 

 <span class="hljs-comment">/*** This parameter contains the AMI ID for the most recent Amazon Linux 2 ami,* managed by AWS.*/</span> 

 data <span class="hljs-string">"aws_ssm_parameter"</span> <span class="hljs-string">"linux2_ami"</span> { 
     name = <span class="hljs-attr">"/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-ebs"</span> 
} 

<span class="hljs-comment">/*** Launch a bastion instance we can use to gain access to the private subnets of* this availabilty zone.*/</span> 

resource <span class="hljs-string">"aws_instance"</span> <span class="hljs-string">"bastion"</span> { 
    ami           = data.aws_ssm_parameter.linux2_ami.value 
    key_name      = aws_key_pair.bastion.key_name 
    instance_type = <span class="hljs-attr">"t2.large"</span> 
    associate_public_ip_address = true 
    subnet_id                   = element(aws_subnet.public_subnets, 0).id 
    vpc_security_group_ids      = [aws_security_group.bastion.id] 

    tags = { 
        Name        = <span class="hljs-attr">"jenkins-bastion"</span> 
    } 
} 

output <span class="hljs-string">"bastion"</span> { value = aws_instance.bastion.public_ip }
</code></pre>
<p>Let's see what's going on in the code here:</p>
<ul>
<li><strong>bastion security group resource –</strong> Newly generated EC2 instances do not allow SSH access.</li>
<li>We will link a security group to the active instance in order to enable SSH access to the bastion hosts. Any inbound (ingress) traffic on port 22 (SSH) from anyplace (0.0.0.0/0) will be permitted by the security group. To improve security and prevent security breaches, you can substitute your own public IP address/32 or network address for the CIDR source block.</li>
<li><strong>aws_key_pair –</strong> To be able to connect to the bastion host using SSH and the private key, we added an SSH key pair when we created the EC2. Our public SSH key is used in the key pair. Using the <strong>sshkeygen</strong> command, you can also create a new one.</li>
<li><strong>aws_ssm_parameter</strong> – The Amazon 2 Linux machine image is used by the EC2 instance. The AMI ID is obtained from the AWS marketplace using the AWS AMI data source</li>
<li><strong>aws_instance –</strong>  Finally, we deploy our EC2 bastion instance with its defined configurations and access</li>
<li><strong>output</strong> – By specifying an output, we use the Terraform outputs functionality to show the IP address in the terminal session.</li>
</ul>
<p>Now, let’s update our variable within the modules and the main.tf with the new <strong>public_key</strong> we are passing as a variable:</p>
<pre><code class="lang-json">variable <span class="hljs-string">"public_key"</span>{}
</code></pre>
<pre><code class="lang-json">varable <span class="hljs-string">"public_key"</span> {} 
module <span class="hljs-string">"subnet_module"</span> { 
    source     = <span class="hljs-attr">"./modules"</span> 
    ... 
    publc_key = var.public_key 
}
</code></pre>
<pre><code class="lang-json">public_key = <span class="hljs-string">"enter-your-public-key"</span>
</code></pre>
<p>We will run <strong>terraform apply</strong> to create the resources. You can run terraform plan before to see what resources you are actually creating.</p>
<p>On the terminal, let's run <code>terraform apply -var-file=secrets.tfvars</code>:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1656907792871_terminal-state-4.PNG" alt="terminal resources" width="600" height="400" loading="lazy">
<em>terminal resources</em></p>
<p>Here's the output in the AWS console:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1656907897195_bastion.PNG" alt="aws-console instances" width="600" height="400" loading="lazy">
<em>aws-console instances</em></p>
<h2 id="heading-how-to-provision-our-compute-service">How to Provision our Compute Service</h2>
<h3 id="heading-jenkins-master-instance">Jenkins master instance</h3>
<p>So far, we have successfully been able to set up our VPC and networking topology. ‌‌Finally, we will create our Jenkins EC2 instance that will use a Jenkins master AMI baked by Packer.</p>
<p>You can check out my previous article on how it was baked: <a target="_blank" href="https://www.freecodecamp.org/news/learn-instructure-as-a-code-by-building-custom-machine-image-in-aws/">Learn Infrastructure as Code by Building a Custom Machine Image in AWS on freecodecamp.org</a>. Regardless, you can used any of your custom images if you have one.</p>
<pre><code class="lang-json"> <span class="hljs-comment">/*** This parameter contains our baked AMI ID fetch from the Amazon Console*/</span> data <span class="hljs-string">"aws_ami"</span> <span class="hljs-string">"jenkins-master"</span> { 
     most_recent = true owners      = [<span class="hljs-attr">"self"</span>] 
} 

resource <span class="hljs-string">"aws_security_group"</span> <span class="hljs-string">"jenkins_master_sg"</span> { 
    name        = <span class="hljs-attr">"jenkins_master_sg"</span> 
    description = <span class="hljs-attr">"Allow traffic on port 8080 and enable SSH"</span> 
    vpc_id      = var.vpc_id 

    ingress { 
        from_port       = <span class="hljs-attr">"22"</span> 
        to_port         = <span class="hljs-attr">"22"</span> 
        protocol        = <span class="hljs-attr">"tcp"</span> 
        security_groups = [aws_security_group.bastion.id] 
   } 
   ingress { 
           from_port       = <span class="hljs-attr">"8080"</span> 
        to_port         = <span class="hljs-attr">"8080"</span> 
        protocol        = <span class="hljs-attr">"tcp"</span> 
        security_groups = [aws_security_group.lb.id] 
   } 
   ingress { 
           from_port   = <span class="hljs-attr">"8080"</span> 
        to_port     = <span class="hljs-attr">"8080"</span> 
        protocol    = <span class="hljs-attr">"tcp"</span> 
        cidr_blocks = [<span class="hljs-attr">"0.0.0.0/0"</span>] 
  } 
  egress { 
          from_port   = <span class="hljs-attr">"0"</span> 
        to_port     = <span class="hljs-attr">"0"</span> 
        protocol    = <span class="hljs-attr">"-1"</span> 
        cidr_blocks = [<span class="hljs-attr">"0.0.0.0/0"</span>] 
  } 

  tags = { 
      Name = <span class="hljs-attr">"jenkins_master_sg"</span> 
  }
}
</code></pre>
<p>Attaching a security group to the instance will enable inbound traffic on port 8080 (the Jenkins web dashboard) and SSH only from the bastion server and the VPC CIDR block.</p>
<pre><code class="lang-json">resource <span class="hljs-string">"aws_key_pair"</span> <span class="hljs-string">"jenkins"</span> { 
    key_name   = <span class="hljs-attr">"key-jenkins"</span> 
    public_key = var.public_key 
} 

resource <span class="hljs-string">"aws_instance"</span> <span class="hljs-string">"jenkins_master"</span> { 
    ami       = data.aws_ami.jenkins-master.id 
    instance_type  = <span class="hljs-attr">"t2.large"</span> 
    key_name       = aws_key_pair.jenkins.key_name 
    vpc_security_group_ids = [aws_security_group.jenkins_master_sg.id]
    subnet_id              = element(aws_subnet.private_subnets, 0).id
    root_block_device { 
        volume_type           = <span class="hljs-attr">"gp3"</span> 
        volume_size           = 30 
        delete_on_termination = false 
    } 

    tags = { 
        Name = <span class="hljs-attr">"jenkins_master"</span> 
     } 
 }
</code></pre>
<p>Next, we create a variable and define the instance type that we used to deploy the EC2 instance. We won't be allocating executors or workers on the master, so t2.large (8 GB of memory and 2vCPU) should be adequate for the purposes of simplicity.</p>
<p>Thus, build jobs won't cause the Jenkins master to get overcrowded. But Jenkins' memory requirements vary depending on your project's build requirements and the tools used in those builds. It will require two to three threads, or at least 2 MB of memory, to connect to each build node.</p>
<p>Just a note: consider installing Jenkins workers to prevent overworking the master. As a result, a general-purpose instance can host a Jenkins master and offer a balance between computation and memory resources. In order to maintain the article's simplicity, we won't do that.</p>
<h2 id="heading-how-to-create-the-load-balancer">How to Create the Load Balancer</h2>
<p>To access the Jenkins dashboard, we will create a public load balancer in front of the EC2 instance.</p>
<p>This Elastic load balancer will accept HTTP traffic on port 80 and forward it to the EC2 instance on port 8080. Also, it automatically checks the health of the registered EC2 instance on port 8080. If the Elastic Load Balancing (ELB) finds the instance unhealthy, it stops sending traffic to the Jenkins instance.</p>
<pre><code class="lang-json"> <span class="hljs-comment">/*** A security group to allow SSH access into our load balancer*/</span> resource <span class="hljs-string">"aws_security_group"</span> <span class="hljs-string">"lb"</span> { 
     name   = <span class="hljs-attr">"ecs-alb-security-group"</span> 
    vpc_id = var.vpc_id 

    ingress { 
        protocol    = <span class="hljs-attr">"tcp"</span> 
        from_port   = 80 
        to_port     = 80 
        cidr_blocks = [<span class="hljs-attr">"0.0.0.0/0"</span>] 
     } 
     egress { 
         from_port   = 0 
        to_port     = 0 
        protocol    = <span class="hljs-attr">"-1"</span> 
        cidr_blocks = [<span class="hljs-attr">"0.0.0.0/0"</span>] 
     } 

     tags = { 
         Name = <span class="hljs-attr">"jenkins-lb-sg"</span> 
      } 
 } 

 <span class="hljs-comment">/***Load Balancer to be attached to the ECS cluster to distribute the load among instances*/</span> 

 resource <span class="hljs-string">"aws_elb"</span> <span class="hljs-string">"jenkins_elb"</span> { 
     subnets    = [for subnet in aws_subnet.public_subnets : subnet.id]
    cross_zone_load_balancing = <span class="hljs-literal">true</span> 
    security_groups       = [aws_security_group.lb.id] 
    instances             = [aws_instance.jenkins_master.id] 

    listener { 
        instance_port     = 8080 
        instance_protocol = <span class="hljs-attr">"http"</span> 
        lb_port           = 80 
        lb_protocol       = <span class="hljs-attr">"http"</span> 
     } 

     health_check { 
         healthy_threshold   = 2 
        unhealthy_threshold = 2 
        timeout             = 3 
        target              = <span class="hljs-attr">"TCP:8080"</span>    
        interval            = 5 
    } 

    tags = { 
        Name = <span class="hljs-attr">"jenkins_elb"</span> 
    } 
 } 

 output <span class="hljs-string">"load-balancer-ip"</span> { 
     value = aws_elb.jenkins_elb.dns_name 
 }
</code></pre>
<p>Before, we do our terraform apply, let’s update our development/output.tf folder to output the load balancer DNS:</p>
<pre><code class="lang-json"> output <span class="hljs-string">"load-balancer-ip"</span> { 
     value = module.subnet_module.load-balancer-ip
 }
</code></pre>
<p>On the terminal, run the following command: <code>terraform apply -var-file="secrets.tfvars"</code>. Which will give you this:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1657669467370_load-balancer.PNG" alt="load balancer output" width="600" height="400" loading="lazy">
<em>load balancer output</em></p>
<p>After you apply the changes with Terraform, the Jenkins master load balancer URL should be displayed in your terminal session.</p>
<p>Point your favorite browser to the URL, and you should have access to the Jenkins web dashboard.</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1657669441393_jenkns.PNG" alt="jenkins-instances" width="600" height="400" loading="lazy">
<em>jenkins-instances</em></p>
<p>Then just follow the screen instructions to UNLOCK.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/screencapture-ec2-3-87-146-72-compute-1-amazonaws-8080-2022-05-25-05_38_35-1.png" alt="unlock jenkins" width="600" height="400" loading="lazy">
<em>unlock jenkins</em></p>
<p>You can find the <a target="_blank" href="https://github.com/Caesarsage/terraform-jenkins-instance">full code at this GitHub repo.</a></p>
<h2 id="heading-cleaning-up">Cleaning Up</h2>
<p>To avoid the unnecessary cost of running AWS services, you will need to run the following command to destroy all created and running resources:‌‌<code>terraform destroy -var-file="secrets.tfvars"</code> which should give this output:</p>
<p><img src="https://paper-attachments.dropbox.com/s_0475B692F95FDEC0A6E8498B95C86079E0E8D8D5196F9F4DEAA5AA6D3B79CB44_1657669741091_destroy.PNG" alt="destroy resources" width="600" height="400" loading="lazy">
<em>destroy resources</em></p>
<p>How interesting, right? With just few lines of code we can destroy and spin up our resources.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this tutorial, you have learned how to use Terraform at a high level. You've also learned one of its applications by provisioning a Jenkins server on the AWS cloud platform.</p>
<p>You have also learned about best practices of Terraform backend states and modules.</p>
<p>To learn more about Terraform and its many use-cases, you can check out the official <a target="_blank" href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs">Terraform docs here</a>.</p>
<p>Happy Learning!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ You Rang, M'Lord? Docker in Docker with Jenkins declarative pipelines ]]>
                </title>
                <description>
                    <![CDATA[ By Balázs Tápai Resources. When they are unlimited they are not important. But when they're limited, boy do you have challenges!  Recently, my team has faced such a challenge ourselves: we realised that we needed to upgrade the Node version on one of... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/you-rang-mlord-docker-in-docker-with-jenkins-declarative-pipelines/</link>
                <guid isPermaLink="false">66d45ddb8812486a37369c77</guid>
                
                    <category>
                        <![CDATA[ CI/CD ]]>
                    </category>
                
                    <category>
                        <![CDATA[ dind ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 17 Jan 2020 19:42:55 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/01/butler.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Balázs Tápai</p>
<p>Resources. When they are unlimited they are not important. But when they're limited, boy do you have challenges! </p>
<p>Recently, my team has faced such a challenge ourselves: we realised that we needed to upgrade the Node version on one of our Jenkins agents so we could build and properly test our Angular 7 app. However, we learned that we would also lose the ability to build our legacy AngularJS apps which require Node 8. </p>
<p>What were we to do?</p>
<p>Apart from eliminating the famous "It works on my machine" problem, Docker came in handy to tackle such a problem. However, there were certain challenges that needed to be addressed, such as Docker in Docker. </p>
<p>For this purpose, after a long period of trial and error, we <a target="_blank" href="https://hub.docker.com/repository/docker/btapai/pipelines">built and published</a> a <a target="_blank" href="https://github.com/TapaiBalazs/build-pipeline-docker-images">docker file</a> that fit our team's needs. It helps run our builds, and it looks like the following:</p>
<pre><code><span class="hljs-number">1.</span> Install dependencies
<span class="hljs-number">2.</span> Lint the code
<span class="hljs-number">3.</span> Run unit tests
<span class="hljs-number">4.</span> Run SonarQube analysis
<span class="hljs-number">5.</span> Build the application
<span class="hljs-number">6.</span> Build a docker image which would be deployed
<span class="hljs-number">7.</span> Run the docker container
<span class="hljs-number">8.</span> Run cypress tests
<span class="hljs-number">9.</span> Push docker image to the repository
<span class="hljs-number">10.</span> Run another Jenkins job to deploy it to the environment
<span class="hljs-number">11.</span> Generate unit and functional test reports and publish them
<span class="hljs-number">12.</span> Stop any running containers
<span class="hljs-number">13.</span> Notify chat/email about the build
</code></pre><h2 id="heading-the-docker-image-we-needed">The docker image we needed</h2>
<p>Our project is an Angular 7 project, which was generated using the <code>angular-cli</code>. We also have some dependencies that need Node 10.x.x. We lint our code with <code>tslint</code>, and run our unit tests with <code>Karma</code> and <code>Jasmine</code>. For the unit tests we need a Chrome browser installed so they can run with headless Chrome.</p>
<p>This is why we decided to use the <code>cypress/browsers:node10.16.0-chrome77</code> image. After we installed the dependencies, linted our code and ran our unit tests, we ran the <a target="_blank" href="https://www.npmjs.com/package/sonar-scanner">SonarQube</a> analysis. This required us to have <code>Openjdk 8</code> as well.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> cypress/browsers:node10.<span class="hljs-number">16.0</span>-chrome77

<span class="hljs-comment"># Install OpenJDK-8</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; \
    apt-get install -y openjdk-8-jdk &amp;&amp; \
    apt-get install -y ant &amp;&amp; \
    apt-get clean;</span>

<span class="hljs-comment"># Fix certificate issues</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; \
    apt-get install ca-certificates-java &amp;&amp; \
    apt-get clean &amp;&amp; \
    update-ca-certificates -f;</span>

<span class="hljs-comment"># Setup JAVA_HOME -- useful for docker commandline</span>
<span class="hljs-keyword">ENV</span> JAVA_HOME /usr/lib/jvm/java-<span class="hljs-number">8</span>-openjdk-amd64/
<span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">export</span> JAVA_HOME</span>
</code></pre>
<p>Once the sonar scan was ready, we built the application. One of the strongest principles in testing is that you should test the thing that will be used by your users.<br>That is the reason that we wanted to test the built code in exactly the same docker container as it would be in production. </p>
<p>We could, of course serve the front-end from a very simple <code>nodejs</code> static server.<br>But that would mean that everything an Apache HTTP server or an NGINX server usually did would be missing (for example all the proxies, <code>gzip</code> or <code>brotli</code>).</p>
<p>Now while this is a strong principle, the biggest problem was that we were already running inside a Docker container. That is why we needed DIND (Docker in Docker). </p>
<p>After spending a whole day with my colleague researching, we found a solution which ended up working like a charm. The first and most important thing is that our build container needed the Docker executable.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Install Docker executable</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; apt-get install -y \
        apt-transport-https \
        ca-certificates \
        curl \
        gnupg2 \
        software-properties-common \
    &amp;&amp; curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - \
    &amp;&amp; add-apt-repository \
        <span class="hljs-string">"deb [arch=amd64] https://download.docker.com/linux/debian \
        <span class="hljs-subst">$(lsb_release -cs)</span> \
        stable"</span> \
    &amp;&amp; apt-get update \
    &amp;&amp; apt-get install -y \
        docker-ce</span>

<span class="hljs-keyword">RUN</span><span class="bash"> usermod -u 1002 node &amp;&amp; groupmod -g 1002 node &amp;&amp; gpasswd -a node docker</span>
</code></pre>
<p>As you can see we installed the docker executable and the necessary certificates, but we also added the rights and groups for our user. This second part is necessary because the host machine, our Jenkins agent, starts the container with <code>-u 1002:1002</code>. That is the user ID of our Jenkins agent which runs the container unprivileged.</p>
<p>Of course this isn't everything. When the container starts, the docker daemon of the host machine must be mounted. So we needed to start the build container<br>with some extra parameters. It looks like the following in a Jenkinsfile:</p>
<pre><code class="lang-groovy">pipeline {
  agent {
    docker {
     image 'btapai/pipelines:node-10.16.0-chrome77-openjdk8-CETtime-dind'
     label 'frontend'
     args '-v /var/run/docker.sock:/var/run/docker.sock -v /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket -e HOME=${workspace} --group-add docker'
    }
  }

// ...
}
</code></pre>
<p>As you can see, we mounted two Unix sockets. <code>/var/run/docker.sock</code> mounts the docker daemon to the build container.</p>
<p><code>/var/run/dbus/system_bus_socket</code> is a socket that allows cypress to run inside our container.</p>
<p>We needed <code>-e HOME=${workspace}</code> to avoid access rights issues during the build.</p>
<p><code>--group-add docker</code> passes the host machines docker group down, so that inside the container our user can use the docker daemon.</p>
<p>With these proper arguments, we were able to build our image, start it up and run our cypress tests against it. </p>
<p>But let's take a deep breath here. In Jenkins, we wanted to use multi-branch pipelines. Multibranch pipelines in Jenkins would create a Jenkins job for each branch that contained a Jenkinsfile. This meant that when we developed multiple branches they would have their own views.</p>
<p>There were some problems with this. The first problem was that if we built our image with the same name in all the branches, there would be conflicts (since our docker daemon was technically not inside our build container).</p>
<p>The second problem arose when the docker run command used the same port in every build (because you can't start the second container on a port that is already taken).</p>
<p>The third issue was getting the proper URL for the running application, because Dorothy, you are not in Localhost anymore.</p>
<p>Let's start with the naming. Getting a unique name is pretty easy with git, because commit hashes are unique. However, to get a unique port we had to use a little trick when we declared our environment variables:</p>
<pre><code class="lang-groovy">pipeline {

// ..

  environment {
    BUILD_PORT = sh(
        script: 'shuf -i 2000-65000 -n 1',
        returnStdout: true
    ).trim()
  }

// ...

    stage('Functional Tests') {
      steps {
        sh "docker run -d -p ${BUILD_PORT}:80 --name ${GIT_COMMIT} application"
        // be patient, we are going to get the url as well. :)
      }
    }

// ...

}
</code></pre>
<p>With the <code>shuf -i 2000-65000 -n 1</code> command on certain Linux distributions you can generate a random number. Our base image uses Debian so we were lucky here.<br>The <code>GIT_COMMIT</code> environment variable was provided in Jenkins via the SCM plugin.</p>
<p>Now came the hard part: we were inside a docker container, there was no localhost, and the network inside docker containers can change.</p>
<p>It was also funny that when we started our container, it was running on the host machine's docker daemon. So technically it was not running inside our container. We had to reach it from the inside.</p>
<p>After several hours of investigation my colleague found a possible solution:<br><code>docker inspect --format "{{ .NetworkSettings.IPAddress }}"</code></p>
<p>But it did not work, because that IP address was not an IP address inside the container, but rather outside it. </p>
<p>Then we tried the <code>NetworkSettings.Gateway</code> property, which worked like a charm.<br>So our Functional testing stage looked like the following:</p>
<pre><code class="lang-groovy">stage('Functional Tests') {
  steps {
    sh "docker run -d -p ${BUILD_PORT}:80 --name ${GIT_COMMIT} application"
    sh 'npm run cypress:run -- --config baseUrl=http://`docker inspect --format "{{ .NetworkSettings.Gateway }}" "${GIT_COMMIT}"`:${BUILD_PORT}'
  }
}
</code></pre>
<p>It was a wonderful feeling to see our cypress tests running inside a docker container. </p>
<p>But then some of them failed miserably. Because the failing cypress tests expected to see some dates.</p>
<pre><code class="lang-javascript">cy.get(<span class="hljs-string">"created-date-cell"</span>)
  .should(<span class="hljs-string">"be.visible"</span>)
  .and(<span class="hljs-string">"contain"</span>, <span class="hljs-string">"2019.12.24 12:33:17"</span>)
</code></pre>
<p>But because our build container was set to a different timezone, the displayed date on our front-end was different. </p>
<p>Fortunately, it was an easy fix, and my colleague had seen it before. We installed the necessary time zones and locales. In our case we set the build container's timezone to <code>Europe/Budapest</code>, because our tests were written in this timezone.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># SETUP-LOCALE</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update \
    &amp;&amp; apt-get install --assume-yes --no-install-recommends locales \
    &amp;&amp; apt-get clean \
    &amp;&amp; sed -i -e <span class="hljs-string">'s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/'</span> /etc/locale.gen \
    &amp;&amp; sed -i -e <span class="hljs-string">'s/# hu_HU.UTF-8 UTF-8/hu_HU.UTF-8 UTF-8/'</span> /etc/locale.gen \
    &amp;&amp; locale-gen</span>

<span class="hljs-keyword">ENV</span> LANG=<span class="hljs-string">"en_US.UTF-8"</span> \
    LANGUAGE= \
    LC_CTYPE=<span class="hljs-string">"en_US.UTF-8"</span> \
    LC_NUMERIC=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_TIME=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_COLLATE=<span class="hljs-string">"en_US.UTF-8"</span> \
    LC_MONETARY=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_MESSAGES=<span class="hljs-string">"en_US.UTF-8"</span> \
    LC_PAPER=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_NAME=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_ADDRESS=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_TELEPHONE=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_MEASUREMENT=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_IDENTIFICATION=<span class="hljs-string">"hu_HU.UTF-8"</span> \
    LC_ALL=

<span class="hljs-comment"># SETUP-TIMEZONE</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update \
    &amp;&amp; apt-get install --assume-yes --no-install-recommends tzdata \
    &amp;&amp; apt-get clean \
    &amp;&amp; <span class="hljs-built_in">echo</span> <span class="hljs-string">'Europe/Budapest'</span> &gt; /etc/timezone &amp;&amp; rm /etc/localtime \
    &amp;&amp; ln -snf /usr/share/zoneinfo/<span class="hljs-string">'Europe/Budapest'</span> /etc/localtime \
    &amp;&amp; dpkg-reconfigure -f noninteractive tzdata</span>
</code></pre>
<p>Since every crucial part of the build was now resolved, pushing the built image to the registry was just a docker push command. You can check out the whole dockerfile <a target="_blank" href="https://github.com/TapaiBalazs/build-pipeline-docker-images/blob/master/pipelines/node-chrome-openjdk-CET-dind/Dockerfile">here</a>.</p>
<p>One thing remained, which was to stop running containers when the cypress tests failed. We did this easily using the <code>always</code> post step.</p>
<pre><code class="lang-groovy">post {
  always {
    script {
      try {
        sh "docker stop ${GIT_COMMIT} &amp;&amp; docker rm ${GIT_COMMIT}"
      } catch (Exception e) {
        echo 'No docker containers were running'
      }
    }
  }
}
</code></pre>
<p>Thank you very much for reading this blog post. I hope it helps you.</p>
<p>The original article can be read on my blog:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://tapaibalazs.netlify.com/jenkins-and-docker-in-docker/">https://tapaibalazs.netlify.com/jenkins-and-docker-in-docker/</a></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Steps you should take to build a healthy Angular project ]]>
                </title>
                <description>
                    <![CDATA[ By Ashish Gaikwad Create your “Angular Fitbit” with Jenkins + SonarQube Just like the recent introduction of wearables to track our health, the software industry has long followed the practice of monitoring the health of software projects. The most ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/steps-you-should-take-to-build-a-healthy-angular-project-84eea6608d5f/</link>
                <guid isPermaLink="false">66c35fdbc7095d76345eb025</guid>
                
                    <category>
                        <![CDATA[ Angular ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Sat, 16 Sep 2017 22:53:15 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*Q_Tuv1uGF5i5opPCuZrh0A.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Ashish Gaikwad</p>
<h4 id="heading-create-your-angular-fitbit-with-jenkins-sonarqube">Create your “Angular Fitbit” with Jenkins + SonarQube</h4>
<p><img src="https://cdn-media-1.freecodecamp.org/images/wjMyKOsxM2Mcyv9WVWqsjKodQ0tksqkMMpGb" alt="Image" width="800" height="266" loading="lazy"></p>
<p>Just like the recent introduction of wearables to track our health, the software industry has long followed the practice of monitoring the health of software projects. The most common processes applied are unit tests, integration tests, continuous integration, and code coverage.</p>
<p>I recently struggled a bit in trying to setup the above mentioned processes for our project, so I wrote this post to document my experience. Since TypeScript is the default language for Angular 2 projects, existing JS setups do not work.</p>
<h3 id="heading-getting-started">Getting started</h3>
<p>Here are the steps to setup a Jenkins CI environment for Angular projects with code coverage using SonarQube on a headless Linux server:</p>
<ul>
<li>Download <a target="_blank" href="https://jenkins.io/">Jenkins</a> and set it up on your build server. Make sure you have Java installed on it, as it is required by Jenkins. <strong>Note</strong>: Jenkins’ default configuration runs with <code>jenkins</code> user, so you might need to set <code>JAVA_HOME</code> for the <code>jenkins</code> user.</li>
<li>Once Jenkins is setup, install or make sure you have the following plugins installed from the manage plugins menu:</li>
</ul>
<p><img src="https://cdn-media-1.freecodecamp.org/images/Z1fjqabmgmBITHEsZaWFDf1FOYOpZI-alnLR" alt="Image" width="800" height="67" loading="lazy">
_[<strong>Git plugin</strong>](http://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin" rel="noopener" target="<em>blank" title=") <strong>for repo configuration</strong></em></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/Ynn9vwg7yFFUHeRcH5IzcmcYLjb2lcnse8mY" alt="Image" width="800" height="54" loading="lazy">
_[<strong>NodeJs Plugin</strong>](http://wiki.jenkins-ci.org/display/JENKINS/NodeJS+Plugin" rel="noopener" target="<em>blank" title=") <strong>for running npm commands and scripts</strong></em></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/DqokQi13Q-TFB0a0VLSEKNP4je9sIdhFiu9e" alt="Image" width="800" height="50" loading="lazy">
_[<strong>SonarQube scanner</strong>](http://redirect.sonarsource.com/plugins/jenkins.html" rel="noopener" target="<em>blank" title=") <strong>for test report analysis and publishing.</strong></em></p>
<ul>
<li>Make Git, Node, and SonarQube scanner available to Jenkins. This can be done from the <strong>Global Tool Configuration</strong> menu in the <strong>Manage Jenkins</strong> menu. You can either chose to install automatically or provide the installation path for these tools. For example:</li>
</ul>
<p><img src="https://cdn-media-1.freecodecamp.org/images/tE0Qjwua7ul3uwGOM-gYThmyhfJD82ASDDt3" alt="Image" width="556" height="137" loading="lazy">
<em>Providing the path for local installation.</em></p>
<ul>
<li>Lastly, make Jenkins aware of the SonarQube server installation from the <strong>Configure</strong> menu in <strong>Manage Jenkins</strong>. For example:</li>
</ul>
<p><img src="https://cdn-media-1.freecodecamp.org/images/SAtVBrKnLxF4kfg2zHLxnQkjvEB3stLLLoXb" alt="Image" width="800" height="243" loading="lazy">
<em>SonarQube Server url configuration in Jenkins</em></p>
<p>Download <a target="_blank" href="https://www.sonarqube.org/">SonarQube</a> and set it up on your server. It is usually a simple package extraction on all platforms.</p>
<p>To enable Typescript support in SonarQube, we will use the <a target="_blank" href="https://github.com/Pablissimo/SonarTsPlugin"><strong>SonarTsPlugin</strong></a> since SonarQube doesn’t yet have a default plugin for Typescript. Simply download the jar from the <a target="_blank" href="https://github.com/Pablissimo/SonarTsPlugin/releases">releases page</a> of the plugin and place it in your SonarQube installations <code>**bin**</code> folder. Restart Jenkins once. That is all.</p>
<p>In the Angular projects <code>**karma.conf.js**</code> file, change/add to the <code>browsers</code> section <code>ChromeHeadless</code>.</p>
<p>Example: <code>**browsers:['ChromeHeadless']**</code> . From version 60 onwards <a target="_blank" href="https://developers.google.com/web/updates/2017/04/headless-chrome">Google Chrome supports headless</a> mode on Windows as well. So you can continue to use this setting on local machine as well, in case you work on a Windows machine in an enterprise environment (as I do). I personally prefer the headless mode only for the 1–2 seconds that it saves me.</p>
<p>Add the following to the <code>**scripts**</code> section in <code>**package.json**</code> file.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/RXJqP0brv2WpNbxhjnmxG8522mLee71UonFR" alt="Image" width="800" height="37" loading="lazy">
<em>NPM command for test followed by build</em></p>
<p>The above command makes sure that the build is <strong>only triggered if all the tests are successful</strong>. The <code>**--cc**</code> flag is a short code for <code>**--code-coverage**</code>. This flag will produce the projects coverage report in a new folder named <code>**coverage**</code> within the project directory. The report file is named <code>**lcov.info**</code>.</p>
<p>The default configuration uses Istanbul reporter to show the code coverage report. To publish this coverage report to SonarQube server, the Jenkins SonarQube scanner plugin requires a configuration which can be added as a <code>**sonar-project.properties**</code> file to the project or within the Jenkins project configuration. Example properties file:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/WlS8OKVSTkUI6CcXgAn-crU-tpNVTWfHbLoS" alt="Image" width="655" height="288" loading="lazy">
<em>Sample sonar-project.properties file.</em></p>
<h3 id="heading-configuration">Configuration</h3>
<p>With the above steps done, the project configuration in Jenkins is fairly simple.</p>
<p>First create a new configuration using <strong>New Item</strong> menu and then a <strong>Freestyle project</strong>.</p>
<p>Next in the <strong>Source Code Management</strong> section enable <strong>Git</strong> and setup the projects repo URL:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/5gEgzqe6qCIPmd60CTD0XzzvlU7AGf3hisnz" alt="Image" width="579" height="176" loading="lazy">
<em>Repo setup in Jenkins project configuration.</em></p>
<p>In the <strong>Build Environment</strong> section, enable the checkbox for providing the node and npm environment to the build configuration.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/m2ioZHC6dtQwyNJq4uS260Xffyxne4oEVlHs" alt="Image" width="308" height="205" loading="lazy">
<em>Provide node and npm to current build.</em></p>
<p>Then in the <strong>Build</strong> sectionn add two build steps. First <strong>Execute Shell</strong> and second <strong>Execute SonarQube Scanner</strong>.</p>
<p>The shell step is to run the <code>**cibuild**</code> npm script and the latter to trigger the coverage report analysis. As mentioned above, the sonar properties can be set in the build configuration as well. Example build configuration:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/olqC-tt1a7qk6BgZxJjOH9iHiLejWRyE5vuD" alt="Image" width="586" height="630" loading="lazy">
<em>Build section with npm and sonar analysis</em></p>
<p>That is all. Now a build can be triggered using the <strong>Build Now</strong> menu on the projects home page.</p>
<blockquote>
<p><em>The build log will show the test results, the build logs, and the publish log to SonarQube server. It is ideal to setup remote triggers or web hooks to trigger the project build. This will ensure the stability of the project whenever there is a change in the repo.</em></p>
</blockquote>
<p>Finally, on visiting the SonarQube server, the project details should be visible with all the metrics captured from code coverage report. Example:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/v3aWz3iidRPk0KIDZRnI2A26a9uEFRaqSm3I" alt="Image" width="800" height="275" loading="lazy">
<em>Sonar Projects Dashboard.</em></p>
<p>This is only the first step towards creating a healthier code base. The Jenkins build can be further enhanced to create a pipeline build for better control and granular modifications.</p>
<p><em>Originally published at <a target="_blank" href="https://medium.com/@ashishgkwd/angular-fitbit-jenkins-sonarqube-829cc6201469">medium.com</a> on September 16, 2017.</em></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
