Skip to main content
Back to Blog
Claude CodeLangChainDevOpsPlatform EngineeringCI/CDDockerNginxAWSShell ScriptingGitHub Actions

AI-Assisted Platform Engineering: Automating CI/CD Pipelines and DevOps with Claude Code, LangChain, and Shell Scripting

Learn how to build self-healing CI/CD pipelines using Claude Code, LangChain agents, Docker, Nginx, and Shell scripting. A practical 2026 guide to AI-assisted platform engineering on AWS.

October 13, 202622 min readNiraj Kumar

AI-Assisted Platform Engineering: Automating CI/CD Pipelines and DevOps with Claude Code, LangChain, and Shell Scripting

It is 2:14 a.m. A deploy fails, the pager goes off, and you spend forty minutes scrolling through logs only to discover a missing environment variable. Every platform engineer has lived this night. In 2026, we finally have tools that can take the first pass at it for us.

AI-assisted platform engineering is not about replacing DevOps teams or handing the keys of production to a chatbot. It is about building a delivery platform where an intelligent agent reads the failure, forms a hypothesis, tries a fix in a safe sandbox, and hands you a pull request with an explanation. You wake up, review it over coffee, and merge.

In this guide, we will build that platform step by step. You will learn how to:

  • Containerize a React.js frontend and a Node.js API with production-grade Docker builds
  • Write advanced Shell scripts for Linux bootstrapping, Nginx configuration, and safe deployments
  • Wire Claude Code into GitHub Actions so failed builds produce reviewable fix PRs
  • Build a LangChain agent that monitors your running services and reasons about incidents
  • Deploy on AWS with short-lived credentials and automatic rollback

Everything here is practical and copy-paste friendly, but I will also explain the thinking behind each decision, because the architecture matters more than any single command.

High-level architecture of an AI-assisted CI/CD platform showing GitHub, Claude Code, AWS, Docker, and a LangChain monitoring agent

What Is AI-Assisted Platform Engineering?

Platform engineering is the discipline of building internal tooling and infrastructure so product developers can ship quickly and safely. Think pipelines, base images, deployment scripts, observability, and the paved roads that make the right way the easy way.

AI-assisted platform engineering adds a reasoning layer on top of that foundation. Instead of a pipeline that only follows fixed steps, you have components that can:

  • Interpret unstructured data such as stack traces, build logs, and error messages
  • Decide what to investigate next based on what they just learned
  • Act through a limited set of tools, such as reading a file, running a test, or restarting a container
  • Explain what they did in plain language, so humans can audit it

Scripts, Agents, and Where Each Belongs

A common beginner mistake is asking the AI to do everything. A better mental model splits responsibilities by predictability:

TaskBest toolWhy
Install Docker on a fresh serverShell scriptDeterministic, repeatable, easy to audit
Render an Nginx config from a templateShell scriptSame input should always give the same output
Run unit tests and build imagesCI pipelineFast, cached, cheap
Diagnose why a test suddenly failsClaude CodeNeeds to read code and reason about it
Watch live services and triage anomaliesLangChain agentNeeds to correlate logs, status, and probes
Approve a production rollbackHuman (or strict policy)High blast radius

If you remember one rule from this article, make it this: deterministic where you can, intelligent where you must.

The Architecture We Are Building

Here is the system at a glance:

  1. A developer pushes code to GitHub.
  2. GitHub Actions runs tests, builds Docker images, and pushes them to Amazon ECR.
  3. A deploy job tells an EC2 instance (through AWS Systems Manager) to run our deploy.sh script.
  4. The script pulls the new images, restarts the stack behind Nginx, runs health checks, and rolls back automatically if they fail.
  5. If CI fails, a second workflow invokes Claude Code to analyze the failure and open a fix pull request.
  6. A LangChain agent runs on the server every few minutes, inspects the stack, and escalates to humans through GitHub Issues when something looks wrong.

Our repository layout looks like this:

.
├── apps/
│   ├── web/                 # React.js (Vite) frontend
│   │   └── Dockerfile
│   └── api/                 # Node.js (Express) backend
│       └── Dockerfile
├── infra/
│   ├── docker-compose.yml
│   ├── nginx/
│   │   └── default.conf.template
│   └── scripts/
│       ├── bootstrap.sh
│       ├── render-nginx.sh
│       └── deploy.sh
├── agents/
│   └── ops_agent.py         # LangChain monitoring agent
├── CLAUDE.md                # Project instructions for Claude Code
└── .github/
    └── workflows/
        ├── ci-cd.yml
        └── self-heal.yml

