# infra scripts 예시 --- ## 좋은 예시 1: `scripts/lib/common.sh` (공통 라이브러리) ```bash #!/usr/bin/env bash # common.sh - shared helpers. source this from bin/ scripts. # do NOT execute directly. # shellcheck disable=SC2034 # variables may be used by callers readonly COMMON_SH_LOADED=1 log() { local level="$1"; shift local ts ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" printf '%s [%s] %s\n' "${ts}" "${level}" "$*" >&2 } info() { log INFO "$@"; } warn() { log WARN "$@"; } error() { log ERROR "$@"; } fatal() { log FATAL "$@"; exit 1; } require_cmd() { local cmd="$1" command -v "${cmd}" >/dev/null 2>&1 \ || fatal "required command not found: ${cmd}" } require_env() { local name="$1" local val="${!name:-}" [[ -n "${val}" ]] || fatal "required env var not set: ${name}" } confirm() { # usage: confirm "delete namespace foo?" || return 1 local prompt="${1:-continue?}" if [[ "${CONFIRM:-no}" == "yes" || "${YES:-0}" -eq 1 ]]; then return 0 fi local reply printf '%s [y/N] ' "${prompt}" >&2 read -r reply [[ "${reply}" == "y" || "${reply}" == "Y" ]] } mask_secrets() { sed -E \ -e 's/(password=)[^ ]+/\1***/g' \ -e 's/(token=)[^ ]+/\1***/g' \ -e 's/(Authorization: Bearer )[A-Za-z0-9._-]+/\1***/g' } retry() { local max="$1"; shift local delay="$1"; shift local n=0 until "$@"; do n=$((n + 1)) if (( n >= max )); then error "retry exhausted after ${max} attempts: $*" return 1 fi warn "retry $n/$max failed, sleeping ${delay}s" sleep "${delay}" done } ``` **왜 좋은가:** - log 함수가 ISO 8601 UTC + LEVEL + stderr. - require_cmd / require_env / confirm / mask_secrets / retry 가 재사용 가능한 작은 단위. - shellcheck suppression 은 이유 주석과 함께. --- ## 좋은 예시 2: `scripts/bin/render-diff-apply` (render → diff → apply wrapper) ```bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" usage() { cat <<'EOF' >&2 Usage: render-diff-apply [OPTIONS] --overlay PATH kustomize overlay directory (required) --context NAME kube context name (required) --namespace NS target namespace (optional, derived from overlay) --timeout DUR rollout status timeout (default: 10m) --yes skip interactive confirmation for apply --dry-run render + diff only, no apply -h, --help show this help Environment: CONFIRM=yes non-interactive confirmation (alternative to --yes) Examples: render-diff-apply --overlay k8s/overlays/prod --context prod-eu CONFIRM=yes render-diff-apply --overlay k8s/overlays/prod --context prod-eu --timeout 15m EOF } parse_args() { OVERLAY="" CONTEXT="" NAMESPACE="" TIMEOUT="10m" YES=0 DRY_RUN=0 while [[ $# -gt 0 ]]; do case "$1" in --overlay) OVERLAY="$2"; shift 2 ;; --context) CONTEXT="$2"; shift 2 ;; --namespace) NAMESPACE="$2"; shift 2 ;; --timeout) TIMEOUT="$2"; shift 2 ;; --yes) YES=1; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage; exit 0 ;; *) usage; fatal "unknown arg: $1" ;; esac done [[ -n "${OVERLAY}" ]] || { usage; fatal "--overlay is required"; } [[ -n "${CONTEXT}" ]] || { usage; fatal "--context is required"; } [[ -d "${OVERLAY}" ]] || fatal "overlay not found: ${OVERLAY}" } kctx() { kubectl --context="${CONTEXT}" "$@" } render() { local out="$1" info "rendering ${OVERLAY}" kubectl kustomize "${OVERLAY}" > "${out}" info "rendered $(wc -l < "${out}") lines to ${out}" } validate() { local rendered="$1" info "server-side dry-run validation" kctx apply -f "${rendered}" --dry-run=server >/dev/null } show_diff() { info "computing diff" # kubectl diff exit code: 0 no diff, 1 diff, >1 error set +e kctx diff -k "${OVERLAY}" local rc=$? set -e case "${rc}" in 0) info "no diff" ;; 1) info "diff present" ;; *) fatal "diff failed with code ${rc}" ;; esac return "${rc}" } apply_overlay() { info "applying ${OVERLAY} to context=${CONTEXT}" kctx apply -k "${OVERLAY}" } watch_rollout() { [[ -n "${NAMESPACE}" ]] || return 0 local deployments deployments="$(kctx -n "${NAMESPACE}" get deploy -o jsonpath='{.items[*].metadata.name}' || true)" for d in ${deployments}; do info "rollout status: deployment/${d}" retry 3 5 kctx -n "${NAMESPACE}" rollout status "deployment/${d}" --timeout="${TIMEOUT}" done } main() { parse_args "$@" require_cmd kubectl require_cmd kustomize TMPDIR="$(mktemp -d)" trap 'rm -rf "${TMPDIR}"' EXIT INT TERM local rendered="${TMPDIR}/rendered.yaml" render "${rendered}" validate "${rendered}" local diff_rc=0 show_diff || diff_rc=$? if (( DRY_RUN == 1 )); then info "dry-run mode: skipping apply" exit 0 fi if (( diff_rc == 0 )); then info "no changes, nothing to apply" exit 0 fi if (( YES != 1 )) && [[ "${CONFIRM:-no}" != "yes" ]]; then confirm "apply changes to context=${CONTEXT} overlay=${OVERLAY}?" \ || fatal "aborted by user" fi apply_overlay watch_rollout info "done" } main "$@" ``` **왜 좋은가:** - strict mode + trap + usage + main "$@" + log 전부 포함. - `--yes` / `CONFIRM=yes` 이중 gate. - `--dry-run=server` validation 이 apply 전 필수. - `kubectl diff` 의 exit code (0/1/>1) 정확히 분기. - retry 함수로 rollout status 불안정성 흡수. - secret 을 argv / 로그에 쓰지 않음. - jsonpath 로 deployment 목록 파싱, regex 없음. --- ## 좋은 예시 3: `scripts/bin/backup-k3s` (etcd snapshot backup, destructive-aware) ```bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" usage() { cat <<'EOF' >&2 Usage: backup-k3s [OPTIONS] --node HOST server node to snapshot on (required) --s3-endpoint URL S3 endpoint for offsite copy (optional) --retention N days to keep local snapshots (default: 7) -h, --help show this help Environment: SSH_USER ssh user (default: current user) S3_ACCESS_KEY required if --s3-endpoint is set S3_SECRET_KEY required if --s3-endpoint is set EOF } main() { local NODE="" S3_ENDPOINT="" RETENTION=7 while [[ $# -gt 0 ]]; do case "$1" in --node) NODE="$2"; shift 2 ;; --s3-endpoint) S3_ENDPOINT="$2"; shift 2 ;; --retention) RETENTION="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) usage; fatal "unknown arg: $1" ;; esac done [[ -n "${NODE}" ]] || { usage; fatal "--node required"; } require_cmd ssh if [[ -n "${S3_ENDPOINT}" ]]; then require_env S3_ACCESS_KEY require_env S3_SECRET_KEY fi local ts ts="$(date -u +'%Y%m%dT%H%M%SZ')" local snap="k3s-snapshot-${ts}.db" info "creating snapshot on node=${NODE}" ssh "${SSH_USER:-$USER}@${NODE}" \ "sudo k3s etcd-snapshot save --name ${snap}" info "pruning snapshots older than ${RETENTION} days on ${NODE}" ssh "${SSH_USER:-$USER}@${NODE}" \ "sudo find /var/lib/rancher/k3s/server/db/snapshots -name 'k3s-snapshot-*.db' -mtime +${RETENTION} -print -delete" if [[ -n "${S3_ENDPOINT}" ]]; then info "uploading ${snap} to ${S3_ENDPOINT} (credentials masked)" # secret 은 env 로 mc 에 전달, argv 노출 금지 ssh "${SSH_USER:-$USER}@${NODE}" \ "S3_ACCESS_KEY='${S3_ACCESS_KEY}' S3_SECRET_KEY='${S3_SECRET_KEY}' \ mc alias set backup ${S3_ENDPOINT} \"\${S3_ACCESS_KEY}\" \"\${S3_SECRET_KEY}\" 2>&1 | mask-secrets || true && \ mc cp /var/lib/rancher/k3s/server/db/snapshots/${snap} backup/k3s-snapshots/${snap}" fi info "backup complete: ${snap}" } main "$@" ``` **왜 좋은가:** - backup 은 destructive 가 아니므로 `--yes` 는 없지만, prune 은 retention 일수로 guard. - secret 은 argv 로 전달 X, env 로 ssh 내부에서만. - ISO 8601 UTC timestamp 로 이름 충돌 방지. - require_env 로 credential 선검증. --- ## 좋은 예시 4: destructive 스크립트 예시 (`scripts/bin/delete-namespace`) ```bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" usage() { cat <<'EOF' >&2 Usage: delete-namespace --context CTX --namespace NS [--yes] DANGER: this deletes the namespace and all its resources (including PVCs if reclaimPolicy=Delete). Requires --yes or CONFIRM=yes. EOF } main() { local CONTEXT="" NS="" YES=0 while [[ $# -gt 0 ]]; do case "$1" in --context) CONTEXT="$2"; shift 2 ;; --namespace) NS="$2"; shift 2 ;; --yes) YES=1; shift ;; -h|--help) usage; exit 0 ;; *) usage; fatal "unknown arg: $1" ;; esac done [[ -n "${CONTEXT}" ]] || { usage; fatal "--context required"; } [[ -n "${NS}" ]] || { usage; fatal "--namespace required"; } require_cmd kubectl if (( YES != 1 )) && [[ "${CONFIRM:-no}" != "yes" ]]; then usage fatal "destructive op requires --yes or CONFIRM=yes" fi warn "will DELETE namespace=${NS} in context=${CONTEXT}" local pvc_count pvc_count="$(kubectl --context="${CONTEXT}" -n "${NS}" get pvc -o json | jq '.items | length')" warn "PVC count in namespace: ${pvc_count}" kubectl --context="${CONTEXT}" delete namespace "${NS}" --wait=true info "deleted namespace=${NS}" } main "$@" ``` **왜 좋은가:** - destructive op 는 `--yes` / `CONFIRM=yes` 이중 gate. - 삭제 전 PVC 수를 jq 로 보여줌 (사용자 자각). - `--wait=true` 로 실제 삭제 완료 확인. - JSON 파싱은 jq, regex 없음. --- ## 좋은 예시 5: local 선언과 command substitution 분리 ```bash get_current_context() { local ctx ctx="$(kubectl config current-context)" # 분리 printf '%s\n' "${ctx}" } ``` **왜 좋은가:** - ShellCheck SC2155: `local ctx="$(...)"` 는 `local` 의 exit status 가 cmd substitution 을 가리므로 에러가 숨는다. - 분리해야 `$?` 가 실제 kubectl 결과 반영. --- ## 좋은 예시 6: JSON 파싱 ```bash # jsonpath get_image() { local ns="$1" deploy="$2" kubectl -n "${ns}" get deploy "${deploy}" \ -o jsonpath='{.spec.template.spec.containers[0].image}' } # jq get_all_images() { local ns="$1" kubectl -n "${ns}" get pods -o json \ | jq -r '.items[].spec.containers[].image' \ | sort -u } ``` **왜 좋은가:** - jsonpath / jq 는 구조적 파싱 → field 순서나 formatting 변화에 내성. --- ## 좋은 예시 7: secret masking 적용 예 ```bash deploy_with_debug() { local overlay="$1" if [[ "${DEBUG:-0}" -eq 1 ]]; then set -x fi kubectl apply -k "${overlay}" 2>&1 | mask_secrets if [[ "${DEBUG:-0}" -eq 1 ]]; then set +x fi } ``` **왜 좋은가:** - debug 시에도 stdout/stderr 에 secret 이 새지 않음. - mask_secrets 가 common lib 에서 재사용. --- ## 나쁜 예시 1: strict mode 없음 ```bash #!/bin/bash # strict mode 없음 TMP=/tmp/foo rm -rf $TMP mkdir $TMP some_command # 실패해도 계속 진행 ``` **문제:** - 실패가 조용히 통과 (`set -e` 없음). - unset variable 에서 빈 경로로 rm → 재앙 가능. - unquoted `$TMP` 공백 split. **Fix:** `set -euo pipefail` + `IFS=$'\n\t'` + trap + quote. --- ## 나쁜 예시 2: heredoc YAML 생성기 ```bash deploy_auth() { cat < /tmp/auth.yaml apiVersion: apps/v1 kind: Deployment metadata: name: auth-server spec: replicas: ${REPLICAS} template: spec: containers: - name: auth image: auth:${VERSION} EOF kubectl apply -f /tmp/auth.yaml } ``` **문제:** - 선언형 원본이 스크립트 안에 숨음. - Git diff 로 환경별 차이 추적 불가. - 리뷰 / audit / kustomize 기능 모두 상실. **Fix:** Kustomize overlay → `kubectl apply -k`. --- ## 나쁜 예시 3: regex 로 kubectl 출력 파싱 ```bash kubectl get pods | grep Running | awk '{print $1}' ``` **문제:** - column 순서나 추가 field 변화에 깨짐. - `Running` 이 pod 이름에 포함되면 오인식. **Fix:** ```bash kubectl get pods --field-selector=status.phase=Running -o jsonpath='{.items[*].metadata.name}' ``` --- ## 나쁜 예시 4: secret 을 argv 로 전달 ```bash mc alias set backup https://s3.example.com "${ACCESS}" "${SECRET}" # ps aux 에 노출, history 에 기록 ``` **문제:** - `ps` 나 audit log 에서 credential 유출. - bash history (`HISTFILE`) 에 기록 가능. **Fix:** ```bash mc alias set backup https://s3.example.com \ "$(echo "${ACCESS}")" "$(cat /run/secrets/s3-secret)" # 또는 환경변수로 mc 가 직접 읽도록 MC_HOST_backup="https://${ACCESS}:${SECRET}@s3.example.com" mc cp ... ``` --- ## 나쁜 예시 5: confirmation 없는 destructive ```bash #!/usr/bin/env bash kubectl delete ns prod ``` **문제:** - 의도 / 권한 / audit 전혀 없음. - 사고 직결. **Fix:** 좋은 예시 4 참조 (`--yes` / `CONFIRM=yes` gate + 사전 정보 표시). --- ## 나쁜 예시 6: trap 없이 임시파일 ```bash TMP="$(mktemp)" do_something > "${TMP}" # 실패 시 /tmp 에 쓰레기 남음 rm "${TMP}" ``` **문제:** - 스크립트 실패 / Ctrl-C 시 임시 파일 누적. - secret 이 들어있으면 유출. **Fix:** ```bash TMP="$(mktemp)" trap 'rm -f "${TMP}"' EXIT INT TERM do_something > "${TMP}" ``` --- ## 나쁜 예시 7: `local` 과 command substitution 한 줄 ```bash bad() { local ctx="$(kubectl config current-context)" # $? 가려짐 } ``` **문제:** - ShellCheck SC2155. `local` 의 exit status 가 cmd substitution 을 덮어 에러 감지 실패. **Fix:** ```bash good() { local ctx ctx="$(kubectl config current-context)" } ```