Ngôn ngữ lập trình cho DevOpsProgramming Languages for DevOps
Thuộc bộ kiến thức DevOps Roadmap.
Tổng quan
DevOps nằm ở giao điểm giữa software engineering và vận hành hệ thống, và gần như mọi thứ trong lĩnh vực này — provisioning, deployment pipeline, monitoring, tooling xử lý sự cố — đều được điều khiển bằng code. Một DevOps engineer không biết lập trình sẽ chỉ dừng lại ở việc click chuột trên console và chắp vá script của người khác. Nắm vững ít nhất một ngôn ngữ đa dụng (và đọc hiểu được vài ngôn ngữ khác) chính là thứ biến “công việc vận hành” thành “công việc kỹ thuật”: bạn có thể tự động hóa tác vụ lặp lại, xây dựng internal tool, mở rộng các platform sẵn có, và debug chính hệ thống mình đang vận hành.
Hệ sinh thái hạ tầng hiện đại cũng “nói” những ngôn ngữ cụ thể. Kubernetes, Docker, Terraform, Prometheus và phần lớn các dự án CNCF được viết bằng Go. Ansible cùng một lượng lớn tooling về automation và data là Python. Các công cụ configuration management đời trước (Chef, Puppet, Vagrant) là Ruby. Hiểu các ngôn ngữ này nghĩa là bạn đọc được source code của công cụ mình phụ thuộc vào, viết được plugin/provider, và tự đóng góp bản vá thay vì chờ upstream.
Bạn không cần thành thạo cả năm ngôn ngữ. Mục tiêu thực tế là: một ngôn ngữ scripting cho automation hằng ngày (thường là Python), một ngôn ngữ systems/tooling để xây CLI và service hiệu năng cao (thường là Go), cộng với shell scripting như một kỹ năng bắt buộc (được trình bày trong chủ đề Terminal & Scripting). Trang này nói về cách chọn có chủ đích thay vì chọn theo thói quen, và về các đánh đổi — hiệu năng, phân phối, concurrency, hệ sinh thái — khiến một ngôn ngữ là công cụ đúng và ngôn ngữ khác là công cụ sai cho một công việc cụ thể.
Một mental model hữu ích: một ngôn ngữ lập trình không chỉ là cú pháp, nó là một câu chuyện về phân phối và vận hành. Câu hỏi “chọn ngôn ngữ nào?” thực ra là ba câu hỏi — tôi viết nó nhanh tới đâu, nó sẽ đến được các máy chạy nó bằng cách nào, và ai sẽ bảo trì nó sau tôi. DevOps đặt trọng số lên câu hỏi thứ hai và thứ ba nặng hơn nhiều so với lập trình viên ứng dụng.
Kiến thức nền tảng
Vì sao DevOps engineer cần một ngôn ngữ lập trình
- Automation: thay các runbook thủ công, dễ sai sót bằng script và tool có tính idempotent. Một runbook viết ra sẽ lỗi thời; một script có test là tài liệu chạy được.
- Glue code: kết nối các API — cloud provider, hệ thống CI, chat, ticketing — thành workflow. Phần lớn “automation” thực chất là integration: lấy output của hệ thống A, biến đổi, đưa vào hệ thống B.
- Đọc source code của công cụ: khi tài liệu không đủ, source code là nguồn sự thật cuối cùng. Đọc được Go và Python nghĩa là cả stack cloud-native trở nên dễ hiểu.
- Mở rộng platform: Terraform provider, Kubernetes operator/controller, plugin CI, module Ansible, plugin Backstage đều đòi hỏi lập trình thực thụ, không phải cấu hình.
- Debug production: hiểu memory model, concurrency và hành vi I/O của ứng dụng mình vận hành giúp chẩn đoán sự cố nhanh hơn. Biết một goroutine leak hay tình trạng nghẽn GIL của Python trông ra sao sẽ rút ngắn thời gian sự cố.
- Xử lý dữ liệu: parse log, biến đổi metric, đối soát inventory và sinh báo cáo là việc hằng ngày, thưởng công cho ngôn ngữ có hỗ trợ text và cấu trúc dữ liệu tốt.
Scripting vs. phát triển tooling
Đây là hai “chế độ” lập trình khác nhau trong DevOps, và mỗi chế độ phù hợp với ngôn ngữ khác nhau:
| Khía cạnh | Scripting | Phát triển tooling |
|---|---|---|
| Mục tiêu | Tự động hóa nhanh một tác vụ | Xây sản phẩm được bảo trì lâu dài (CLI, service, operator) |
| Vòng đời | Vài giờ đến vài tháng | Nhiều năm |
| Quy mô điển hình | 10–500 dòng | Hàng nghìn dòng, nhiều package |
| Ưu tiên | Viết nhanh, dễ đọc | Hiệu năng, static typing, dễ phân phối |
| Cách phân phối | Copy file / pip install | Một binary tĩnh duy nhất, container image |
| Xử lý lỗi | Fail fast, in ra rồi thoát | Structured error, retry, observability |
| Testing | Thủ công hoặc smoke test | Bộ unit + integration test trong CI |
| Ngôn ngữ điển hình | Bash, Python | Go, Rust |
Một lỗi thường gặp: script Bash 2.000 dòng lẽ ra phải là Python, hoặc script Python “viết vội” lớn dần thành service quan trọng mà không ai deploy nổi một cách ổn định. Hãy nhận ra thời điểm script đã vượt ngưỡng và viết lại nó thành một tool đúng nghĩa. Dấu hiệu script đã vượt khỏi ngôn ngữ của nó: cần cấu trúc dữ liệu thật (map lồng nhau, struct), có hơn vài hàm, nhiều người cùng sửa, chạy theo lịch quan trọng, hoặc một lỗi trong nó gây sự cố.
Các ngôn ngữ chính trong hệ sinh thái DevOps
| Ngôn ngữ | Kiểu typing | Điểm mạnh với DevOps | Ví dụ trong hệ sinh thái | Điểm yếu |
|---|---|---|---|---|
| Python | Dynamic (có type hint) | Viết nhanh, thư viện khổng lồ (boto3, requests), dễ đọc | Ansible, AWS CLI, SaltStack, Jinja2 | Chậm, quản lý dependency/venv phiền phức, GIL hạn chế threading |
| Go | Static | Một binary tĩnh duy nhất, concurrency mạnh (goroutine), compile nhanh | Kubernetes, Docker, Terraform, Prometheus, Vault, etcd | Dài dòng hơn Python, stdlib xử lý data yếu hơn |
| Ruby | Dynamic | DSL đẹp, trưởng thành trong config management đời cũ | Chef, Puppet (DSL), Vagrant, Homebrew, Fluentd | Vị thế trong infra đang giảm, chậm |
| JavaScript / TypeScript | Dynamic / Static | Phổ biến khắp nơi, hợp serverless, hỗ trợ CDK/Pulumi, team full-stack dùng chung một ngôn ngữ | AWS CDK, Pulumi, nhiều plugin CI, tooling trên Node, Backstage | Nhiều “quirk” runtime, rủi ro supply-chain của npm |
| Rust | Static | An toàn bộ nhớ không cần GC, hiệu năng hàng đầu | Firecracker, ripgrep, nhiều CLI tool mới, tooling eBPF (Aya), Deno | Đường cong học tập dốc, viết chậm hơn |
| PowerShell | Dynamic | Số một trên Windows, pipeline hướng object, automation Azure/M365 | Windows Server, Azure, Active Directory | Dài dòng, di sản thiên về Windows |
Phân biệt interpreted vs. compiled
Trục duy nhất này giải thích phần lớn khác biệt thực dụng bạn cảm nhận hằng ngày.
- Interpreted (Python, Ruby, JS): không có bước build, sửa-là-chạy, tuyệt vời cho khám phá và tác vụ một lần. Cái giá là runtime (interpreter + dependency) phải hiện diện trên mọi máy đích, và những lỗi mà compiler bắt được chỉ lộ ra khi dòng lệnh đó thực thi.
- Compiled (Go, Rust): một bước build sinh ra artifact tự chứa. Compiler bắt cả lớp bug trước khi ship, và kết quả chạy mà không cần điều kiện runtime nào. Cái giá là vòng lặp phát triển chậm hơn và nhiều nghi thức hơn.
Với DevOps, tính chất “một binary, không dependency” của mô hình compiled có giá trị vượt trội: bạn có thể scp một binary Go vào một host trần hoặc bỏ nó vào container FROM scratch và nó chạy ngay. Đó là lý do tooling cloud-native hội tụ về Go.
Khái niệm chính
Chọn ngôn ngữ nào, khi nào
- Bắt đầu với Python nếu bạn mới vào nghề. Nó phủ được 80% công việc DevOps hằng ngày: gọi API, xử lý file/text, cloud SDK, web service nhỏ, module Ansible. Tính dễ đọc giúp script dùng chung trong team bền vững.
- Học Go thứ hai nếu bạn làm với Kubernetes/cloud-native, cần phát hành CLI cho đồng đội, hoặc viết controller/operator. Riêng khả năng phân phối bằng một binary duy nhất đã đủ đáng giá, và chính công cụ bạn vận hành cũng viết bằng nó.
- Chỉ học Ruby khi bắt buộc — tức là công ty bạn đang chạy Chef hoặc Puppet với nhiều custom code. Nó là ngôn ngữ đáng yêu, nhưng công việc hạ tầng mới hiếm khi bắt đầu bằng nó.
- TypeScript hợp lý khi tổ chức chuẩn hóa trên AWS CDK hoặc Pulumi, hoặc khi platform team phục vụ chủ yếu các product team dùng Node.js, hoặc khi bạn xây developer portal nội bộ (Backstage).
- Rust là khoản đầu tư cho tooling đòi hỏi hiệu năng cao (agent, proxy, eBPF, sidecar nơi từng millisecond và megabyte đều quan trọng); không nên là ngôn ngữ thứ nhất hay thứ hai của đa số DevOps engineer.
- PowerShell là bắt buộc ở các môi trường nặng Windows/Azure; pipeline hướng object của nó thực sự mạnh hơn shell dựa trên text khi xử lý dữ liệu có cấu trúc.
Bảng gợi ý ra quyết định:
| Tình huống | Nên dùng |
|---|---|
| Tác vụ một lần, chủ yếu là lệnh shell | Bash |
| Tác vụ một lần nhưng có logic, data, hoặc gọi API | Python |
| Tool sống lâu, phân phối cho nhiều máy | Go |
| Kubernetes controller / operator | Go (client-go / controller-runtime) |
| Infrastructure as code (lập trình thật, không phải HCL) | TypeScript/Go/Python qua CDK hoặc Pulumi |
| Agent latency cực thấp, eBPF, hạn chế bộ nhớ | Rust |
| Quản trị Windows/Active Directory/Azure | PowerShell |
| Xử lý dữ liệu log/metric, cận ML | Python |
Cùng một tác vụ viết bằng Python và Go
Kiểm tra health của một URL — Python thiên về ngắn gọn:
import requests, sys
try:
r = requests.get("https://example.com/healthz", timeout=5)
r.raise_for_status()
print("healthy")
except requests.RequestException as e:
print(f"unhealthy: {e}")
sys.exit(1)
Go thiên về xử lý lỗi tường minh và compile ra binary không phụ thuộc gì thêm:
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://example.com/healthz")
if err != nil || resp.StatusCode >= 400 {
fmt.Println("unhealthy:", err)
os.Exit(1)
}
fmt.Println("healthy")
}
Chú ý sự đánh đổi thu nhỏ ở đây: bản Python ngắn hơn và cần cài requests; bản Go dài hơn, xử lý nhánh lỗi tường minh (không có exception ẩn), và ship ra một binary chạy được ở bất cứ đâu không cần interpreter. Không bản nào “tốt hơn” — chúng tối ưu cho hai điểm khác nhau trên trục scripting/tooling.
Mô hình concurrency (vì sao quan trọng với infra tooling)
Tool DevOps hầu hết là I/O-bound: chúng chờ hàng nghìn API call, network read và thao tác đĩa. Mô hình concurrency quyết định tool fan-out công việc đó gọn gàng tới đâu.
- Python:
asynciocho concurrency thiên về I/O; GIL (Global Interpreter Lock) hạn chế threading nặng CPU về một core tại một thời điểm (dùngmultiprocessing,concurrent.futures.ProcessPoolExecutor, hoặc native extension cho tác vụ CPU). Với bài toán “gọi 500 endpoint song song”,asyncio+aiohttphoặcThreadPoolExecutorchạy tốt vì thời gian chờ giải phóng GIL. - Go: goroutine + channel giúp fan-out hàng nghìn API call đồng thời hay viết watch loop cực kỳ dễ — lý do Go thống trị tooling cloud-native. Một goroutine tốn vài KB; bạn có thể tạo hàng trăm nghìn.
sync.WaitGroup,context.Contextcho cancel/timeout, và channel để phối hợp là những primitive hằng ngày. - Node/TypeScript: event loop đơn luồng; rất tốt cho glue code nặng I/O, kém với tác vụ CPU (làm nghẽn loop — nên đẩy sang worker thread).
Promise.allcho I/O đồng thời. - Rust:
async/awaitvới zero-cost abstraction trên các runtime như Tokio; “fearless concurrency” được borrow checker cưỡng chế tại compile time — kiểm soát tối đa, công sức tối đa.
Một quy tắc thực dụng: nếu việc của tool là “nói chuyện với nhiều thứ cùng lúc mà không sập”, concurrency của Go đỡ đau đầu nhất; nếu là “thi thoảng gọi vài API rồi xử lý kết quả”, sự đơn giản của Python thắng.
Phân phối và đóng gói
Cách code đến được máy chủ quan trọng không kém bản thân code:
| Ngôn ngữ | Cách phân phối điển hình | Điểm đau |
|---|---|---|
| Python | pip + virtualenv, pipx, container image, uv | Lệch version interpreter, xung đột dependency, “chạy được trên máy tôi” |
| Go | Một binary tĩnh (cross-compile bằng GOOS/GOARCH) | Kích thước binary (giảm bằng build flag), không có plugin động |
| Ruby | gem, Bundler | Trình quản lý version Ruby (rbenv/rvm), native extension |
| TypeScript | npm, bundler, pkg/nexe để ra binary | node_modules nặng nề, rủi ro supply-chain |
| Rust | Một binary tĩnh (cargo build --release) | Thời gian compile lâu |
Cross-compile một tool Go cho server Linux ngay từ máy Mac chỉ mất một dòng — không cần máy đích, không cần toolchain trên host:
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 ./cmd/myapp
GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 ./cmd/myapp # cho node Graviton/ARM
Với Python, câu trả lời hiện đại cho “địa ngục dependency” là ngừng cài vào interpreter hệ thống. Dùng virtual environment theo từng project và một tool dựa trên lockfile:
python -m venv .venv && source .venv/bin/activate # cô lập dependency
pip install -r requirements.txt # hoặc: uv pip sync requirements.txt
pipx install some-cli-tool # cài CLI tool cô lập
Hệ thống type: dynamic vs. static, và gradual typing
Static typing (Go, Rust, TypeScript) bắt được một lớp lớn lỗi — gõ nhầm, sai kiểu tham số, xử lý nil/None — trước khi code chạy, điều cực kỳ quan trọng với tool thay đổi hạ tầng, nơi một crash lúc runtime nghĩa là một thay đổi áp dụng dở dang. Ngôn ngữ dynamic đánh đổi an toàn đó lấy tốc độ viết.
Trung dung thực dụng là gradual typing: type hint của Python được mypy/pyright kiểm tra, và TypeScript trên JavaScript. Bạn có phần lớn sự an toàn mà không từ bỏ vòng lặp phản hồi nhanh. Với bất kỳ đoạn Python nào sống lâu hơn một tuần hoặc do nhiều người sửa, hãy thêm type hint — chi phí nhỏ, lợi ích (autocomplete trong editor, refactor an toàn, tự làm tài liệu) lớn.
def scale_deployment(name: str, replicas: int, namespace: str = "default") -> None:
... # cả reviewer lẫn mypy đều biết chính xác hàm này cần gì
Ngoài các ngôn ngữ đa dụng
DevOps còn dùng nhiều domain-specific language mà bạn cần nhận diện được. Chúng không thay thế ngôn ngữ lập trình, nhưng bạn sẽ đọc và viết chúng mỗi ngày:
| DSL / định dạng | Lĩnh vực | Ghi chú |
|---|---|---|
| HCL | Terraform / OpenTofu | Khai báo hạ tầng; không phải ngôn ngữ đa dụng — cần logic thật thì dùng CDKTF/Pulumi |
| YAML | Kubernetes, CI, Ansible, Compose | Khắp nơi; nhạy với whitespace; nên sinh ra thay vì viết tay ở quy mô lớn |
| Jsonnet / CUE / Dhall | Sinh config | Templating và validation có kiểu, để chế ngự “cơn lũ” YAML |
| PromQL | Prometheus | Ngôn ngữ truy vấn metric và luật alert |
| Rego | Open Policy Agent (OPA) | Policy-as-code cho admission control và authorization |
| Jinja2 / Go template | Ansible, Helm | Templating text nhúng trong công cụ |
| Starlark | Bazel, một số CI | Ngôn ngữ config giống Python, mang tính deterministic |
Một anti-pattern lặp đi lặp lại là đẩy YAML hay HCL vượt giới hạn bằng copy-paste và templating thông minh cho tới khi nó thành một pseudo-program không bảo trì nổi. Khi cảm thấy điều đó, đó là tín hiệu để chuyển logic sang một ngôn ngữ thật (Pulumi, CDK, một bộ code generator) và coi DSL chỉ là định dạng đầu ra.
Các lỗi thường gặp
- Viết lại mọi thứ bằng ngôn ngữ “hot”. Một script Python đang chạy tốt không cần thành Rust. Việc viết lại được biện minh bởi nỗi đau cụ thể (phân phối, hiệu năng, an toàn), không phải trào lưu.
- Bỏ qua câu chuyện phân phối cho tới phút chót. Một tool Python xuất sắc mà không ai cài đặt ổn định được còn tệ hơn một binary Go tầm thường nhưng chạy ngay.
- Dependency không pin. “Hôm qua chạy được” gần như luôn truy ra một version dependency thả nổi.
- Shell script lẽ ra phải là chương trình. Khi bạn cần mảng dữ liệu có cấu trúc, số học với số thực, hay xử lý lỗi thật, Bash sẽ chống lại bạn.
- Hardcode secret và giả định về môi trường. Code chỉ chạy trên laptop tác giả vì đường dẫn, env var, hay credential ngầm định.
- Không test code thay đổi hạ tầng. Bán kính ảnh hưởng của một bug trong tool provisioning chính là môi trường production của bạn.
Best Practices
- Nắm sâu một ngôn ngữ trước khi học thêm. Biết hời hợt ba ngôn ngữ tạo ra tooling tệ hơn là giỏi hẳn một ngôn ngữ. Sâu nghĩa là bạn biết standard library, các idiom, và công cụ debug.
- Chọn ngôn ngữ theo vòng đời và bán kính ảnh hưởng của công việc. Tác vụ chạy một lần → shell/Python. Tool phân tán chạy nhiều năm → Go. Viết lại khi script vượt quá ngôn ngữ của nó thay vì chồng chất phức tạp.
- Đưa mọi script vào version control. Nếu nó từng chạy trên production, nó phải nằm trong Git với lịch sử và review — không phải trong home directory của ai đó hay một trang wiki. Automation không được track là một sự cố tương lai.
- Pin và lock dependency. Dùng
requirements.txtkèm hash /poetry.lock/uv.lock,go.mod+go.sum,package-lock.json,Cargo.lock. Dependency không pin là lý do “hôm qua chạy được” mà hôm nay hỏng, và là cửa cho tấn công supply-chain. - Viết automation có tính idempotent. Chạy script hai lần phải an toàn. Kiểm tra trạng thái hiện tại trước khi thay đổi — tư tưởng cốt lõi của Ansible và Terraform. Idempotency là thứ cho phép chạy lại automation sau một thất bại nửa chừng.
- Xử lý lỗi tường minh — không bao giờ bỏ qua. Script provisioning tiếp tục chạy sau một bước thất bại sẽ tạo ra hệ thống “cấu hình nửa vời” — trạng thái khó debug nhất. Trong Go, đừng vứt
err; trong Bash, dùngset -e; trong Python, đừngexcept: passmột cách bao trùm. - Thêm flag
--dry-run(plan) cho tooling có tính phá hủy. Cho phép operator xem trước thay đổi trước khi chạm vào production. Đó là lý doterraform plantồn tại trướcapply. - Dùng type hint và linter, và chạy chúng trong CI.
mypy/ruffcho Python,golangci-lint/go vetcho Go, ESLint +tsconfigstrict cho TS,clippycho Rust. Static analysis bắt những lỗi mà reviewer mệt mỏi bỏ sót và cưỡng chế tính nhất quán miễn phí. - Format code tự động.
gofmt/ruff format/prettier/rustfmtchấm dứt tranh cãi về style và giữ diff có ý nghĩa. Đưa format thành pre-commit hook hoặc kiểm tra CI. - Log ra stdout/stderr theo dạng có cấu trúc. Tool in log JSON tích hợp được với mọi log pipeline; tránh tự ghi file log mà không ai rotate. Theo quy tắc twelve-factor: coi log là một luồng sự kiện.
- Không bao giờ hardcode secret. Đọc từ biến môi trường hoặc secrets manager (Vault, AWS Secrets Manager, SOPS); quét repo bằng
gitleakshoặctrufflehogtrong CI. Một secret đã commit coi như bị lộ ngay khi push. - Viết test cho mọi thứ thay đổi hạ tầng. Chỉ cần một bộ
pytest/go testnhỏ cho phần logic (mock API) cũng ngăn được những sai lầm đắt giá. Test kỹ nhất những nhánh nguy hiểm — xóa, scale down, migration phá hủy. - Làm tool có thể cấu hình, không hardcode. Nhận config qua flag, env var và file (theo thứ tự ưu tiên đó); có giá trị mặc định hợp lý; validate input sớm và báo lỗi rõ ràng.
- Thiết kế cho observability. Thoát với exit code có ý nghĩa, phát ra metric hoặc chí ít là log thời gian, và làm cho lỗi có thể chẩn đoán chỉ từ output — bạn sẽ chạy các tool này lúc 3 giờ sáng.
- Giữ dependency ít và đã được kiểm chứng. Mỗi dependency là bề mặt tấn công và gánh nặng bảo trì tương lai. Ưu tiên standard library; audit thứ bạn thêm vào; cảnh giác với package bị bỏ hoang.
- Đọc source code của các công cụ bạn dùng. Clone Kubernetes hay Terraform rồi lần theo một code path là cách lên trình nhanh bậc nhất — và là cách duy nhất để thực sự hiểu một bug trong chúng.
Tài liệu tham khảo
- DevOps Roadmap — roadmap.sh/devops
- Tài liệu chính thức của Python
- Real Python — hướng dẫn
- Tài liệu chính thức của Go
- A Tour of Go
- Effective Go
- Go by Example
- Standard Go Project Layout
- Tài liệu chính thức của Ruby
- Tài liệu TypeScript
- The Rust Programming Language (sách)
- Tài liệu PowerShell
- Automate the Boring Stuff with Python — Al Sweigart
- uv — trình quản lý package Python nhanh
- Ruff — linter/formatter cho Python
- golangci-lint
- The Twelve-Factor App
- CNCF landscape
- Sách: The Phoenix Project — Gene Kim, Kevin Behr, George Spafford (bối cảnh vì sao automation quan trọng)
- Sách: The Go Programming Language — Donovan & Kernighan
- Sách: Fluent Python — Luciano Ramalho (O’Reilly)
- Sách: Site Reliability Engineering — Google (đọc miễn phí)
Part of the DevOps Roadmap knowledge base.
Overview
DevOps sits at the intersection of software engineering and operations, and virtually everything in that space — provisioning, deployment pipelines, monitoring, incident tooling — is driven by code. A DevOps engineer who cannot program is limited to clicking through consoles and gluing together other people’s scripts. Knowing at least one general-purpose language well (and being able to read several others) is what turns “operations work” into “engineering work”: you can automate repetitive tasks, build internal tools, extend existing platforms, and debug the systems you operate.
The modern infrastructure ecosystem also speaks specific languages. Kubernetes, Docker, Terraform, Prometheus, and most CNCF projects are written in Go. Ansible, and a huge share of automation and data tooling, is Python. Older configuration management tools (Chef, Puppet, Vagrant) are Ruby. Understanding these languages means you can read the source of the tools you depend on, write plugins and providers, and contribute fixes instead of waiting for upstream.
You do not need to master five languages. The realistic goal is: one scripting language for daily automation (usually Python), one systems/tooling language for building performant CLIs and services (usually Go), plus shell scripting as table stakes (covered in the Terminal & Scripting topic). This page is about how to choose deliberately rather than by habit, and about the trade-offs — performance, distribution, concurrency, ecosystem — that make one language the right tool and another the wrong one for a given job.
A useful mental model: a programming language is not just syntax, it is a distribution and operations story. The question “which language?” is really three questions — how fast can I write it, how will it reach the machines that run it, and who will maintain it after me. DevOps weighs the second and third questions much more heavily than application developers do.
Fundamentals
Why a DevOps engineer needs a programming language
- Automation: replace manual, error-prone runbooks with idempotent scripts and tools. A documented runbook decays; a tested script is executable documentation.
- Glue code: connect APIs — cloud providers, CI systems, chat, ticketing — into workflows. Most “automation” is actually integration: take output from system A, transform it, feed it to system B.
- Reading tool source: when documentation ends, source code is the ground truth. Being able to read Go and Python means the entire cloud-native stack becomes legible.
- Extending platforms: Terraform providers, Kubernetes operators/controllers, CI plugins, Ansible modules, Backstage plugins all require real programming, not configuration.
- Debugging production: understanding memory models, concurrency, and I/O behavior of the apps you run helps you diagnose incidents faster. Knowing what a goroutine leak or a Python GIL contention looks like shortens an outage.
- Data wrangling: parsing logs, reshaping metrics, reconciling inventories, and generating reports are everyday tasks that reward a language with good text and data-structure support.
Scripting vs. tooling development
These are two distinct modes of programming in DevOps, and they favor different languages:
| Aspect | Scripting | Tooling development |
|---|---|---|
| Goal | Automate a task quickly | Build a maintained product (CLI, service, operator) |
| Lifespan | Hours to months | Years |
| Typical size | 10–500 lines | Thousands of lines, multiple packages |
| Priorities | Speed of writing, readability | Performance, static typing, easy distribution |
| Distribution | Copy the file / pip install | Single static binary, container image |
| Error handling | Fail fast, print and exit | Structured errors, retries, observability |
| Testing | Manual or a smoke test | Unit + integration test suite in CI |
| Typical language | Bash, Python | Go, Rust |
A common failure mode is a 2,000-line Bash script that should have been Python, or a quick Python script that grew into a critical service nobody can deploy reliably. Recognize when a script has crossed the line and rewrite it as a tool. Warning signs that a script has outgrown its language: it needs real data structures (nested maps, structs), it has more than a couple of functions, multiple people edit it, it runs on a schedule that matters, or a failure in it causes an incident.
The main languages in the DevOps ecosystem
| Language | Typing | Strengths for DevOps | Ecosystem examples | Weaknesses |
|---|---|---|---|---|
| Python | Dynamic (optional hints) | Fast to write, huge library ecosystem (boto3, requests), readable | Ansible, AWS CLI, SaltStack, Jinja2 | Slow, dependency/venv management, GIL limits threading |
| Go | Static | Single static binary, great concurrency (goroutines), fast compile | Kubernetes, Docker, Terraform, Prometheus, Vault, etcd | More verbose than Python, smaller stdlib for data work |
| Ruby | Dynamic | Elegant DSLs, mature in legacy config management | Chef, Puppet (DSL), Vagrant, Homebrew, Fluentd | Declining mindshare in infra, slower |
| JavaScript / TypeScript | Dynamic / Static | Ubiquitous, serverless-friendly, CDK/Pulumi support, full-stack teams share one language | AWS CDK, Pulumi, many CI plugins, Node-based tooling, Backstage | Runtime quirks, npm supply-chain risk |
| Rust | Static | Memory safety without GC, top-tier performance | Firecracker, ripgrep, many new CLI tools, eBPF tooling (Aya), Deno | Steep learning curve, slower to write |
| PowerShell | Dynamic | First-class on Windows, object pipeline, Azure/M365 automation | Windows Server, Azure, Active Directory | Verbose, Windows-centric heritage |
The interpreted vs. compiled distinction
This single axis explains most of the practical differences you will feel day to day.
- Interpreted (Python, Ruby, JS): no build step, edit-and-run, excellent for exploration and one-offs. The cost is that the runtime (interpreter + dependencies) must be present on every target machine, and errors that a compiler would catch surface only when that line executes.
- Compiled (Go, Rust): a build step produces a self-contained artifact. The compiler catches whole classes of bugs before shipping, and the result runs with no runtime prerequisites. The cost is slower iteration and more ceremony.
For DevOps, the compiled model’s “one binary, no dependencies” property is disproportionately valuable: you can scp a Go binary to a bare host or drop it in a FROM scratch container and it just runs. That is why cloud-native tooling converged on Go.
Key Concepts
Which language to pick, and when
- Start with Python if you are new. It covers 80% of daily DevOps work: API calls, file/text processing, cloud SDKs, small web services, Ansible modules. Its readability makes shared team scripts sustainable.
- Learn Go second if you work with Kubernetes/cloud-native tooling, need to ship CLIs to teammates, or write controllers/operators. The single-binary distribution story alone justifies it, and the tooling you operate is written in it.
- Learn Ruby only if forced — i.e., your organization runs Chef or Puppet with heavy custom code. It is a lovely language, but new infrastructure work rarely starts in it.
- TypeScript makes sense when your org standardizes on AWS CDK or Pulumi, or when platform engineers serve predominantly Node.js product teams, or you are building internal developer portals (Backstage).
- Rust is an investment for performance-critical tooling (agents, proxies, eBPF, sidecars where every millisecond and megabyte counts); not a first or second language for most DevOps engineers.
- PowerShell is non-negotiable in Windows/Azure-heavy shops; its object pipeline is genuinely more powerful than text-based shells for structured data.
A decision cheat-sheet:
| Situation | Reach for |
|---|---|
| Quick one-off, lots of shell commands involved | Bash |
| One-off with logic, data, or API calls | Python |
| Long-lived tool distributed to many machines | Go |
| Kubernetes controller / operator | Go (client-go / controller-runtime) |
| Infrastructure as code (real programming, not HCL) | TypeScript/Go/Python via CDK or Pulumi |
| Ultra-low-latency agent, eBPF, memory-constrained | Rust |
| Windows/Active Directory/Azure administration | PowerShell |
| Log/metric data crunching, ML-adjacent | Python |
The same task in Python and Go
Checking a URL’s health — Python favors brevity:
import requests, sys
try:
r = requests.get("https://example.com/healthz", timeout=5)
r.raise_for_status()
print("healthy")
except requests.RequestException as e:
print(f"unhealthy: {e}")
sys.exit(1)
Go favors explicit error handling and compiles to a dependency-free binary:
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://example.com/healthz")
if err != nil || resp.StatusCode >= 400 {
fmt.Println("unhealthy:", err)
os.Exit(1)
}
fmt.Println("healthy")
}
Notice the trade-off in miniature: the Python version is shorter and needs requests installed; the Go version is longer, handles the error path explicitly (no hidden exceptions), and ships as a binary you can run anywhere with no interpreter. Neither is “better” — they optimize for different points in the scripting/tooling spectrum.
Concurrency models (why it matters for infra tooling)
DevOps tools are overwhelmingly I/O-bound: they wait on thousands of API calls, network reads, and disk operations. The concurrency model determines how gracefully a tool fans that work out.
- Python:
asynciofor I/O-bound concurrency; the GIL (Global Interpreter Lock) restricts CPU-bound threading to one core at a time (usemultiprocessing,concurrent.futures.ProcessPoolExecutor, or native extensions for CPU work). For “call 500 endpoints in parallel,”asyncio+aiohttpor aThreadPoolExecutorworks well because the waiting releases the GIL. - Go: goroutines + channels make it trivial to fan out thousands of concurrent API calls or watch loops — the reason it dominates cloud-native tooling. A goroutine costs a few KB; you can spawn hundreds of thousands.
sync.WaitGroup,context.Contextfor cancellation/timeouts, and channels for coordination are the everyday primitives. - Node/TypeScript: single-threaded event loop; excellent for I/O-heavy glue, poor for CPU work (which blocks the loop — offload to worker threads).
Promise.allfor concurrent I/O. - Rust:
async/awaitwith zero-cost abstractions over runtimes like Tokio; fearless concurrency enforced by the borrow checker at compile time — maximum control, maximum effort.
A concrete rule of thumb: if your tool’s job is “talk to many things at once and don’t fall over,” Go’s concurrency will save you the most grief; if it is “occasionally call a few APIs and process the result,” Python’s simplicity wins.
Distribution and packaging
How your code reaches machines matters as much as the code:
| Language | Typical distribution | Pain points |
|---|---|---|
| Python | pip + virtualenv, pipx, container image, uv | Interpreter version drift, dependency conflicts, “works on my machine” |
| Go | Single static binary (cross-compile with GOOS/GOARCH) | Binary size (mitigate with build flags), no dynamic plugins |
| Ruby | gem, Bundler | Ruby version managers (rbenv/rvm), native extensions |
| TypeScript | npm, bundlers, pkg/nexe for binaries | node_modules weight, supply-chain exposure |
| Rust | Single static binary (cargo build --release) | Long compile times |
Cross-compiling a Go tool for a Linux server from your Mac is one line — no target machine, no toolchain on the host:
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 ./cmd/myapp
GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 ./cmd/myapp # for Graviton/ARM nodes
For Python, the modern answer to dependency hell is to stop installing into the system interpreter. Use per-project virtual environments and a lockfile-based tool:
python -m venv .venv && source .venv/bin/activate # isolate dependencies
pip install -r requirements.txt # or: uv pip sync requirements.txt
pipx install some-cli-tool # install CLI tools in isolation
Type systems: dynamic vs. static, and gradual typing
Static typing (Go, Rust, TypeScript) catches a large class of errors — typos, wrong argument types, nil/None handling — before the code runs, which matters enormously for tools that mutate infrastructure where a runtime crash means a half-applied change. Dynamic languages trade that safety for speed of authoring.
The pragmatic middle ground is gradual typing: Python’s type hints checked by mypy/pyright, and TypeScript over JavaScript. You get much of the safety without abandoning the fast feedback loop. For any Python that lives longer than a week or is edited by more than one person, add type hints — the cost is small and the payoff (editor autocomplete, refactor safety, self-documentation) is large.
def scale_deployment(name: str, replicas: int, namespace: str = "default") -> None:
... # a reviewer and mypy both know exactly what this expects
Beyond general-purpose languages
DevOps also involves domain-specific languages you should recognize. These are not substitutes for a programming language, but you will read and write them daily:
| DSL / format | Domain | Notes |
|---|---|---|
| HCL | Terraform / OpenTofu | Declarative infra; not a general language — reach for CDKTF/Pulumi when you need real logic |
| YAML | Kubernetes, CI, Ansible, Compose | Ubiquitous; whitespace-sensitive; generate it rather than hand-writing at scale |
| Jsonnet / CUE / Dhall | Config generation | Templating and validation with types, to tame YAML sprawl |
| PromQL | Prometheus | Query language for metrics and alerting rules |
| Rego | Open Policy Agent (OPA) | Policy-as-code for admission control and authorization |
| Jinja2 / Go templates | Ansible, Helm | Text templating embedded in tools |
| Starlark | Bazel, some CI | Python-like config language, deterministic |
A recurring anti-pattern is pushing YAML or HCL past its limits with copy-paste and clever templating until it becomes an unmaintainable pseudo-program. When you feel that happening, that is the signal to move logic into a real language (Pulumi, CDK, a code generator) and treat the DSL as an output format.
Common pitfalls
- Rewriting everything in the “cool” language. A working Python script does not need to be Rust. Rewrites are justified by concrete pain (distribution, performance, safety), not fashion.
- Ignoring the distribution story until the end. A brilliant Python tool that nobody can install reliably is worse than a mediocre Go binary that just runs.
- Unpinned dependencies. “It worked yesterday” almost always traces to a floating dependency version.
- Shell scripts that should be programs. Once you need arrays of structured data, arithmetic on floats, or real error handling, Bash is fighting you.
- Hardcoded secrets and environment assumptions. Code that only runs on the author’s laptop because of implicit paths, env vars, or credentials.
- No tests on infra-mutating code. The blast radius of a bug in a provisioning tool is your production environment.
Best Practices
- Master one language deeply before adding another. Shallow knowledge of three languages produces worse tooling than deep knowledge of one. Depth means you know the standard library, the idioms, and the debugging tools.
- Match the language to the job’s lifespan and blast radius. Quick one-off task → shell/Python. Long-lived distributed tool → Go. Rewrite when a script outgrows its language rather than piling on complexity.
- Version-control every script. If it ran in production once, it belongs in Git with history and review — not in someone’s home directory or a wiki page. Untracked automation is a future outage.
- Pin and lock dependencies. Use
requirements.txtwith hashes /poetry.lock/uv.lock,go.mod+go.sum,package-lock.json,Cargo.lock. Unpinned dependencies are how “worked yesterday” breaks today and how supply-chain attacks land. - Write idempotent automation. Running a script twice must be safe. Check current state before mutating — the core idea behind Ansible and Terraform. Idempotency is what makes automation re-runnable after a partial failure.
- Handle errors explicitly — never ignore them. A provisioning script that continues after a failed step causes half-configured systems, the worst state to debug. In Go, do not discard
err; in Bash, useset -e; in Python, do not blanket-except: pass. - Add
--dry-run(plan) flags to destructive tooling. Let operators preview changes before they touch production. This is whyterraform planexists beforeapply. - Use type hints and linters, and run them in CI.
mypy/rufffor Python,golangci-lint/go vetfor Go, ESLint + stricttsconfigfor TS,clippyfor Rust. Static analysis catches what tired reviewers miss and enforces consistency for free. - Format code automatically.
gofmt/ruff format/prettier/rustfmtend style debates and keep diffs meaningful. Make formatting a pre-commit hook or CI check. - Log to stdout/stderr in structured form. Tools that print JSON logs integrate with any log pipeline; avoid writing your own log files that nobody rotates. Follow the twelve-factor rule: treat logs as an event stream.
- Never hardcode secrets. Read from environment variables or a secrets manager (Vault, AWS Secrets Manager, SOPS); scan repos with tools like
gitleaksortrufflehogin CI. A committed secret is compromised the moment it is pushed. - Write tests for anything that mutates infrastructure. Even a small
pytest/go testsuite over the logic (with mocked APIs) prevents expensive mistakes. Test the dangerous paths — deletion, scaling down, destructive migrations — hardest. - Make tools configurable, not hardcoded. Accept config via flags, env vars, and files (in that precedence order); provide sane defaults; validate inputs early and fail with a clear message.
- Design for observability. Exit with meaningful codes, emit metrics or at least timing logs, and make failures diagnosable from output alone — you will run these tools at 3 AM.
- Keep dependencies few and vetted. Every dependency is attack surface and future maintenance. Prefer the standard library; audit what you add; watch for abandoned packages.
- Read the source of your tools. Cloning Kubernetes or Terraform and stepping through a code path is one of the fastest ways to level up — and the only way to truly understand a bug in them.
References
- DevOps Roadmap — roadmap.sh/devops
- Python official documentation
- Real Python — tutorials
- Go official documentation
- A Tour of Go
- Effective Go
- Go by Example
- Standard Go Project Layout
- Ruby official documentation
- TypeScript documentation
- The Rust Programming Language (book)
- PowerShell documentation
- Automate the Boring Stuff with Python — Al Sweigart
- uv — fast Python package manager
- Ruff — Python linter/formatter
- golangci-lint
- The Twelve-Factor App
- CNCF landscape
- Book: The Phoenix Project — Gene Kim, Kevin Behr, George Spafford (context on why automation matters)
- Book: The Go Programming Language — Donovan & Kernighan
- Book: Fluent Python — Luciano Ramalho (O’Reilly)
- Book: Site Reliability Engineering — Google (free online)