CtrlK
BlogDocsLog inGet started
Tessl Logo

customerio-deploy-pipeline

Deploy Customer.io integrations to production cloud platforms. Use when deploying to Cloud Run, Vercel, AWS Lambda, or Kubernetes with proper secrets management and health checks. Trigger: "deploy customer.io", "customer.io cloud run", "customer.io kubernetes", "customer.io lambda", "customer.io vercel".

84

Quality

82%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

SKILL.md
Quality
Evals
Security

Customer.io Deploy Pipeline

Overview

Deploy Customer.io integrations to production: GCP Cloud Run with Secret Manager, Vercel serverless functions, AWS Lambda with SSM, Kubernetes with external secrets, plus health check endpoints and blue-green deployment scripts.

Prerequisites

  • CI/CD pipeline configured (see customerio-ci-integration)
  • Cloud platform credentials and access
  • Production Customer.io credentials in a secrets manager

Instructions

Step 1: Deploy to Google Cloud Run

# .github/workflows/deploy-cloud-run.yml
name: Deploy to Cloud Run
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # Required for Workload Identity Federation
    steps:
      - uses: actions/checkout@v4

      - id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
          service_account: ${{ secrets.WIF_SA }}

      - uses: google-github-actions/setup-gcloud@v2

      - name: Build and push
        run: |
          gcloud builds submit --tag gcr.io/${{ secrets.GCP_PROJECT }}/cio-service

      - name: Deploy
        run: |
          gcloud run deploy cio-service \
            --image gcr.io/${{ secrets.GCP_PROJECT }}/cio-service \
            --region us-central1 \
            --set-secrets "CUSTOMERIO_SITE_ID=cio-site-id:latest,\
              CUSTOMERIO_TRACK_API_KEY=cio-track-key:latest,\
              CUSTOMERIO_APP_API_KEY=cio-app-key:latest" \
            --set-env-vars "CUSTOMERIO_REGION=us,NODE_ENV=production" \
            --min-instances 1 \
            --max-instances 10 \
            --memory 512Mi \
            --cpu 1 \
            --allow-unauthenticated

Step 2: Health Check Endpoint

// routes/health.ts
import { TrackClient, RegionUS } from "customerio-node";
import { Router } from "express";

const router = Router();

router.get("/health", async (_req, res) => {
  const checks: Record<string, { status: string; latency_ms?: number }> = {};

  // Check Track API
  const cio = new TrackClient(
    process.env.CUSTOMERIO_SITE_ID!,
    process.env.CUSTOMERIO_TRACK_API_KEY!,
    { region: RegionUS }
  );

  const start = Date.now();
  try {
    await cio.identify("health-check", {
      email: "health@internal.example.com",
      _health_check: true,
    });
    checks.track_api = { status: "ok", latency_ms: Date.now() - start };
  } catch (err: any) {
    checks.track_api = { status: `error: ${err.statusCode}` };
  }

  const allOk = Object.values(checks).every((c) => c.status === "ok");

  res.status(allOk ? 200 : 503).json({
    status: allOk ? "healthy" : "degraded",
    checks,
    version: process.env.npm_package_version ?? "unknown",
    uptime_seconds: Math.floor(process.uptime()),
    timestamp: new Date().toISOString(),
  });
});

export default router;

Step 3: Vercel Serverless Functions

// api/customerio/identify.ts (Vercel serverless function)
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { TrackClient, RegionUS } from "customerio-node";

const cio = new TrackClient(
  process.env.CUSTOMERIO_SITE_ID!,
  process.env.CUSTOMERIO_TRACK_API_KEY!,
  { region: RegionUS }
);

export default async function handler(req: VercelRequest, res: VercelResponse) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }

  const { userId, attributes } = req.body;
  if (!userId || !attributes?.email) {
    return res.status(400).json({ error: "userId and attributes.email required" });
  }

  try {
    await cio.identify(userId, {
      ...attributes,
      last_seen_at: Math.floor(Date.now() / 1000),
    });
    return res.status(200).json({ success: true });
  } catch (err: any) {
    return res.status(err.statusCode ?? 500).json({ error: err.message });
  }
}

Step 4: Kubernetes Deployment

# k8s/customerio-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: customerio-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: customerio-service
  template:
    metadata:
      labels:
        app: customerio-service
    spec:
      containers:
        - name: app
          image: gcr.io/my-project/cio-service:latest
          ports:
            - containerPort: 3000
          env:
            - name: CUSTOMERIO_SITE_ID
              valueFrom:
                secretKeyRef:
                  name: customerio-secrets
                  key: site-id
            - name: CUSTOMERIO_TRACK_API_KEY
              valueFrom:
                secretKeyRef:
                  name: customerio-secrets
                  key: track-api-key
            - name: CUSTOMERIO_APP_API_KEY
              valueFrom:
                secretKeyRef:
                  name: customerio-secrets
                  key: app-api-key
            - name: CUSTOMERIO_REGION
              value: "us"
            - name: NODE_ENV
              value: "production"
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: customerio-service
spec:
  selector:
    app: customerio-service
  ports:
    - port: 80
      targetPort: 3000

Step 5: Blue-Green Deployment

#!/usr/bin/env bash
set -euo pipefail
# scripts/blue-green-deploy.sh

SERVICE="cio-service"
REGION="us-central1"
IMAGE="gcr.io/${GCP_PROJECT}/${SERVICE}:${COMMIT_SHA}"

echo "=== Blue-Green Deploy: ${SERVICE} ==="

# 1. Deploy with no traffic
gcloud run deploy "${SERVICE}" \
  --image "${IMAGE}" \
  --region "${REGION}" \
  --no-traffic \
  --tag "canary"

echo "Deployed canary. Running health check..."

# 2. Health check on canary
CANARY_URL=$(gcloud run services describe "${SERVICE}" \
  --region "${REGION}" --format 'value(status.url)' \
  | sed 's|https://|https://canary---|')

HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "${CANARY_URL}/health")
if [ "${HEALTH}" != "200" ]; then
  echo "FAIL: Health check returned ${HEALTH}. Aborting."
  exit 1
fi

# 3. Shift traffic: 10% → 50% → 100%
for pct in 10 50 100; do
  echo "Shifting ${pct}% traffic to canary..."
  gcloud run services update-traffic "${SERVICE}" \
    --region "${REGION}" \
    --to-tags "canary=${pct}"
  sleep 30
done

echo "Deploy complete. 100% traffic on new revision."

Deployment Checklist

  • Production secrets in secrets manager (not env files)
  • Health check endpoint responds 200
  • Readiness and liveness probes configured
  • Resource limits set (CPU, memory)
  • Min instances > 0 (avoid cold starts)
  • Blue-green or canary deployment configured
  • Rollback procedure documented
  • Post-deploy smoke test automated

Error Handling

IssueSolution
Secret not foundVerify secret name in secrets manager
Health check timeoutIncrease initialDelaySeconds, check CIO connectivity
Cold start latencySet --min-instances 1 (Cloud Run) or keep-alive
Memory OOMIncrease memory limits, check for event queue buildup

Resources

  • Cloud Run Documentation
  • Vercel Serverless Functions
  • Kubernetes Secrets

Next Steps

After deployment, proceed to customerio-webhooks-events for webhook handling.

Repository
jeremylongshore/claude-code-plugins-plus-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.