- GitHub Actions와 FTP를 이용한 공유기 기반 정적 문서 배포는 GitHub 저장소에서 생성한 정적 문서를 가정용 공유기의 USB 저장소로 자동 전송하고 외부에 제공하는 배포 구조
- GitHub Actions는 YAML workflow에 정의한 문서 생성·검증·배포 작업을 실행하는 자동화 엔진
- FTP는
LIST,STOR명령으로 원격 디렉터리의 파일을 조회하고 업로드하는 파일 전송 프로토콜 - DDNS는 변경될 수 있는 공인 IP를 고정된 호스트 이름으로 연결하는 주소 관리 서비스
- URL 서비스는 USB 저장소의 HTML, CSS, JavaScript, 이미지 파일을 HTTP로 제공하는 정적 웹 서버 기능
- WireGuard는 FTP 연결을 사설 암호화 터널 안으로 넣을 때 사용할 수 있는 선택적 VPN 기술
이 구성이 필요한 이유
비공개 GitHub 저장소에서 무료 GitHub Pages 사용이 제한되거나, 문서를 직접 운영하는 서버를 별도로 두고 싶지 않을 수 있습니다. 이때 공유기의 USB 저장소를 정적 파일 저장소로 사용하면 다음 구성을 만들 수 있습니다.
flowchart LR A["GitHub Repository"] --> B["GitHub Actions"] B --> C["FTP"] C --> D["공유기 USB 저장소"] D --> E["URL 서비스"] E --> F["외부 브라우저"]
이 방식은 별도의 PC를 항상 켜둘 필요가 없다는 장점이 있습니다. 반면 일반 FTP를 인터넷에 공개하는 구조이므로 전송 암호화와 계정 격리를 별도로 검토해야 합니다.
전체 동작 흐름
AS-IS: 로컬에서 수동 업로드
sequenceDiagram autonumber participant PC as 로컬 컴퓨터 participant FTP as 공유기 FTP 서비스 participant USB as USB 저장소 participant Browser as 외부 브라우저 participant HTTP as 공유기 URL 서비스 PC->>FTP: FTP 계정 로그인 PC->>FTP: STOR 파일 업로드 FTP->>USB: 정적 파일 저장 Browser->>HTTP: HTTP 요청 HTTP->>USB: 파일 읽기 HTTP-->>Browser: HTML 문서 반환
TO-BE: GitHub Actions 자동 배포
sequenceDiagram autonumber participant Dev as 개발자 participant Repo as GitHub Repository participant Action as GitHub Actions participant Router as 공유기 participant USB as USB 저장소 participant User as 외부 사용자 Dev->>Repo: 문서 소스 변경사항 push Repo->>Action: workflow 실행 Action->>Action: 소스 checkout Action->>Action: 정적 문서 생성 Action->>Action: 문서 검증 Action->>Router: FTP 로그인 Action->>Router: 생성 파일 업로드 Router->>USB: 프로젝트 디렉터리에 저장 User->>Router: DDNS 주소로 HTTP 접속 Router->>USB: 정적 문서 읽기 Router-->>User: 문서 페이지 반환
GitHub Actions workflow는 .github/workflows 디렉터리의 YAML 파일로 정의하며, push, pull_request, workflow_dispatch 등을 실행 조건으로 사용할 수 있습니다. GitHub Actions workflow 문법
공유기 쪽 구성
1. USB 저장소 연결
공유기에 USB 저장장치를 연결하고, 공유기 관리 화면에서 장치가 인식되는지 확인합니다. 저장소의 파일 시스템은 해당 공유기가 지원하는 형식을 사용해야 합니다. 일반적으로 공유기 호환성과 안정성을 먼저 확인한 뒤 초기화합니다.
2. FTP 서비스 활성화
FTP 서비스에서 다음 항목을 설정합니다.
- FTP 서비스 활성화
- FTP 포트 설정
- Passive 포트 범위 설정
- 전용 사용자 ID와 비밀번호 생성
- 업로드 대상 폴더 지정
- 해당 사용자에게 읽기/쓰기 권한 부여
ipTIME의 USB/NAS 기능은 USB 저장장치를 FTP와 웹 서버로 제공할 수 있으며, 사용자별 폴더와 읽기/쓰기 속성을 설정할 수 있습니다. ipTIME USB/NAS 및 FTP 서버 설정
3. DDNS와 URL 서비스 설정
DDNS에 예시 호스트 이름을 등록합니다.
docs.example.ipdisk.co.kr블로그에서는 실제 주소 대신 위와 같은 예시 주소를 사용하고, 운영 환경에서는 자신의 DDNS 주소로 교체합니다.
URL 서비스는 USB 저장소의 정적 파일을 HTTP로 제공하는 기능입니다. FTP 포트와 URL 서비스 포트는 서로 다른 서비스이므로 각각 확인해야 합니다.
업로드: FTP → :21 또는 설정한 FTP 포트
조회: HTTP → :8000, :9000 등 URL 서비스 포트DDNS가 정상 등록되어도 공인 IP가 아니거나 상위 공유기·ISP가 포트를 차단하면 외부 접속이 되지 않을 수 있습니다. ipTIME DDNS 외부 접속 문제 해결
범용 저장소 디렉터리 구조
프로젝트 이름을 고정하지 않고, 각 프로젝트를 별도 디렉터리로 배치합니다.
/HDD1/docs/
├── index.html
├── project-a/
│ ├── index.html
│ └── generated/
│ ├── redoc.html
│ ├── swagger.html
│ └── openapi-inline.js
└── project-b/
├── index.html
└── generated/
├── redoc.html
├── swagger.html
└── openapi-inline.js여기서 project-a, project-b는 실제 프로젝트의 slug로 교체합니다. 프로젝트별 배포 workflow는 자신에게 할당된 디렉터리만 수정하도록 구성하는 것이 안전합니다.
루트 index.html은 프로젝트 목록 역할을 합니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API 문서 목록</title>
</head>
<body>
<h1>API 문서</h1>
<ul>
<li><a href="./project-a/index.html">Project A 문서</a></li>
<li><a href="./project-b/index.html">Project B 문서</a></li>
</ul>
</body>
</html>새 프로젝트를 추가할 때 루트 목록과 해당 프로젝트의 FTP 대상 디렉터리를 함께 추가합니다. 공유기만으로 하위 폴더를 자동 탐색해 목록을 생성하는 기능은 URL 서비스 구현에 따라 제공되지 않을 수 있으므로, 정적 루트 목록을 명시적으로 관리하는 방식이 예측 가능하고 안전합니다.
GitHub Actions workflow 예시
다음 예시는 public/ 아래에 생성된 정적 문서를 공유기 저장소의 프로젝트 디렉터리로 업로드하는 일반형 workflow입니다.
name: Deploy static docs
on:
push:
branches: [main]
paths:
- 'docs/**'
- 'public/**'
- '.github/workflows/deploy-docs.yml'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate static documents
run: ./scripts/generate-docs.sh
- name: Verify documents
run: ./scripts/verify-docs.sh
- name: Upload to router
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.DOCS_FTP_HOST }}
port: ${{ secrets.DOCS_FTP_PORT }}
protocol: ftp
username: ${{ secrets.DOCS_FTP_USER }}
password: ${{ secrets.DOCS_FTP_PASS }}
local-dir: ./public/
server-dir: /HDD1/docs/project-a/프로젝트마다 server-dir만 자신의 경로로 바꾸면 같은 패턴을 재사용할 수 있습니다.
GitHub Repository Secrets에는 다음 값을 저장합니다.
DOCS_FTP_HOST
DOCS_FTP_PORT
DOCS_FTP_USER
DOCS_FTP_PASSSecrets는 workflow에서 사용할 수 있지만 로그에 직접 출력하면 안 됩니다. GitHub Actions Secrets 공식 문서
FTP 경로와 권한 이해
FTP 로그인 직후 표시되는 /는 로컬 컴퓨터의 루트가 아니라 FTP 서버가 제공하는 가상 루트입니다.
FTP 가상 루트 /
└── HDD1/
└── docs/
└── project-a/따라서 공유기의 FTP 계정이 전체 USB 저장소를 볼 수 있다면 다음과 같은 경로를 사용합니다.
curl --user "$FTP_USER" \
--upload-file ./index.html \
ftp://docs.example.ipdisk.co.kr:21/HDD1/docs/project-a/index.html반대로 계정의 홈 디렉터리가 이미 /HDD1/docs/project-a로 제한되어 있다면 경로를 다음처럼 줄여야 할 수 있습니다.
curl --user "$FTP_USER" \
--upload-file ./index.html \
ftp://docs.example.ipdisk.co.kr:21/index.html다음 오류는 각각 다른 문제를 의미합니다.
| 응답 | 의미 |
|---|---|
530 Access denied | 로그인 실패 또는 계정 인증 거부 |
550 Operation not permitted | 경로는 존재하지만 쓰기 권한 부족 |
550 No such file or directory | 원격 경로가 존재하지 않음 |
229 Entering Extended Passive Mode | Passive 데이터 연결 준비 완료 |
LIST가 되더라도 STOR가 실패할 수 있습니다. 디렉터리 조회 권한과 파일 생성 권한은 별도로 설정될 수 있기 때문입니다.
외부 접속 구조
flowchart LR A["외부 브라우저"] --> B["docs.example.ipdisk.co.kr"] B --> C["현재 공인 IP"] C --> D["공유기 URL 서비스"] D --> E["USB 저장소"] E --> F["/HDD1/docs/index.html"]
외부 사용자는 FTP가 아니라 URL 서비스의 HTTP 주소로 문서를 조회합니다.
http://docs.example.ipdisk.co.kr:<HTTP_PORT>/index.html
http://docs.example.ipdisk.co.kr:<HTTP_PORT>/project-a/index.htmlDDNS는 도메인과 공인 IP를 연결하는 역할만 하며, 외부 서비스가 실제로 동작하려면 공유기 포트와 서비스 상태도 정상이어야 합니다.
FTP와 HTTP의 역할 차이
| 목적 | 프로토콜 | 담당 기능 |
|---|---|---|
| GitHub Actions 파일 업로드 | FTP | 원격 저장소에 파일 생성·갱신 |
| 외부 문서 조회 | HTTP | 정적 파일 브라우저 제공 |
| 주소 연결 | DDNS | 호스트 이름과 공인 IP 연결 |
| 실제 파일 보관 | USB 저장소 | HTML·CSS·JS·이미지 저장 |
보안상 주의점
현재 구성이 일반 FTP라면 FTP 계정과 파일이 암호화되지 않은 상태로 전송될 수 있습니다.
- FTP ID와 비밀번호가 평문으로 노출될 가능성
- 업로드 파일 내용이 전송 구간에서 노출될 가능성
- FTP 포트와 Passive 포트 범위의 인터넷 노출
- 자동 스캐너와 무차별 대입 공격 가능성
- 익명 FTP 또는 과도한 쓰기 권한으로 인한 파일 변조 위험
비표준 FTP 포트를 사용하면 단순 스캔을 줄일 수 있지만 암호화가 추가되는 것은 아닙니다. FTP 보안 위험과 Passive Mode 관련 고려사항은 RFC 2577 FTP Security Considerations에 정리되어 있습니다.
최소한 다음 설정을 권장합니다.
- 공유기 관리자 페이지의 외부 접속 비활성화
- 문서 배포 전용 FTP 계정 사용
- 계정의 쓰기 범위를 프로젝트 폴더로 제한
- 강력하고 별도인 FTP 비밀번호 사용
- Passive 포트 범위 최소화
- HTML·CSS·JS·이미지만 업로드
- 공유기 펌웨어 최신화
- FTP 접속 로그 정기 확인
WireGuard를 추가하는 경우
현재 workflow는 WireGuard를 사용하지 않습니다.
flowchart LR A["현재"] --> B["GitHub Actions"] B --> C["일반 FTP"] C --> D["공유기 공인 주소"] D --> E["USB 저장소"]
WireGuard를 추가하면 FTP 연결을 VPN 터널 안에 넣을 수 있습니다.
flowchart LR A["GitHub Actions Runner"] --> B["WireGuard 암호화 터널"] B --> C["공유기 또는 내부 VPN 서버"] C --> D["내부망 FTP"] D --> E["USB 저장소"]
이때 필요한 추가 설정은 다음과 같습니다.
- WireGuard 서버와 Peer 키 생성
- GitHub Actions에 개인 키를 Secret으로 저장
- workflow에서 WireGuard 클라이언트 실행
- VPN 내부 주소로 FTP 접속
- 공유기에서 해당 Peer와 내부망 접근 허용
- VPN 주소 대역에 대한 라우팅 설정
보안성은 높아지지만 GitHub-hosted runner에서 VPN을 연결하고 라우팅해야 하므로 현재의 공개 FTP 방식보다 운영 복잡도가 커집니다.
프로젝트 추가 절차
-
USB 저장소에 새 프로젝트 디렉터리 생성
/HDD1/docs/new-project/ -
새 프로젝트 저장소에 동일한 형태의 GitHub Actions workflow 추가
-
server-dir를 새 프로젝트 디렉터리로 지정 -
프로젝트별 FTP 계정 또는 최소한 프로젝트별 쓰기 폴더 사용
-
루트
index.html에 새 문서 링크 추가 -
GitHub Actions 실행 후 외부 URL에서 확인
검증 순서
flowchart TD A["문서 소스 변경"] --> B["GitHub Actions 실행"] B --> C["문서 생성"] C --> D["문서 검증"] D --> E["FTP 업로드"] E --> F["공유기 저장소 확인"] F --> G["외부 HTTP 접속"] G --> H["루트 목록과 프로젝트 문서 확인"]
로컬에서 문서 생성과 검증을 먼저 수행합니다.
./scripts/generate-docs.sh
./scripts/verify-docs.sh
git diff --check그 다음 다음 항목을 확인합니다.
- GitHub Actions workflow 성공 여부
- 대상 프로젝트 디렉터리에
index.html존재 여부 - 외부 네트워크에서 DDNS 주소 접속 여부
- 루트 문서 목록 링크 동작 여부
- 프로젝트별 문서 렌더러 동작 여부
- 다른 프로젝트 폴더가 삭제되거나 덮어써지지 않았는지 여부