← DevOps← DevOps
DevOpsDevOps18 Th7, 2026Jul 18, 202620 phút đọc16 min read

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

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ạnhScriptingPhát triển tooling
Mục tiêuTự độ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 đờiVài giờ đến vài thángNhiều năm
Quy mô điển hình10–500 dòngHàng nghìn dòng, nhiều package
Ưu tiênViết nhanh, dễ đọcHiệu năng, static typing, dễ phân phối
Cách phân phốiCopy file / pip installMột binary tĩnh duy nhất, container image
Xử lý lỗiFail fast, in ra rồi thoátStructured error, retry, observability
TestingThủ công hoặc smoke testBộ unit + integration test trong CI
Ngôn ngữ điển hìnhBash, PythonGo, 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 DevOpsVí dụ trong hệ sinh tháiĐiểm yếu
PythonDynamic (có type hint)Viết nhanh, thư viện khổng lồ (boto3, requests), dễ đọcAnsible, AWS CLI, SaltStack, Jinja2Chậm, quản lý dependency/venv phiền phức, GIL hạn chế threading
GoStaticMột binary tĩnh duy nhất, concurrency mạnh (goroutine), compile nhanhKubernetes, Docker, Terraform, Prometheus, Vault, etcdDài dòng hơn Python, stdlib xử lý data yếu hơn
RubyDynamicDSL đẹp, trưởng thành trong config management đời cũChef, Puppet (DSL), Vagrant, Homebrew, FluentdVị thế trong infra đang giảm, chậm
JavaScript / TypeScriptDynamic / StaticPhổ 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, BackstageNhiều “quirk” runtime, rủi ro supply-chain của npm
RustStaticAn toàn bộ nhớ không cần GC, hiệu năng hàng đầuFirecracker, 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
PowerShellDynamicSố một trên Windows, pipeline hướng object, automation Azure/M365Windows Server, Azure, Active DirectoryDà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.

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ảng gợi ý ra quyết định:

