> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CICD Pipeline Setup

> Reproducible guide using GitHub Actions to test and SSH deploy a Flask Wi‑Fi QR code generator to an Ubuntu EC2 instance with provisioning and troubleshooting

In this lesson we build a reproducible CI/CD pipeline to deploy a small Flask application (a Wi‑Fi QR code generator) to an Ubuntu EC2 instance using GitHub and GitHub Actions. The workflow runs tests and, when a pull request is merged or code is pushed to `main`, publishes the new version on the EC2 host via SSH.

Overview

* Application: small Flask app that generates Wi‑Fi QR codes.
* Target host: Ubuntu EC2 instance.
* CI/CD: GitHub Actions to run tests and deploy via SSH.
* Deployment helper: a server-side `deploy.sh` provisioning script and a systemd service for the Flask app.

Application summary
This Flask app renders a simple form (SSID, password, security, hidden) and builds a Wi‑Fi QR payload using the standard format:
`WIFI:T:<AUTH>;S:<SSID>;P:<PASSWORD>;H:<true|false>;;`. Input is escaped per the spec so special characters do not break the payload. The generator returns an inline image (data URI) for display.

Key parts of the application (consolidated, corrected)

```python theme={null}
# app.py
from flask import Flask, render_template, request
import io
import base64
import qrcode
import os

app = Flask(__name__)

def escape_wifi_text(s: str) -> str:
    """
    Escape special chars per Wi-Fi QR standard: \\, ; , : and quotes.
    Returns empty string when None is provided.
    """
    if s is None:
        return ""
    s = s.replace("\\", "\\\\")
    s = s.replace(";", r"\;").replace(",", r"\,").replace(":", r"\:")
    s = s.replace('"', r'\"').replace("'", r"\'")
    return s

def build_wifi_payload(ssid: str, password: str, auth: str, hidden: bool) -> str:
    """
    Build a Wi‑Fi QR payload using the standard format:
      WIFI:T:<auth>;S:<ssid>;P:<password>;H:<true|false>;;
    auth should map to: WPA/WPA2/WPA3 -> WPA, WEP -> WEP, None (open) -> nopass
    """
    auth_map = {"WPA/WPA2/WPA3": "WPA", "WEP": "WEP", "None (open)": "nopass"}
    t = auth_map.get(auth, "WPA")
    ssid_e = escape_wifi_text(ssid or "")
    pwd_e = escape_wifi_text(password or "")
    hidden_flag = "true" if hidden else "false"
    # If using no password, use 'nopass' for type, but include P: empty
    payload = f"WIFI:T:{t};S:{ssid_e};P:{pwd_e};H:{hidden_flag};;"
    return payload

# production-friendly app.run:
if __name__ == "__main__":
    # Use FLASK_DEBUG=1 to enable debug during development; production should set FLASK_DEBUG=0 (or not set)
    debug_mode = os.getenv('FLASK_DEBUG') == '1'
    app.run(debug=debug_mode, host="0.0.0.0", port=5001)
```

Design decisions

* Use `FLASK_DEBUG` environment variable to toggle debug mode at runtime (safe default: disabled in production).
* Escape only the characters required by the Wi‑Fi QR spec (`\`, `;`, `,`, `:`, and quotes) to ensure valid payloads.
* Run the Flask app via a systemd service for simple process supervision and automatic restarts.

CI/CD plan

* Use GitHub Actions to run tests with pytest on pushes to `main`.
* On successful test runs, a deploy job SSHes into the EC2 host to pull the repository, install dependencies into a Python venv, and restart the systemd service and nginx.
* Use repository secrets to store deployment values (`EC2_HOST`, `EC2_USER`, `EC2_SSH_KEY`).

Repository secrets (recommended)

| Secret name | Purpose | Example |
| - | - | - |
| `EC2_HOST` | EC2 public IP or hostname | `203.0.113.25` |
| `EC2_USER` | SSH user on EC2 | `ubuntu` |
| `EC2_SSH_KEY` | Private key contents for SSH from Actions | (store private key as secret) |

Example GitHub Actions workflow (consolidated and corrected)

```yaml theme={null}
# .github/workflows/deploy.yml
name: Deploy to EC2

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
    types: [ closed ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python 3.11
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run tests
        run: |
          pytest -q

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true) }}
    steps:
      - uses: actions/checkout@v4

      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts

      - name: Test SSH connection
        run: |
          ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} "echo 'SSH connection successful'"

      - name: Deploy to EC2
        run: |
          ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
            set -e
            cd /opt/wifi-qr-generator || exit 1

            # Pull latest changes
            git fetch origin
            git reset --hard origin/main

            # Create virtual environment if it doesn't exist
            if [ ! -d "venv" ]; then
              python3 -m venv venv
            fi

            # Activate virtual environment and install/update dependencies
            source venv/bin/activate
            python -m pip install --upgrade pip
            pip install -r requirements.txt

            # Restart app and nginx
            sudo systemctl restart wifi-qr-generator
            sudo systemctl reload nginx

            # Check service status
            if sudo systemctl is-active --quiet wifi-qr-generator; then
              echo "Service is running"
            else
              echo "Service failed to start"
              sudo journalctl -u wifi-qr-generator -n 200 --no-pager
              exit 1
            fi
          EOF
