CircleCI Orbs Cost Optimization ลดค่าใช้จ่าย
CircleCI เป็น CI/CD platform ยอดนิยมที่ใช้ credits-based pricing model Orbs คือ reusable packages ของ CircleCI configuration ที่ช่วยลด boilerplate และเพิ่ม consistency ข้าม projects Cost Optimization คือการลดค่าใช้จ่ายของ CI/CD pipeline โดยไม่กระทบ quality และ speed บทความนี้อธิบายวิธีใช้ Orbs อย่างมีประสิทธิภาพ ลด build time ลด credit usage และ optimize resource allocation เพื่อประหยัดค่าใช้จ่ายสำหรับทีม development
CircleCI Pricing & Credits
# pricing.py — CircleCI pricing model
import json
class CircleCIPricing:
PLANS = {
"free": {
"name": "Free Plan",
"credits": "30,000 credits/month",
"users": "1 user",
"parallelism": "1x",
"cost": "$0",
},
"performance": {
"name": "Performance Plan",
"credits": "Custom (pay-as-you-go)",
"users": "Unlimited",
"parallelism": "Up to 80x",
"cost": "From $15/month + credits",
},
"scale": {
"name": "Scale Plan",
"credits": "Custom + self-hosted runners",
"users": "Unlimited",
"parallelism": "Unlimited",
"cost": "Custom pricing",
},
}
CREDIT_RATES = {
"docker_medium": {"name": "Docker Medium (2 vCPU, 4GB)", "credits": "10 credits/min", "use": "Default, general builds"},
"docker_large": {"name": "Docker Large (4 vCPU, 8GB)", "credits": "20 credits/min", "use": "Heavy builds, compilation"},
"docker_xlarge": {"name": "Docker XLarge (8 vCPU, 16GB)", "credits": "40 credits/min", "use": "Parallel tests, large projects"},
"machine_medium": {"name": "Machine Medium (2 vCPU, 8GB)", "credits": "40 credits/min", "use": "Docker-in-Docker, privileged"},
"machine_large": {"name": "Machine Large (4 vCPU, 16GB)", "credits": "80 credits/min", "use": "Heavy Docker builds"},
"macos_medium": {"name": "macOS Medium", "credits": "50 credits/min", "use": "iOS/macOS builds"},
"arm_medium": {"name": "ARM Medium", "credits": "13 credits/min", "use": "ARM builds (cheaper!)"},
}
def show_plans(self):
print("=== CircleCI Plans ===\n")
for key, plan in self.PLANS.items():
print(f"[{plan['name']}] {plan['credits']} | {plan['cost']}")
def show_rates(self):
print(f"\n=== Credit Rates ===")
for key, rate in self.CREDIT_RATES.items():
print(f" [{rate['name']}] {rate['credits']} — {rate['use']}")
def cost_calculator(self):
print(f"\n=== Monthly Cost Example ===")
builds = [
{"name": "Unit Tests", "resource": "docker_medium", "mins": 5, "runs": 200, "credits_per_min": 10},
{"name": "Integration Tests", "resource": "docker_large", "mins": 10, "runs": 100, "credits_per_min": 20},
{"name": "Docker Build", "resource": "machine_medium", "mins": 8, "runs": 100, "credits_per_min": 40},
{"name": "Deploy", "resource": "docker_medium", "mins": 3, "runs": 50, "credits_per_min": 10},
]
total = 0
for b in builds:
credits = b["mins"] * b["runs"] * b["credits_per_min"]
total += credits
print(f" {b['name']}: {b['mins']}min × {b['runs']} runs × {b['credits_per_min']} = {credits:,} credits")
cost = total * 0.0006
print(f"\n Total: {total:,} credits (~/month)")
pricing = CircleCIPricing()
pricing.show_plans()
pricing.show_rates()
pricing.cost_calculator()
Orbs สำหรับ Cost Optimization
# orbs_optimization.py — Orbs for cost optimization
import json
class OrbsOptimization:
USEFUL_ORBS = {
"node": {
"name": "circleci/node",
"optimization": "Built-in caching สำหรับ node_modules — ลด install time 50-80%",
"config": "node/install: with_cache: true",
},
"docker": {
"name": "circleci/docker",
"optimization": "Docker Layer Caching (DLC) — ลด Docker build time 60%+",
"config": "docker/build: docker-layer-caching: true",
},
"aws_cli": {
"name": "circleci/aws-cli",
"optimization": "Pre-installed AWS CLI — ไม่ต้อง install ทุก build (ลด 1-2 min)",
"config": "aws-cli/setup",
},
"slack": {
"name": "circleci/slack",
"optimization": "Notify เฉพาะ failure — ลด noise, ไม่เปลือง build time",
"config": "slack/notify: event: fail",
},
}
OPTIMIZATION_CONFIG = """
# .circleci/config.yml — Optimized configuration
version: 2.1
orbs:
node: circleci/node@5.2
docker: circleci/docker@2.6
slack: circleci/slack@4.13
# 1. Use smallest resource class possible
executors:
small:
docker:
- image: cimg/node:20.11
resource_class: small # 1 vCPU, 2GB — 5 credits/min (ถูกที่สุด!)
medium:
docker:
- image: cimg/node:20.11
resource_class: medium # 2 vCPU, 4GB — 10 credits/min
jobs:
# 2. Cache aggressively
test:
executor: small # ใช้ small สำหรับ tests!
steps:
- checkout
- node/install-packages:
cache-path: node_modules
override-ci-command: npm ci --prefer-offline
- run:
name: Run Tests
command: npm test
- store_test_results:
path: test-results
# 3. Build only when needed
build:
executor: medium
steps:
- checkout
- node/install-packages:
cache-path: node_modules
- run: npm run build
- persist_to_workspace:
root: .
paths: [dist]
workflows:
# 4. Filter branches — don't build everything
main:
jobs:
- test:
filters:
branches:
ignore: [docs/*, dependabot/*] # Skip non-code branches
- build:
requires: [test]
filters:
branches:
only: [main, develop] # Build only main branches
"""
def show_orbs(self):
print("=== Useful Orbs for Optimization ===\n")
for key, orb in self.USEFUL_ORBS.items():
print(f"[{orb['name']}]")
print(f" Optimization: {orb['optimization']}")
print()
def show_config(self):
print("=== Optimized Config ===")
print(self.OPTIMIZATION_CONFIG[:600])
orbs = OrbsOptimization()
orbs.show_orbs()
orbs.show_config()
Cost Reduction Strategies
# strategies.py — Cost reduction strategies
import json
class CostStrategies:
STRATEGIES = {
"resource_class": {
"name": "1. ใช้ Resource Class เล็กที่สุด",
"saving": "50-75%",
"detail": "small (5 cr/min) แทน medium (10 cr/min) สำหรับ lint, unit tests",
"example": "resource_class: small → ลดจาก 10 เป็น 5 credits/min",
},
"caching": {
"name": "2. Cache ทุกอย่างที่ cache ได้",
"saving": "30-60%",
"detail": "node_modules, pip packages, Docker layers, build artifacts",
"example": "npm ci: 120s → 15s (with cache) = ลด 87%",
},
"parallelism": {
"name": "3. Parallelism อย่างชาญฉลาด",
"saving": "20-40%",
"detail": "แบ่ง tests เป็นกลุ่ม รัน parallel — ลด wall time แต่ใช้ credits เท่าเดิม",
"example": "parallelism: 4 → tests 20min → 5min (same total credits, faster feedback)",
},
"conditional": {
"name": "4. Conditional Execution",
"saving": "30-50%",
"detail": "รัน jobs เฉพาะเมื่อ files ที่เกี่ยวข้องเปลี่ยน",
"example": "path-filtering orb: skip backend tests ถ้าเปลี่ยนแค่ frontend",
},
"branch_filter": {
"name": "5. Branch Filtering",
"saving": "20-40%",
"detail": "ไม่ build ทุก branch — skip docs, dependabot, feature branches",
"example": "filters: branches: ignore: [docs/*, dependabot/*]",
},
"docker_layer": {
"name": "6. Docker Layer Caching (DLC)",
"saving": "40-70% Docker build time",
"detail": "Cache Docker layers ระหว่าง builds — จ่าย 200 credits/job แต่คุ้มถ้า build > 5min",
"example": "Docker build 15min → 4min with DLC",
},
"arm": {
"name": "7. ใช้ ARM Resource Class",
"saving": "20-30%",
"detail": "ARM medium = 13 credits/min vs x86 medium = 10 credits/min (comparable performance)",
"example": "resource_class: arm.medium → ถูกกว่าสำหรับ compatible workloads",
},
"self_hosted": {
"name": "8. Self-Hosted Runners",
"saving": "50-80%",
"detail": "รัน builds บน infrastructure ของตัวเอง — ไม่เสีย credits",
"example": "Scale plan + self-hosted runners สำหรับ heavy builds",
},
}
def show_strategies(self):
print("=== Cost Reduction Strategies ===\n")
for key, strat in self.STRATEGIES.items():
print(f"[{strat['name']}] Saving: {strat['saving']}")
print(f" {strat['detail']}")
print()
def before_after(self):
print("=== Before vs After Optimization ===")
print(f"\n Before (Unoptimized):")
print(f" Resource: medium (10 cr/min) for everything")
print(f" No caching, no filtering")
print(f" Monthly: 500,000 credits (~$300/month)")
print(f"\n After (Optimized):")
print(f" Resource: small for lint/test, medium for build")
print(f" Full caching, branch filtering, conditional jobs")
print(f" Monthly: 150,000 credits (~$90/month)")
print(f"\n Saving: 70% ($210/month)")
strat = CostStrategies()
strat.show_strategies()
strat.before_after()
Monitoring & Analytics
# monitoring.py — CI/CD cost monitoring
import json
import random
class CICDMonitoring:
INSIGHTS_API = """
# circleci_insights.py — CircleCI Insights API
import requests
class CircleCIInsights:
def __init__(self, token, org_slug):
self.token = token
self.org_slug = org_slug
self.base_url = "https://circleci.com/api/v2"
self.headers = {"Circle-Token": token}
def get_project_workflows(self, project_slug):
resp = requests.get(
f"{self.base_url}/insights/{project_slug}/workflows",
headers=self.headers,
)
return resp.json().get("items", [])
def get_workflow_metrics(self, project_slug, workflow_name):
resp = requests.get(
f"{self.base_url}/insights/{project_slug}/workflows/{workflow_name}",
headers=self.headers,
)
return resp.json()
def get_job_metrics(self, project_slug, workflow_name):
resp = requests.get(
f"{self.base_url}/insights/{project_slug}/workflows/{workflow_name}/jobs",
headers=self.headers,
)
return resp.json().get("items", [])
def find_expensive_jobs(self, project_slug, workflow_name):
jobs = self.get_job_metrics(project_slug, workflow_name)
sorted_jobs = sorted(jobs, key=lambda j: j.get("metrics", {}).get("duration_metrics", {}).get("median", 0), reverse=True)
return sorted_jobs[:5]
# Usage
# insights = CircleCIInsights("your-token", "gh/org")
# expensive = insights.find_expensive_jobs("gh/org/repo", "build-and-test")
"""
def show_api(self):
print("=== CircleCI Insights API ===")
print(self.INSIGHTS_API[:500])
def cost_dashboard(self):
print(f"\n=== CI/CD Cost Dashboard ===")
jobs = [
{"name": "unit-tests", "avg_min": random.uniform(2, 8), "runs": random.randint(100, 300), "resource": "small"},
{"name": "integration-tests", "avg_min": random.uniform(5, 15), "runs": random.randint(50, 150), "resource": "medium"},
{"name": "docker-build", "avg_min": random.uniform(3, 12), "runs": random.randint(50, 100), "resource": "medium"},
{"name": "deploy", "avg_min": random.uniform(1, 5), "runs": random.randint(20, 60), "resource": "small"},
]
rates = {"small": 5, "medium": 10, "large": 20}
total_credits = 0
for j in jobs:
credits = j["avg_min"] * j["runs"] * rates[j["resource"]]
total_credits += credits
print(f" {j['name']:<20} {j['avg_min']:>5.1f}min × {j['runs']:>3} runs = {credits:>8,.0f} credits")
cost = total_credits * 0.0006
print(f"\n Total: {total_credits:,.0f} credits (~/month)")
def optimization_tips(self):
print(f"\n=== Quick Wins ===")
tips = [
"ลด resource_class ของ lint job จาก medium เป็น small → ลด 50%",
"เพิ่ม cache สำหรับ npm/pip → ลด install time 60-80%",
"Skip CI สำหรับ docs-only changes → ลด 20-30% runs",
"ใช้ path-filtering → รัน tests เฉพาะ changed modules",
]
for tip in tips:
print(f" • {tip}")
mon = CICDMonitoring()
mon.show_api()
mon.cost_dashboard()
mon.optimization_tips()
Custom Orb สำหรับองค์กร
# custom_orb.py — Custom Orb for organization
import json
class CustomOrb:
ORB_TEMPLATE = """
# orb.yml — Custom cost-optimized orb
version: 2.1
description: "Organization CI/CD orb with cost optimization built-in"
executors:
# Pre-defined optimized executors
light:
docker:
- image: cimg/base:current
resource_class: small
standard:
docker:
- image: cimg/node:20.11
resource_class: medium
heavy:
docker:
- image: cimg/node:20.11
resource_class: large
commands:
# Cached install with fallback
smart-install:
parameters:
package-manager:
type: enum
enum: [npm, yarn, pnpm]
default: npm
steps:
- restore_cache:
keys:
- deps-v2-{{ checksum "package-lock.json" }}
- deps-v2-
- run:
name: Install Dependencies
command: |
if [ -d node_modules ]; then
echo "Cache hit - skipping install"
else
npm ci --prefer-offline
fi
- save_cache:
key: deps-v2-{{ checksum "package-lock.json" }}
paths: [node_modules]
# Conditional step based on changed files
run-if-changed:
parameters:
paths:
type: string
command:
type: string
steps:
- run:
name: Check changed files
command: |
CHANGED=$(git diff --name-only HEAD~1 | grep -E "<< parameters.paths >>" || true)
if [ -z "$CHANGED" ]; then
echo "No relevant changes — skipping"
circleci-agent step halt
fi
- run:
name: Execute
command: << parameters.command >>
jobs:
test:
executor: light
steps:
- checkout
- smart-install
- run: npm test
"""
def show_template(self):
print("=== Custom Orb Template ===")
print(self.ORB_TEMPLATE[:600])
def publish_steps(self):
print(f"\n=== Publish Custom Orb ===")
steps = [
"circleci namespace create my-org github",
"circleci orb create my-org/cost-optimized",
"circleci orb pack src/ > orb.yml",
"circleci orb validate orb.yml",
"circleci orb publish orb.yml my-org/cost-optimized@0.1.0",
]
for i, step in enumerate(steps, 1):
print(f" {i}. {step}")
orb = CustomOrb()
orb.show_template()
orb.publish_steps()
FAQ - คำถามที่พบบ่อย
Q: CircleCI แพงไหม?
A: Free plan: 30,000 credits/month (พอสำหรับ side projects) Performance plan: เริ่ม $15/month + credits ($0.0006/credit) ทีมเล็ก (5 devs): ~$50-150/month (optimized) ทีมกลาง (20 devs): ~$200-800/month ลดค่าใช้จ่าย: optimize resource class, caching, branch filtering
Q: Orbs ช่วยลดค่าใช้จ่ายอย่างไร?
A: 1. Built-in caching (node, python orbs) — ลด install time 50-80% 2. Pre-configured executors — ใช้ resource class ที่เหมาะสม 3. Reusable config — ลด config errors ที่ทำให้ build ช้า 4. Docker Layer Caching orb — ลด Docker build time 60%+ 5. Path-filtering orb — skip jobs ที่ไม่เกี่ยวข้อง
Q: Resource class ไหนคุ้มที่สุด?
A: small (5 cr/min): lint, unit tests, simple scripts → ถูกที่สุด medium (10 cr/min): general builds, integration tests → default ดี large (20 cr/min): parallel tests, heavy compilation → ใช้เมื่อจำเป็น machine (40 cr/min): Docker-in-Docker → แพง ใช้เฉพาะ Docker builds กฎ: เริ่มจาก small แล้ว upgrade เมื่อ job ช้าเกินไป
Q: Self-hosted runners คุ้มไหม?
A: คุ้มเมื่อ: ใช้ credits > 500,000/month, มี idle servers, ต้องการ custom hardware (GPU) ไม่คุ้มเมื่อ: ทีมเล็ก, ไม่มี DevOps ดูแล infrastructure, ใช้ credits น้อย ข้อดี: ไม่เสีย credits, custom environment, faster (local network) ข้อเสีย: ต้องดูแล infrastructure, security, scaling เอง