Tình huốngNên dùng
Tác vụ một lần, chủ yếu là lệnh shellBash
Tác vụ một lần nhưng có logic, data, hoặc gọi APIPython
Tool sống lâu, phân phối cho nhiều máyGo
Kubernetes controller / operatorGo (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/AzurePowerShell
Xử lý dữ liệu log/metric, cận MLPython

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.

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
Pythonpip + virtualenv, pipx, container image, uvLệch version interpreter, xung đột dependency, “chạy được trên máy tôi”
GoMộ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
Rubygem, BundlerTrình quản lý version Ruby (rbenv/rvm), native extension
TypeScriptnpm, bundler, pkg/nexe để ra binarynode_modules nặng nề, rủi ro supply-chain
RustMộ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ạngLĩnh vựcGhi chú
HCLTerraform / OpenTofuKhai 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
YAMLKubernetes, CI, Ansible, ComposeKhắp nơi; nhạy với whitespace; nên sinh ra thay vì viết tay ở quy mô lớn
Jsonnet / CUE / DhallSinh configTemplating và validation có kiểu, để chế ngự “cơn lũ” YAML
PromQLPrometheusNgôn ngữ truy vấn metric và luật alert
RegoOpen Policy Agent (OPA)Policy-as-code cho admission control và authorization
Jinja2 / Go templateAnsible, HelmTemplating text nhúng trong công cụ
StarlarkBazel, một số CINgô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

Best Practices

  1. 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.
  2. 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.
  3. Đư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.
  4. Pin và lock dependency. Dùng requirements.txt kè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.
  5. 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.
  6. 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ùng set -e; trong Python, đừng except: pass một cách bao trùm.
  7. 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ý do terraform plan tồn tại trước apply.
  8. Dùng type hint và linter, và chạy chúng trong CI. mypy/ruff cho Python, golangci-lint/go vet cho Go, ESLint + tsconfig strict cho TS, clippy cho 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í.
  9. Format code tự động. gofmt/ruff format/prettier/rustfmt chấ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.
  10. 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.
  11. 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 gitleaks hoặc trufflehog trong CI. Một secret đã commit coi như bị lộ ngay khi push.
  12. Viết test cho mọi thứ thay đổi hạ tầng. Chỉ cần một bộ pytest/go test nhỏ 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.
  13. 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.
  14. 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.
  15. 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.
  16. Đọ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

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

Scripting vs. tooling development

These are two distinct modes of programming in DevOps, and they favor different languages:

AspectScriptingTooling development
GoalAutomate a task quicklyBuild a maintained product (CLI, service, operator)
LifespanHours to monthsYears
Typical size10–500 linesThousands of lines, multiple packages
PrioritiesSpeed of writing, readabilityPerformance, static typing, easy distribution
DistributionCopy the file / pip installSingle static binary, container image
Error handlingFail fast, print and exitStructured errors, retries, observability
TestingManual or a smoke testUnit + integration test suite in CI
Typical languageBash, PythonGo, 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

LanguageTypingStrengths for DevOpsEcosystem examplesWeaknesses
PythonDynamic (optional hints)Fast to write, huge library ecosystem (boto3, requests), readableAnsible, AWS CLI, SaltStack, Jinja2Slow, dependency/venv management, GIL limits threading
GoStaticSingle static binary, great concurrency (goroutines), fast compileKubernetes, Docker, Terraform, Prometheus, Vault, etcdMore verbose than Python, smaller stdlib for data work
RubyDynamicElegant DSLs, mature in legacy config managementChef, Puppet (DSL), Vagrant, Homebrew, FluentdDeclining mindshare in infra, slower
JavaScript / TypeScriptDynamic / StaticUbiquitous, serverless-friendly, CDK/Pulumi support, full-stack teams share one languageAWS CDK, Pulumi, many CI plugins, Node-based tooling, BackstageRuntime quirks, npm supply-chain risk
RustStaticMemory safety without GC, top-tier performanceFirecracker, ripgrep, many new CLI tools, eBPF tooling (Aya), DenoSteep learning curve, slower to write
PowerShellDynamicFirst-class on Windows, object pipeline, Azure/M365 automationWindows Server, Azure, Active DirectoryVerbose, Windows-centric heritage

The interpreted vs. compiled distinction

This single axis explains most of the practical differences you will feel day to day.

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

A decision cheat-sheet:

SituationReach for
Quick one-off, lots of shell commands involvedBash
One-off with logic, data, or API callsPython
Long-lived tool distributed to many machinesGo
Kubernetes controller / operatorGo (client-go / controller-runtime)
Infrastructure as code (real programming, not HCL)TypeScript/Go/Python via CDK or Pulumi
Ultra-low-latency agent, eBPF, memory-constrainedRust
Windows/Active Directory/Azure administrationPowerShell
Log/metric data crunching, ML-adjacentPython

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.

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:

LanguageTypical distributionPain points
Pythonpip + virtualenv, pipx, container image, uvInterpreter version drift, dependency conflicts, “works on my machine”
GoSingle static binary (cross-compile with GOOS/GOARCH)Binary size (mitigate with build flags), no dynamic plugins
Rubygem, BundlerRuby version managers (rbenv/rvm), native extensions
TypeScriptnpm, bundlers, pkg/nexe for binariesnode_modules weight, supply-chain exposure
RustSingle 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 / formatDomainNotes
HCLTerraform / OpenTofuDeclarative infra; not a general language — reach for CDKTF/Pulumi when you need real logic
YAMLKubernetes, CI, Ansible, ComposeUbiquitous; whitespace-sensitive; generate it rather than hand-writing at scale
Jsonnet / CUE / DhallConfig generationTemplating and validation with types, to tame YAML sprawl
PromQLPrometheusQuery language for metrics and alerting rules
RegoOpen Policy Agent (OPA)Policy-as-code for admission control and authorization
Jinja2 / Go templatesAnsible, HelmText templating embedded in tools
StarlarkBazel, some CIPython-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

Best Practices

  1. 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.
  2. 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.
  3. 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.
  4. Pin and lock dependencies. Use requirements.txt with 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.
  5. 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.
  6. 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, use set -e; in Python, do not blanket-except: pass.
  7. Add --dry-run (plan) flags to destructive tooling. Let operators preview changes before they touch production. This is why terraform plan exists before apply.
  8. Use type hints and linters, and run them in CI. mypy/ruff for Python, golangci-lint/go vet for Go, ESLint + strict tsconfig for TS, clippy for Rust. Static analysis catches what tired reviewers miss and enforces consistency for free.
  9. Format code automatically. gofmt/ruff format/prettier/rustfmt end style debates and keep diffs meaningful. Make formatting a pre-commit hook or CI check.
  10. 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.
  11. Never hardcode secrets. Read from environment variables or a secrets manager (Vault, AWS Secrets Manager, SOPS); scan repos with tools like gitleaks or trufflehog in CI. A committed secret is compromised the moment it is pushed.
  12. Write tests for anything that mutates infrastructure. Even a small pytest/go test suite over the logic (with mocked APIs) prevents expensive mistakes. Test the dangerous paths — deletion, scaling down, destructive migrations — hardest.
  13. 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.
  14. 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.
  15. 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.
  16. 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