```

Notes about the workflow

* The deploy job expects these repository secrets: `EC2_HOST`, `EC2_USER`, and `EC2_SSH_KEY`.
* The "Test SSH connection" step gives immediate feedback when credentials or network access are misconfigured — this reduces debugging time when a deploy fails.

Prepare the EC2 instance

* The workflow assumes `/opt/wifi-qr-generator` exists (or you have permission to create it). For a reliable first-time setup, run a provisioning script (`deploy.sh`) on the EC2 host to install packages, create the app directory, clone the repo, create a virtualenv, create a systemd service, and configure nginx.

Example deploy.sh (server-side provisioning + service setup)

```bash theme={null}
#!/bin/bash
# deploy.sh - Run on EC2 as Ubuntu user to ensure environment and service exist
set -e

echo "Starting deployment setup..."

sudo apt update
sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv nginx git

# Create application directory and set ownership
sudo mkdir -p /opt/wifi-qr-generator
sudo chown ubuntu:ubuntu /opt/wifi-qr-generator

# Clone repository if missing, otherwise pull
if [ ! -d "/opt/wifi-qr-generator/.git" ]; then
  git clone https://github.com/YOUR_USERNAME/WIFI-QR-Code-Generator.git /opt/wifi-qr-generator
else
  cd /opt/wifi-qr-generator
  git fetch origin
  git reset --hard origin/main
fi

cd /opt/wifi-qr-generator

# Create & activate virtualenv and install requirements
if [ ! -d "venv" ]; then
  python3 -m venv venv
fi
source venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt

# Create a systemd service for the Flask app (wifi-qr-generator.service)
sudo tee /etc/systemd/system/wifi-qr-generator.service > /dev/null <<'SERVICE'
[Unit]
Description=WiFi QR Generator Flask App
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/opt/wifi-qr-generator
# Use FLASK_DEBUG=0 in production; set FLASK_DEBUG=1 for development if desired
Environment=FLASK_DEBUG=0
ExecStart=/opt/wifi-qr-generator/venv/bin/python /opt/wifi-qr-generator/app.py
Restart=always

[Install]
WantedBy=multi-user.target
SERVICE

sudo systemctl daemon-reload
sudo systemctl enable --now wifi-qr-generator

