init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
# 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 <<EOF > /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)"
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user