init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 deletions
+602
View File
@@ -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)"
}
```