# Setup nginx reverse proxy
sudo tee /etc/nginx/sites-available/wifi-qr-generator > /dev/null <<'NGINX'
server {
    listen 80;
    server_name YOUR_DOMAIN_OR_IP;

    location / {
        proxy_pass http://127.0.0.1:5001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
NGINX

sudo ln -sf /etc/nginx/sites-available/wifi-qr-generator /etc/nginx/sites-enabled/wifi-qr-generator
sudo nginx -t
sudo systemctl restart nginx

echo "Deployment setup completed. Visit http://YOUR_EC2_PUBLIC_IP"
```

DEPLOYMENT.md (brief)

````markdown theme={null}
## Prerequisites
- Ubuntu 22.04 LTS or later on EC2
- GitHub repository with Actions enabled
- SSH key pair for deployment (see security notes)

## Initial EC2 Setup
1. SSH to instance:
   ```bash
   ssh -i your-key.pem ubuntu@your-ec2-public-ip
   ```text

2. Download and run the provisioning script:
   ```bash
   wget https://raw.githubusercontent.com/YOUR_USERNAME/WIFI-QR-Code-Generator/main/deploy.sh
   chmod +x deploy.sh
   sudo ./deploy.sh
   ```text

3. Verify:
   - Check systemd logs:
     ```bash
     sudo journalctl -u wifi-qr-generator -f
     ```text
   - Visit: `http://YOUR_EC2_PUBLIC_IP`
````

Security notes and SSH key handling

<Callout icon="lightbulb" color="#1CB2FE">
  Use a dedicated deploy SSH key for CI/CD. CI runners typically require a non-interactive private key (no passphrase). Store the private key content as the `EC2_SSH_KEY` GitHub secret and never commit it to the repo. For improved security, restrict the deploy user's permissions, limit key access on the server, or use forced command restrictions in `authorized_keys`.
</Callout>

Troubleshooting: SSH authentication issues

* Example error seen in Actions:
  ```bash theme={null}
  ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain
  ```
  Common causes and checks:
  * The private key in `EC2_SSH_KEY` may be passphrase-protected — CI cannot interactively provide a passphrase. Use an unencrypted deploy key or configure an ssh-agent step (complex and rarely needed).
  * The public key may not have been added to the EC2 user's `~/.ssh/authorized_keys` correctly.
  * The private key may have been copied incorrectly (missing BEGIN/END lines or line breaks). On the server use `cat ~/.ssh/deploy-key` to verify and copy the full content into the GitHub secret.
  * EC2 security groups may block inbound SSH (port 22). Ensure the security group allows SSH from GitHub Actions runners or the IPs you expect.

Recommended SSH key flow (repeatable, reliable steps)

```bash theme={null}
# Locally: generate an ed25519 deploy key (more reliable)
ssh-keygen -t ed25519 -f ~/.ssh/github-actions-key -N "" -C "github-actions-deploy"

# Add the public key to EC2 (from your local machine)
ssh-copy-id -i ~/.ssh/github-actions-key.pub ubuntu@your-ec2-ip

# Verify you can ssh using the key locally
ssh -i ~/.ssh/github-actions-key ubuntu@your-ec2-ip

# Copy the private key contents into the GitHub secret EC2_SSH_KEY
cat ~/.ssh/github-actions-key
```

Important warning about passphrases and CI

<Callout icon="warning" color="#FF6B6B">
  Passphrase-protected private keys will fail in non-interactive CI unless you provide the passphrase via an ssh-agent or automation (not recommended). For CI deployments prefer a dedicated key without a passphrase, scoped to a deploy user with limited permissions.
</Callout>

When SSH authentication is failing in Actions

* Confirm SSH access from your local machine using the same private key content you added to GitHub Secrets.
* Use the included "Test SSH connection" step in the workflow to confirm authentication from the runner environment:
  ```bash theme={null}
  ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} "echo 'SSH connection successful'"
  ```
  This quickly indicates whether the problem is authentication, networking, or host key verification.

Images (kept in original positions)

* GitHub secrets screenshot (shown when discussing adding secrets on the repository):

<Frame>
  <img src="https://mintcdn.com/kodekloud-c4ac6d9a/Xs8Ruv7b0QCZf7iM/images/Claude-Code-for-Beginners/Advanced-Features/CICD-Pipeline-Setup/github-repo-secrets-ec2-vscode.jpg?fit=max&auto=format&n=Xs8Ruv7b0QCZf7iM&q=85&s=015dc7e9c6006b501a532b86f29aac45" alt="A screenshot of a GitHub repository's Settings → Secrets and variables → Actions page showing no environment secrets and three repository secrets (EC2_HOST, EC2_SSH_KEY, EC2_USER). In the background a code editor (likely VS Code) with project files and a terminal is visible." width="1920" height="1080" data-path="images/Claude-Code-for-Beginners/Advanced-Features/CICD-Pipeline-Setup/github-repo-secrets-ec2-vscode.jpg" />
</Frame>

* GitHub Actions failed runs screenshot (shown when discussing failed deploy attempts and debugging):

<Frame>
  <img src="https://mintcdn.com/kodekloud-c4ac6d9a/Xs8Ruv7b0QCZf7iM/images/Claude-Code-for-Beginners/Advanced-Features/CICD-Pipeline-Setup/github-actions-deploy-ec2-failed-vscode.jpg?fit=max&auto=format&n=Xs8Ruv7b0QCZf7iM&q=85&s=a3f5a8011401039eb09babb4c76f6a1d" alt="A dark-themed screenshot of a GitHub repository Actions page showing a &#x22;Deploy to EC2&#x22; workflow with several failed runs. The page is shown inside a code editor/IDE window (likely VS Code) with project files and terminals visible in the background." width="1920" height="1080" data-path="images/Claude-Code-for-Beginners/Advanced-Features/CICD-Pipeline-Setup/github-actions-deploy-ec2-failed-vscode.jpg" />
</Frame>

Practical tips and lessons learned

* Prepare the server first: create `/opt/wifi-qr-generator`, clone the repo, set up the systemd service and reverse proxy before relying on the automated workflow to finish provisioning.
* Use a dedicated deploy SSH key for CI (un-encrypted), limit its scope, and store it in GitHub Secrets.
* Add a quick SSH test step to the Actions workflow to detect authentication problems early.
* Keep unit tests in the pipeline — failing tests should block production deploys.
* Use `systemctl` checks and `journalctl` output in the deploy script to provide clear failure context.

Next steps

1. Add your EC2 public IP to `EC2_HOST`, `ubuntu` to `EC2_USER`, and the private key content to `EC2_SSH_KEY` in your repository (Settings → Secrets and variables → Actions).
2. Run the initial server setup (execute `deploy.sh` on the EC2 instance or follow the DEPLOYMENT.md steps).
3. Push a change and verify the workflow runs the test job and then the deploy job.
4. Harden and monitor: restrict SSH key usage, add logging/monitoring, and configure TLS with Let's Encrypt in nginx.

This lesson demonstrated an end-to-end flow: preparing a Flask app for production, creating a server-side provisioning script, authoring a GitHub Actions workflow to run tests and SSH to deploy, and troubleshooting common SSH-related issues.

<CardGroup>
  <Card title="Watch Video" icon="video" cta="Learn more" href="https://learn.kodekloud.com/user/courses/claude-code-for-beginners/module/a295c914-f61e-47bb-8adc-7a3145745aa6/lesson/ea90bff1-adfc-47f8-b64b-af6f51c0975c" />
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.