name: 도메인 생성 description: nginx-proxy-manager에 프록시 호스트를 등록하고 Let's Encrypt SSL을 적용 (멱등성 보장) inputs: npm-host: required: true description: nginx-proxy-manager 주소 (npm-api.conf의 NGINX_IP) npm-port: required: false default: "81" description: nginx-proxy-manager 관리 API 포트 (npm-api.conf의 NGINX_PORT) npm-user: required: true description: NPM 로그인 이메일 (npm-api.conf의 API_USER) npm-password: required: true description: NPM 로그인 비밀번호 (npm-api.conf의 API_PASS) domain: required: true description: "생성할 도메인 (예: app.example.com)" forward-host: required: true description: 프록시가 바라볼 대상 호스트/IP forward-port: required: true description: 프록시가 바라볼 대상 포트 websockets: required: false default: "true" description: 웹소켓 지원 활성화 outputs: host-id: description: 생성되었거나 이미 존재하는 프록시 호스트 ID value: ${{ steps.create.outputs.host-id }} created: description: 이번 실행에서 새로 생성했으면 true, 이미 있었으면 false value: ${{ steps.create.outputs.created }} runs: using: composite steps: - name: jq 설치 확인 shell: bash run: | set -eu # jq가 이미 있으면 건너뜁니다. if command -v jq >/dev/null 2>&1; then exit 0 fi # 패키지 매니저에 따라 jq를 설치합니다. if command -v apt-get >/dev/null 2>&1; then apt-get update && apt-get install -y jq elif command -v apk >/dev/null 2>&1; then apk add --no-cache jq elif command -v yum >/dev/null 2>&1; then yum install -y jq else echo "jq 설치 실패: 지원하지 않는 러너 이미지" exit 1 fi - name: 프록시 호스트 생성 id: create shell: bash env: NPM_HOST: ${{ inputs.npm-host }} NPM_PORT: ${{ inputs.npm-port }} NPM_USER: ${{ inputs.npm-user }} NPM_PASS: ${{ inputs.npm-password }} DOMAIN: ${{ inputs.domain }} FORWARD_HOST: ${{ inputs.forward-host }} FORWARD_PORT: ${{ inputs.forward-port }} WEBSOCKETS: ${{ inputs.websockets }} run: | set -eu # 개발서버 전용 고정값 BASE="http://${NPM_HOST}:${NPM_PORT}/api" # NPM_SCHEME=http 고정 FORWARD_SCHEME="http" # 대상 프로토콜 http 고정 # NPM API 호출 헬퍼: 응답 본문과 상태코드를 분리해, 2xx가 아니면 # NPM이 보낸 에러 본문을 그대로 출력하고 실패시킵니다. # 사용법: api [JSON_DATA] (성공 시 응답 본문을 stdout으로) api() { local method="$1" path="$2" data="${3:-}" resp code body if [ -n "$data" ]; then resp=$(curl -sS -w $'\n%{http_code}' -X "$method" "${BASE}${path}" \ -H "$AUTH" -H "Content-Type: application/json" -d "$data") else resp=$(curl -sS -w $'\n%{http_code}' -X "$method" "${BASE}${path}" -H "$AUTH") fi code="${resp##*$'\n'}" body="${resp%$'\n'*}" if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then echo "API 오류 [$method $path] HTTP $code: $body" >&2 return 1 fi printf '%s' "$body" } # --------------------------------------------------------------- # 1. 토큰 발급 (로그인) # --------------------------------------------------------------- TOKEN_RESP=$(curl -sS -w $'\n%{http_code}' -X POST "${BASE}/tokens" \ -H "Content-Type: application/json" \ -d "$(jq -nc --arg i "$NPM_USER" --arg s "$NPM_PASS" '{identity:$i, secret:$s}')") TOKEN_CODE="${TOKEN_RESP##*$'\n'}" TOKEN_BODY="${TOKEN_RESP%$'\n'*}" if [ "$TOKEN_CODE" != "200" ]; then echo "NPM 토큰 발급 실패 (HTTP $TOKEN_CODE): $TOKEN_BODY" exit 1 fi TOKEN=$(printf '%s' "$TOKEN_BODY" | jq -r '.token') AUTH="Authorization: Bearer ${TOKEN}" WS=$([ "$WEBSOCKETS" = "true" ] && echo true || echo false) # --------------------------------------------------------------- # 2. 기존 프록시 호스트 확인 → 있으면 즉시 종료 (덮어쓰지 않음) # 이미 등록된 도메인은 기존 설정을 보존하기 위해 아무것도 건드리지 않습니다. # --------------------------------------------------------------- HOST_ID=$(api GET "/nginx/proxy-hosts" \ | jq -r --arg d "$DOMAIN" \ 'map(select(.domain_names | index($d))) | .[0].id // empty') if [ -n "$HOST_ID" ]; then echo "이미 존재하는 도메인입니다. 덮어쓰지 않고 종료합니다: ${DOMAIN} (host id=${HOST_ID})" echo "host-id=${HOST_ID}" >> "$GITHUB_OUTPUT" echo "created=false" >> "$GITHUB_OUTPUT" exit 0 fi # --------------------------------------------------------------- # 3. HTTP 전용 프록시 호스트 먼저 생성 (SSL 없이) # 도메인이 80포트로 응답해야 Let's Encrypt HTTP-01 검증이 통과합니다. # --------------------------------------------------------------- echo "HTTP 프록시 호스트 생성 중: ${DOMAIN} -> ${FORWARD_SCHEME}://${FORWARD_HOST}:${FORWARD_PORT}" HTTP_PAYLOAD=$(jq -nc \ --arg d "$DOMAIN" \ --arg fh "$FORWARD_HOST" \ --arg fp "$FORWARD_PORT" \ --arg fs "$FORWARD_SCHEME" \ --argjson ws "$WS" \ '{domain_names:[$d], forward_host:$fh, forward_port:($fp|tonumber), forward_scheme:$fs, certificate_id:0, ssl_forced:false, http2_support:false, hsts_enabled:false, hsts_subdomains:false, caching_enabled:false, allow_websocket_upgrade:$ws, block_exploits:true, access_list_id:0, advanced_config:"", locations:[], meta:{}}') HOST_ID=$(api POST "/nginx/proxy-hosts" "$HTTP_PAYLOAD" | jq -r '.id') if [ -z "$HOST_ID" ] || [ "$HOST_ID" = "null" ]; then echo "프록시 호스트 생성 실패: ${DOMAIN}" exit 1 fi # --------------------------------------------------------------- # 4. Let's Encrypt 인증서 발급 (HTTP 호스트가 떠 있는 상태에서 검증) # 같은 도메인 인증서가 이미 있으면 재사용합니다. # --------------------------------------------------------------- CERT_ID=$(api GET "/nginx/certificates" \ | jq -r --arg d "$DOMAIN" \ 'map(select(.provider=="letsencrypt" and (.domain_names | index($d)))) | .[0].id // empty') if [ -z "$CERT_ID" ]; then echo "Let's Encrypt 인증서 발급 중: ${DOMAIN}" # 이메일/약관동의는 NPM 설정(Settings > Default Let's Encrypt)에서 가져오므로 # 요청 meta에는 dns_challenge만 보냅니다. (스키마가 그 외 키를 거부함) CERT_ID=$(api POST "/nginx/certificates" \ "$(jq -nc --arg d "$DOMAIN" \ '{provider:"letsencrypt", domain_names:[$d], meta:{dns_challenge:false}}')" \ | jq -r '.id') if [ -z "$CERT_ID" ] || [ "$CERT_ID" = "null" ]; then echo "인증서 발급 실패: ${DOMAIN} (도메인 DNS가 NPM을 가리키는지 확인하세요)" exit 1 fi else echo "기존 인증서 재사용: ${DOMAIN} (cert id=${CERT_ID})" fi # --------------------------------------------------------------- # 5. 프록시 호스트에 SSL 적용 (PUT 업데이트: SSL 강제 + HTTP/2) # --------------------------------------------------------------- echo "프록시 호스트에 SSL 적용 중: ${DOMAIN} (cert id=${CERT_ID})" SSL_PAYLOAD=$(jq -nc \ --arg d "$DOMAIN" \ --arg fh "$FORWARD_HOST" \ --arg fp "$FORWARD_PORT" \ --arg fs "$FORWARD_SCHEME" \ --argjson cert "$CERT_ID" \ --argjson ws "$WS" \ '{domain_names:[$d], forward_host:$fh, forward_port:($fp|tonumber), forward_scheme:$fs, certificate_id:$cert, ssl_forced:true, http2_support:true, hsts_enabled:false, hsts_subdomains:false, caching_enabled:false, allow_websocket_upgrade:$ws, block_exploits:true, access_list_id:0, advanced_config:"", locations:[], meta:{}}') api PUT "/nginx/proxy-hosts/${HOST_ID}" "$SSL_PAYLOAD" > /dev/null echo "도메인 생성/SSL 적용 완료: ${DOMAIN} (host id=${HOST_ID})" echo "host-id=${HOST_ID}" >> "$GITHUB_OUTPUT" echo "created=true" >> "$GITHUB_OUTPUT"