Step 1: Containerize React.js and Node.js the Right Way

Containers are the contract between your code and your servers. If the contract is sloppy, no amount of AI will save you. Let's start with the Node.js API.

Node.js API Dockerfile

# apps/api/Dockerfile
FROM node:24-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:24-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app

# Run as a non-root user
RUN addgroup -S app && adduser -S app -G app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER app

EXPOSE 4000
HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:4000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

CMD ["node", "src/server.js"]

Three details are worth noticing. First, npm ci --omit=dev gives reproducible installs without dev dependencies. Second, the process runs as a non-root user, which limits damage if the app is compromised. Third, the HEALTHCHECK lets Docker itself report whether the app is alive, and our agent will later read that status.

React.js Frontend Dockerfile

# apps/web/Dockerfile
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

The multi-stage build keeps Node.js and all your build tooling out of the final image. The result is a tiny container that only serves static files.

Docker Compose for the Whole Stack

# infra/docker-compose.yml
services:
  web:
    image: ${ECR_REGISTRY}/myapp-web:${IMAGE_TAG}
    restart: unless-stopped

  api:
    image: ${ECR_REGISTRY}/myapp-api:${IMAGE_TAG}
    restart: unless-stopped
    env_file: /opt/app/.env
    expose:
      - "4000"

  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - /opt/app/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - web
      - api

Notice that only Nginx publishes a port to the outside world. The frontend and API live on the internal Docker network, which is exactly what we want from a reverse proxy setup.

Diagram of Nginx reverse proxy routing traffic to React and Node.js containers inside a Docker network on an AWS EC2 instance

Step 2: Shell Scripting for Linux Environments

Shell scripts are the unglamorous backbone of platform engineering. In 2026 they are still the fastest way to glue together Linux, Docker, and AWS tooling. The trick is writing them defensively.

The Safety Header

Start every production script with this:

#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

trap 'echo "[ERROR] line $LINENO exited with status $?" >&2' ERR

Here is what each flag buys you:

  • -e stops the script when a command fails
  • -u treats unset variables as errors, which catches typos
  • -o pipefail makes a pipeline fail if any command inside it fails
  • -E ensures the ERR trap also fires inside functions

Without this header, a script can silently continue after a failure and deploy half-broken software. Claude Code is also much better at reviewing and extending scripts that follow a consistent, strict pattern.

Bootstrapping a Fresh Ubuntu Server

#!/usr/bin/env bash
# infra/scripts/bootstrap.sh
set -Eeuo pipefail
IFS=$'\n\t'

APP_DIR="/opt/app"

log() { printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$*"; }

require_root() {
  if [[ $EUID -ne 0 ]]; then
    echo "Run as root (or with sudo)." >&2
    exit 1
  fi
}

install_docker() {
  if command -v docker >/dev/null 2>&1; then
    log "Docker already installed, skipping."
    return
  fi
  log "Installing Docker Engine..."
  apt-get update -y
  apt-get install -y ca-certificates curl gnupg
  install -m 0755 -d /etc/apt/keyrings
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
    > /etc/apt/sources.list.d/docker.list
  apt-get update -y
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  systemctl enable --now docker
}

prepare_dirs() {
  mkdir -p "${APP_DIR}/nginx"
  touch "${APP_DIR}/.env"
  chmod 600 "${APP_DIR}/.env"
}

setup_firewall() {
  apt-get install -y ufw
  ufw default deny incoming
  ufw default allow outgoing
  ufw allow 22/tcp
  ufw allow 80/tcp
  ufw allow 443/tcp
  ufw --force enable
}

require_root
install_docker
prepare_dirs
setup_firewall
log "Bootstrap complete."

Notice the idempotency. You can run this script ten times and get the same server. Idempotent scripts are a gift to AI agents too, because retrying a step is always safe.

Dynamically Configuring Nginx

Hard-coding domain names and ports into Nginx configs leads to copy-paste drift between environments. Instead, we use a template and render it at deploy time.

# infra/nginx/default.conf.template
upstream api_backend {
    server api:${API_PORT};
    keepalive 16;
}

server {
    listen 80;
    server_name ${SERVER_NAME};

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;

    location /api/ {
        proxy_pass http://api_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }

    location / {
        proxy_pass http://web:80;
        proxy_set_header Host $host;
    }
}

Now the script that renders it:

#!/usr/bin/env bash
# infra/scripts/render-nginx.sh
set -Eeuo pipefail

: "${SERVER_NAME:?SERVER_NAME is required}"
export API_PORT="${API_PORT:-4000}"

TEMPLATE="/opt/app/nginx/default.conf.template"
OUTPUT="/opt/app/nginx/default.conf"

# Only substitute OUR variables, never Nginx's own $host, $scheme, etc.
envsubst '${SERVER_NAME} ${API_PORT}' < "${TEMPLATE}" > "${OUTPUT}.new"

# Validate before replacing the live config
docker run --rm \
  -v "${OUTPUT}.new:/etc/nginx/conf.d/default.conf:ro" \
  --add-host api:127.0.0.1 --add-host web:127.0.0.1 \
  nginx:1.27-alpine nginx -t

mv "${OUTPUT}.new" "${OUTPUT}"
echo "Nginx config rendered for ${SERVER_NAME}"

The most common bug here is forgetting to restrict envsubst to a specific list of variables. Without that list, it will happily replace $host with an empty string and silently break your proxy headers. Passing the explicit variable list is the fix.

A Deployment Script With Automatic Rollback

This is the script your pipeline and your agents will call. It is deliberately boring, and that is the point.

#!/usr/bin/env bash
# infra/scripts/deploy.sh
set -Eeuo pipefail
IFS=$'\n\t'

NEW_TAG="${1:?usage: deploy.sh <image-tag>}"
APP_DIR="/opt/app"
COMPOSE_FILE="${APP_DIR}/docker-compose.yml"
LAST_GOOD_FILE="${APP_DIR}/.last_good_tag"
HEALTH_URL="http://127.0.0.1/api/health"

export ECR_REGISTRY="${ECR_REGISTRY:?ECR_REGISTRY is required}"

log() { printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$*"; }

compose() { docker compose -f "${COMPOSE_FILE}" "$@"; }

wait_healthy() {
  local attempts=20
  for ((i = 1; i <= attempts; i++)); do
    if curl -fsS --max-time 3 "${HEALTH_URL}" >/dev/null 2>&1; then
      return 0
    fi
    log "Health check ${i}/${attempts} not ready yet..."
    sleep 3
  done
  return 1
}

deploy_tag() {
  export IMAGE_TAG="$1"
  compose pull
  compose up -d --remove-orphans
}

PREVIOUS_TAG="$(cat "${LAST_GOOD_FILE}" 2>/dev/null || true)"

log "Deploying ${NEW_TAG} (previous good: ${PREVIOUS_TAG:-none})"
aws ecr get-login-password --region "${AWS_REGION:-us-east-1}" \
  | docker login --username AWS --password-stdin "${ECR_REGISTRY}"

deploy_tag "${NEW_TAG}"

if wait_healthy; then
  echo "${NEW_TAG}" > "${LAST_GOOD_FILE}"
  docker image prune -f >/dev/null
  log "Deployment ${NEW_TAG} is healthy."
else
  log "Deployment ${NEW_TAG} FAILED health checks."
  if [[ -n "${PREVIOUS_TAG}" ]]; then
    log "Rolling back to ${PREVIOUS_TAG}"
    deploy_tag "${PREVIOUS_TAG}"
    wait_healthy && log "Rollback succeeded." || log "Rollback ALSO unhealthy. Page a human."
  fi
  exit 1
fi

Two things make this script agent-friendly. The exit code tells the truth, and the log lines are written in plain, consistent language. That means any LLM reading the output can understand what happened without guesswork.

Step 3: Teach Claude Code About Your Project

Claude Code works best when it understands your project conventions. The simplest way to do that is a CLAUDE.md file at the repository root. Treat it like onboarding documentation for a new teammate.

# CLAUDE.md

## Project overview
Monorepo with a React (Vite) frontend in apps/web and a Node.js API in apps/api.
Infrastructure scripts live in infra/scripts and must stay POSIX-friendly Bash.

## Commands
- Install: `npm ci` inside each app folder
- Test API: `cd apps/api && npm test`
- Test web: `cd apps/web && npm test`
- Lint: `npm run lint`

## Rules
- Never edit anything under infra/ unless the task explicitly mentions deployment.
- Never touch secrets, .env files, or GitHub workflow permissions.
- Prefer the smallest possible fix. Do not refactor unrelated code.
- Every fix must include or update a test when the failure is a logic bug.
- Shell scripts must start with `set -Eeuo pipefail`.

This single file prevents a surprising amount of unwanted creativity. The agent knows what to run, what to avoid, and how small to keep its changes.

Step 4: Automate CI/CD With GitHub Actions on AWS

Our main pipeline tests, builds, pushes to ECR, and deploys. Notice the use of OpenID Connect (OIDC) instead of long-lived AWS keys. This is the 2026 standard: no static credentials sitting in repository secrets.

# .github/workflows/ci-cd.yml
name: CI/CD

on:
  push:
    branches: [main]
  pull_request:

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: us-east-1

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: |
            apps/api/package-lock.json
            apps/web/package-lock.json
      - run: npm ci
        working-directory: apps/api
      - run: npm test
        working-directory: apps/api
      - run: npm ci
        working-directory: apps/web
      - run: npm test -- --run
        working-directory: apps/web

  build-and-deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - id: ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push images
        env:
          REGISTRY: ${{ steps.ecr.outputs.registry }}
          TAG: ${{ github.sha }}
        run: |
          docker build -t "$REGISTRY/myapp-api:$TAG" apps/api
          docker build -t "$REGISTRY/myapp-web:$TAG" apps/web
          docker push "$REGISTRY/myapp-api:$TAG"
          docker push "$REGISTRY/myapp-web:$TAG"

      - name: Deploy through SSM
        env:
          REGISTRY: ${{ steps.ecr.outputs.registry }}
          TAG: ${{ github.sha }}
        run: |
          aws ssm send-command \
            --instance-ids "${{ secrets.EC2_INSTANCE_ID }}" \
            --document-name "AWS-RunShellScript" \
            --comment "Deploy $TAG" \
            --parameters "commands=[\"ECR_REGISTRY=$REGISTRY /opt/app/scripts/deploy.sh $TAG\"]" \
            --output text

Using SSM instead of SSH means you never open port 22 to GitHub's IP ranges, and every command is logged in AWS. It is a quiet security win.

Step 5: The Self-Healing Workflow With Claude Code

Now for the exciting part. When the main pipeline fails on a branch, we trigger a second workflow. It downloads the failure logs, hands them to Claude Code in headless mode, and opens a pull request with the proposed fix.

# .github/workflows/self-heal.yml
name: Self-Heal

on:
  workflow_run:
    workflows: ["CI/CD"]
    types: [completed]

permissions:
  contents: write
  pull-requests: write
  actions: read

jobs:
  propose-fix:
    if: >
      github.event.workflow_run.conclusion == 'failure' &&
      github.event.workflow_run.head_branch != 'main' &&
      !startsWith(github.event.workflow_run.head_branch, 'ai-fix/')
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.workflow_run.head_sha }}

      - uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Collect failure logs
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh run view ${{ github.event.workflow_run.id }} --log-failed \
            | tail -n 300 > failure.log

      - name: Ask Claude Code for a fix
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p "The CI pipeline failed. Read failure.log, find the root cause, \
          and apply the smallest safe fix. Run the relevant tests to confirm. \
          Follow CLAUDE.md. Finish with a short summary of the cause and the fix." \
            --allowedTools "Read,Edit,Bash(npm test:*),Bash(npm run lint:*)" \
            --max-turns 15 \
            --output-format json > claude-result.json

      - name: Open pull request
        uses: peter-evans/create-pull-request@v7
        with:
          branch: ai-fix/${{ github.event.workflow_run.head_branch }}
          base: ${{ github.event.workflow_run.head_branch }}
          title: "AI fix for failed CI on ${{ github.event.workflow_run.head_branch }}"
          body: "Automated proposal from Claude Code. Please review carefully before merging."
          commit-message: "fix: AI-proposed repair for failing CI"
          delete-branch: true

Let's unpack the design decisions, because they matter more than the YAML:

  • It never pushes to main. The AI proposes, humans dispose.
  • It skips its own branches. The ai-fix/ check prevents an infinite loop of AI fixing AI fixes.
  • Tools are allowlisted. The agent can read, edit, and run tests and lint. It cannot run arbitrary shell commands, touch the network, or read secrets.
  • Turns are capped. --max-turns 15 is your cost and runaway protection.
  • Logs are truncated. Sending the last 300 lines of failed output keeps the context focused and the bill small.

Flow diagram of a self-healing pipeline: failed CI run, log collection, Claude Code analysis, fix pull request, human review, and merge

Step 6: A LangChain Agent That Watches Production

CI covers what happens before deployment. But what about the server at 3 a.m. on a Sunday? This is where a LangChain agent shines. We give it a handful of carefully designed tools and let it reason about what it sees.

Install the dependencies:

python3 -m venv /opt/app/agent-venv
/opt/app/agent-venv/bin/pip install langchain-anthropic langgraph langchain-core

Now the agent:

# agents/ops_agent.py
import os
import subprocess
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

COMPOSE = ["docker", "compose", "-f", "/opt/app/docker-compose.yml"]
ALLOWED_SERVICES = {"web", "api", "nginx"}
MAX_OUTPUT = 4000


def _run(cmd: list[str], timeout: int = 20) -> str:
    """Run a command safely: no shell, hard timeout, truncated output."""
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        return "ERROR: command timed out"
    return (result.stdout + result.stderr)[-MAX_OUTPUT:]


@tool
def service_status() -> str:
    """Show the status and health of every container in the stack."""
    return _run(COMPOSE + ["ps", "--format", "json"])


@tool
def tail_logs(service: str, lines: int = 100) -> str:
    """Return the most recent log lines for one service: web, api, or nginx."""
    if service not in ALLOWED_SERVICES:
        return f"ERROR: service must be one of {sorted(ALLOWED_SERVICES)}"
    lines = max(10, min(lines, 300))
    return _run(COMPOSE + ["logs", "--no-color", "--tail", str(lines), service])


@tool
def http_probe(path: str = "/api/health") -> str:
    """Probe an HTTP path on the local reverse proxy and return status and latency."""
    if not path.startswith("/") or ".." in path:
        return "ERROR: invalid path"
    return _run([
        "curl", "-s", "-o", "/dev/null", "--max-time", "5",
        "-w", "status=%{http_code} time=%{time_total}s",
        f"http://127.0.0.1{path}",
    ])


_restart_count = {"n": 0}


@tool
def restart_service(service: str) -> str:
    """Restart ONE service. Limited to two restarts per run. Use only after reading logs."""
    if service not in ALLOWED_SERVICES:
        return "ERROR: unknown service"
    if _restart_count["n"] >= 2:
        return "ERROR: restart budget exhausted. Escalate to a human instead."
    _restart_count["n"] += 1
    return _run(COMPOSE + ["restart", service], timeout=60)


@tool
def escalate_to_human(title: str, details: str) -> str:
    """Open a GitHub issue for a human. Use when unsure or when a restart did not help."""
    return _run([
        "gh", "issue", "create",
        "--repo", os.environ["GITHUB_REPO"],
        "--title", f"[ops-agent] {title}"[:120],
        "--body", details[:3000],
        "--label", "incident",
    ])


SYSTEM_PROMPT = """You are an on-call platform engineer for a React and Node.js stack.
Investigate before acting: check status, then logs, then probe the app.
Restart a service only if logs show a clear, recoverable problem.
Never guess. If you are unsure, or a restart does not fix things, escalate.
End with a two-sentence summary of what you found and what you did."""


def main() -> None:
    llm = ChatAnthropic(
        model=os.environ.get("OPS_AGENT_MODEL", "claude-sonnet-5-5"),
        temperature=0,
        timeout=60,
    )
    agent = create_react_agent(
        llm,
        tools=[service_status, tail_logs, http_probe, restart_service, escalate_to_human],
        prompt=SYSTEM_PROMPT,
    )
    result = agent.invoke(
        {"messages": [("user", "Run the routine health review of the production stack.")]},
        config={"recursion_limit": 12},
    )
    print(result["messages"][-1].content)


if __name__ == "__main__":
    main()

Run it on a schedule with a simple cron entry:

*/5 * * * * ANTHROPIC_API_KEY=*** GITHUB_REPO=your-org/your-repo /opt/app/agent-venv/bin/python /opt/app/agents/ops_agent.py >> /var/log/ops-agent.log 2>&1

In production you would load secrets from AWS Secrets Manager or a root-owned environment file rather than inlining them. The cron line is for clarity.

Look at how deliberately limited this agent is. It can look at things, restart one service at most twice, and open an issue. It cannot delete volumes, change configs, or roll back. Those actions have a larger blast radius, so they stay with humans or with the deterministic deploy.sh script.

Why Tool Design Matters More Than Prompts

New builders often spend days tuning prompts and minutes designing tools. It should be the other way around. Good tools:

  • Validate input (see the service allowlist and path checks)
  • Bound output (truncated to 4,000 characters so logs never flood the context window)
  • Fail with helpful messages so the model can recover instead of looping
  • Enforce budgets in code rather than politely asking the model to behave

A prompt is a suggestion. A tool boundary is a rule.

Best Practices for AI-Driven DevOps in 2026

1. Use Least Privilege Everywhere

Give the CI job only the AWS permissions it needs, scoped through an OIDC role. Give the agent a Linux user that can only run Docker Compose for one project. Give the GitHub token only the permissions the workflow declares.

2. Keep Humans in the Loop for High-Blast-Radius Actions

Opening a PR is cheap and reversible. Deleting a database is neither. Map every action to a risk level and decide where approval is required.

3. Make Everything Observable

Log every tool call the agent makes, along with its arguments and results. When something strange happens, you need a flight recorder. Tools like LangSmith or plain structured logs shipped to CloudWatch both work well.

4. Pin and Review Your Dependencies

Pin versions of GitHub Actions, base images, and Python packages. Agents amplify supply chain risk because they run with real credentials. Pinning actions to a commit SHA is the strictest option.

5. Test Your Automation Like Product Code

Run shellcheck on every script in CI, add dry-run flags, and keep a staging environment where your agent can practice. The safest agent is the one that failed a hundred times in staging first.

# Add this step to CI to lint all shell scripts
shellcheck infra/scripts/*.sh

6. Control Costs From Day One

Trigger AI only on failures, truncate context, cap turns, and set budgets in your API console. A runaway loop at 3 a.m. should hit a limit rather than your credit card.

Common Mistakes to Avoid

  • Giving the agent broad shell access. A generic "run any command" tool turns a helpful assistant into a liability. Build narrow tools instead.
  • Letting AI push directly to main. Always route changes through pull requests and branch protection.
  • Skipping health checks. If your deploy script cannot tell good from bad, neither can your agent.
  • Creating infinite loops. An AI fix that fails CI can trigger another AI fix. Guard with branch naming rules, attempt counters, and timeouts.
  • Dumping entire logs into the prompt. More context is not always better. Filter to the failing section and recent lines.
  • Forgetting to restrict envsubst. As covered earlier, unrestricted substitution quietly breaks your Nginx variables.
  • Storing secrets in images or repositories. Use environment files with strict permissions, AWS Secrets Manager, or SSM Parameter Store.
  • Trusting AI output without review. Treat AI-generated PRs like code from a talented but new teammate: helpful, fast, and in need of review.
  • Using floating tags like latest. Deploy by immutable tag, such as the Git commit SHA, so rollbacks are exact.

🚀 Pro Tips

  • Write a great CLAUDE.md. Ten minutes spent documenting your commands and rules saves hours of cleanup. Update it whenever the AI makes the same mistake twice.
  • Use a dry-run mode in every script. A --dry-run flag that prints what would happen lets both humans and agents rehearse safely.
  • Store structured results. Ask the agent to end with JSON containing cause, action, and confidence. You can then route low-confidence results to humans automatically.
  • Add a "kill switch." A single environment variable, such as OPS_AGENT_ENABLED=false, that stops all automation instantly is worth its weight in gold.
  • Pre-warm the context. Teach your agent your architecture with a short runbook file it can read. Incident response improves dramatically when it knows what "normal" looks like.
  • Review the agent's mistakes weekly. Treat misfires as feature requests for better tools, clearer rules, or tighter permissions.
  • Run docker compose config in CI. It catches broken Compose files long before they hit a server.
  • Version your prompts. Keep system prompts in the repository, review changes in PRs, and treat them like code.

What Comes Next

Once this foundation is in place, you can extend it naturally:

  • Add blue-green deployments using two Compose projects and an Nginx upstream switch
  • Introduce HTTPS with Let's Encrypt by adding Certbot to your bootstrap script
  • Move from a single EC2 instance to ECS or EKS while keeping the same CI structure
  • Give your agent metrics tools that query CloudWatch or Prometheus instead of only reading logs
  • Add an AI code reviewer workflow that comments on risky infrastructure changes before merge

The principles stay constant even when the infrastructure grows: narrow tools, strong defaults, human oversight, and clear audit trails.

📌 Key Takeaways

  • AI belongs in the reasoning layer of your platform, while Shell scripts and CI handle the deterministic layer.
  • Claude Code in GitHub Actions can analyze failures and open fix pull requests, as long as you allowlist tools, cap turns, and block pushes to main.
  • A LangChain agent with a small, validated toolset can triage incidents, restart a service within a strict budget, and escalate to humans through GitHub Issues.
  • Strict Bash, multi-stage Docker builds, templated Nginx, health checks, and automatic rollback are the foundation that makes AI automation trustworthy.
  • OIDC and SSM remove long-lived AWS keys and open SSH ports from your pipeline.
  • The most important design principle: a prompt is a suggestion, but a tool boundary is a rule.

Conclusion

AI-assisted platform engineering is not magic, and it is certainly not a reason to stop understanding your infrastructure. If anything, it rewards engineers who understand it deeply. The better your scripts, containers, and pipelines are, the more value an AI agent can add on top of them.

We built a complete picture in this guide: React.js and Node.js containerized with Docker, Nginx configured dynamically from a template, hardened Shell scripts for bootstrapping and deploying, a GitHub Actions pipeline that ships to AWS with short-lived credentials, a Claude Code workflow that turns red builds into reviewable pull requests, and a LangChain agent that keeps watch while you sleep.

Start small. Pick one pain point, perhaps the flaky test failures that eat your mornings, and automate that first. Measure the results, tighten the guardrails, and expand from there. The goal is not a pipeline that never breaks. The goal is a platform that notices quickly, explains clearly, and recovers gracefully, and that gives your team back the nights and the focus they deserve.

References

Frequently asked questions

What is AI-assisted platform engineering?

It is the practice of using LLM-powered agents to help build, monitor, and repair the delivery platform that ships your software. Instead of only running fixed scripts, the pipeline can read logs, reason about failures, propose code changes, and escalate to humans when it is unsure.

Is it safe to let an AI agent deploy to production?

Not without guardrails. The safe pattern is to let agents propose changes through pull requests, restrict their tools to an explicit allowlist, use short-lived cloud credentials, and keep destructive actions such as rollbacks behind human approval or well-tested automation.

How is Claude Code different from LangChain in a DevOps workflow?

Claude Code is an agentic coding tool that works inside a repository: it reads files, edits code, and runs commands. LangChain is a framework for building your own agents with custom tools. In this guide, Claude Code fixes code in CI, while a LangChain agent watches the running servers.

Can I run Claude Code inside GitHub Actions?

Yes. You can install it with npm and run it in non-interactive mode with the -p flag, or use the official Claude Code GitHub Action. Always restrict allowed tools and cap the number of turns to control cost and risk.

Why use Shell scripts when I already have AI agents?

Shell scripts are deterministic, auditable, and fast. They handle the predictable 95 percent of the work, such as bootstrapping servers, rendering Nginx configs, and health-checking deployments. The AI handles the unpredictable 5 percent where judgment is required.

How much does an AI-driven pipeline cost to run?

Cost depends on how often agents run and how much context they read. Triggering the AI only on failures, truncating logs to the relevant lines, capping turns, and running monitoring checks every few minutes rather than continuously keeps spend predictable.

Discussion

All Articles
Claude CodeLangChainDevOpsPlatform EngineeringCI/CDDockerNginxAWSShell ScriptingGitHub Actions

Written by

Niraj Kumar

Software Developer — building scalable systems for businesses.

Building this for real? LangChain Developer Services — Production LangChain & LangGraph systems for RAG, agents, and AI workflows.