Git 서브모듈 + GitHub Actions로 블로그 자동 배포 파이프라인 구성하기
Git 서브모듈 + GitHub Actions로 블로그 자동 배포 파이프라인 구성하기
배경
이 블로그는 두 개의 레포로 구성되어 있습니다.
| 레포 | 역할 |
|---|---|
blog-content |
콘텐츠 (마크다운 글) |
blog-site |
블로그 빌드 + 배포 (11ty 등) |
blog-content는 blog-site의 Git 서브모듈로 연결되어 있습니다. 동시에 Obsidian에서 볼트로 직접 열어 글을 작성하는 용도로도 쓰입니다.
문제는 글을 쓰고 blog-content에 푸시해도 블로그가 자동으로 배포되지 않는다는 점이었습니다. 서브모듈은 부모 레포에서 명시적으로 커밋해야 반영되기 때문입니다.
목표
Obsidian도 잘 쓰고싶은데, blog-site 쪽에 매번 문서내용이 추가되는 것도 싫었고, 기왕 이전도 잘 했으니 Obsidian화 하면 더 낫겠단 생각이 들었습니다. 정리해보니 아래 과정을 자동화하는 게 목표였어요.
- Obsidian으로 글을 쓰고 blog-content에 푸시
- blog-site 서브모듈 자동 업데이트
- 트리거를 받으면 블로그에서 재배포
구조 개요
구현을 위해 깃헙 액션 트리거를 쓰기로 했고, 이를 구성해보면 아래 플로우였습니다.
blog-content (push to main)
│
│ repository_dispatch: notes-updated
▼
blog-site
│ 1. git submodule update --remote
│ 2. commit & push
▼
Deploy workflow (build → S3 등)
핵심은 repository_dispatch 이벤트입니다. GitHub Actions에서 다른 레포의 워크플로우를 트리거할 수 있는 공식 메커니즘이죠.
1. 콘텐츠 레포: dispatch 이벤트 발송
blog-content에 아래 워크플로우를 추가합니다.
# .github/workflows/notify-base-blog.yml
name: Notify base blog
on:
push:
branches: [main]
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Trigger submodule sync
run: |
gh api repos/<owner>/blog-site/dispatches \
-f event_type=notes-updated
env:
GH_TOKEN: ${{ secrets.BASE_BLOG_PAT }}
BASE_BLOG_PAT는 대상 레포(blog-site)에 contents: write 권한이 있는 Fine-grained Personal Access Token입니다. 콘텐츠 레포의 GitHub Secrets에 등록합니다.
왜 Fine-grained PAT인가?
- Classic PAT는 사용자의 모든 레포에 접근할 수 있어 과도한 권한
- Fine-grained PAT는 특정 레포 + 특정 권한만 부여 가능
repository_dispatch트리거에는 대상 레포의contents: write만 있으면 충분
이런 장점이 있다보니 생각이상으로 깔끔하게 잘 떨어지고 아래 스크린샷처럼 잘 나와서 너무 좋았습니다.

2. 블로그 레포: 서브모듈 동기화
blog-site에서 repository_dispatch 이벤트를 받아 서브모듈을 업데이트합니다.
# .github/workflows/sync-notes.yml
name: Sync notes submodule
on:
repository_dispatch:
types: [notes-updated]
workflow_dispatch:
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Checkout and update submodule
env:
GIT_SSH_COMMAND: >-
ssh -i ~/.ssh/deploy_key
-o StrictHostKeyChecking=yes
-o UserKnownHostsFile=~/.ssh/known_hosts
-o IdentitiesOnly=yes
run: |
set -euo pipefail
umask 077
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "${{ secrets.NOTES_DEPLOY_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
printf '%s\n' \
"github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" \
>> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
git submodule sync --recursive
git submodule update --init --recursive
git submodule update --remote _vault
rm -f ~/.ssh/deploy_key
- name: Commit and push if changed
run: |
git diff --quiet && exit 0
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add _vault
git commit -m "chore(notes): update submodule ref"
git push
보안 포인트
- Deploy Key: 서브모듈 레포에 대한 읽기 전용 SSH 키. PAT 대신 사용하여 권한 범위를 최소화
- Host key pinning:
StrictHostKeyChecking=yes와 GitHub의 공식 ed25519 핑거프린트를 사용하여 MITM 방지- 워크플로우에 하드코딩된
ssh-ed25519 AAAAC3N...값은 GitHub가 공식 문서에서 공개한 SSH host key 핑거프린트입니다. - CI 환경에서는
ssh-keyscan으로 런타임에 가져오는 것보다 알려진 값을 고정하는 쪽이 안전(ssh-keyscan자체가 MITM에 취약하기 때문입니다)
- 키 노출 최소화: 사용 직후
rm -f로 삭제 - umask 077: 키 파일이 생성될 때 다른 사용자가 읽을 수 없도록 설정
왜 Deploy Key인가?
서브모듈의 URL이 SSH(git@github.com:...)인 경우, GitHub Actions의 기본 GITHUB_TOKEN으로는 다른 레포의 서브모듈을 체크아웃할 수 없습니다. Deploy Key는 특정 레포에만 접근 가능하므로 PAT보다 안전합니다.
3. 전체 흐름
그럼 이걸로 자동화한 구성을 살펴보시죠. 이제는 이게 됩니다.
- Obsidian에서 글 작성 →
blog-content에 push Notify base blog워크플로우가 실행 →repository_dispatch이벤트 발송blog-site의Sync notes submodule워크플로우가 트리거- 서브모듈 최신 커밋으로 업데이트 → 자동 커밋 & 푸시
- 푸시로 인해 기존 배포 워크플로우(Build → Deploy)가 트리거
- 블로그 배포 완료
삽질 기록
한번에 되면 그게 이상하죠. 뭐가 이상했던게 있었나, 어떻게 해결했나 공유합니다.
repository_dispatch가 404를 반환한다
PAT의 권한 문제입니다. Fine-grained PAT를 만들 때:
- Repository access에서 대상 레포를 명시적으로 선택해야 합니다. 레포를 잘못 선택하진 않았는지 확인하세요!
- Permissions > Contents를
Read and write로 설정해야 합니다.Read-only로는 dispatch 이벤트를 보낼 수 없어요
workflow_dispatch도 추가하면 좋다
sync-notes.yml에 workflow_dispatch 트리거를 함께 넣으면 GitHub UI나 gh workflow run으로 수동 실행할 수 있습니다. 디버깅할 때 유용합니다.