Vercel 비즈니스 모델을 exploit해서 $20로 인원 제한 없이 사용하기
2022 / 10 / 25
Vercel API를 exploit해서 조직의 크기와 상관없이 월 $20만 지불해도 Vercel을 사용할 수 있는 GitHub Actions를 작성해봤습니다.
Preview 배포는 실제 사용자에게 공개하지 않는 배포입니다. 저장소에 Pull Request(이하 PR)을 올리면 Vercel plugin이 PR의 내용으로 Preview 배포를 수행하고 PR에 댓글을 답니다. 댓글에는 Preview 배포 상태를 확인할 수 있는 insepctor 링크와 preview 배포에 접근할 수 있는 링크가 있습니다.
Production 배포는 실제 사용자에게 공개하는 배포입니다. Preview 배포는 사용자가 접속하는 도메인 네임에 전혀 영향을 주지 않습니다. 특정 Preview 배포에 대해서 ‘Promote to Production’을 하면 해당 Preview 배포로 Production 배포를 수행하면서 사용자가 접속하는 도메인 네임의 사이트에 변경사항이 반영됩니다.
Vercel은 무료 plan도 제공하지만 상업적으로 사용하기 위해서는 최소한 Pro plan을 사용해야 합니다.
Pro plan을 사용했을 때 과금체계가 흥미로운데, Vercel계정에 연동한 GitHub 계정 수 만큼 과금이 됩니다. Vercel 프로젝트 설정을 하기 위한 첫 번째 배포 말고는 Vercel에서 직접 배포를 수행할 수 없습니다. 코드 저장소에 Vercel plugin을 연동해서 PR이 올라오거나 branch에 commit을 추가하면 Vercel plugin이 배포를 진행합니다. 그리고 Vercel plugin은 연동한 GitHub 계정에 대해서만 작동을 합니다.
즉 돈을 내야만 배포를 할 수 있습니다.
하지만 GitHub에 연동해서 변경사항을 배포하는 방식 말고 Vercel에 직접 신호를 줘서 배포하는 방법이 있을 것 같은데.. 해서 찾아보니 배포 API가 있었습니다: Create a new deployment | Vercel
이 API를 잘 쓰면 배포할 수 있을 것 같지만, 가장 중요한 gitSource
property에 대한 문서가 비어있습니다. 뭐야?
저같은 사람이 많아서 숨겨놨나봅니다.. 하지만 문서에서 gitSource를 찾아보니 다행히 response에 동일한 property를 찾을 수 있었습니다.
gitSource?:
| {
type: "github"
repoId: string | number
ref?: string | null
sha?: string
prId?: number | null
}
| {
type: "github"
org: string
repo: string
ref?: string | null
sha?: string
prId?: number | null
}
| {
type: "gitlab"
projectId: string | number
ref?: string | null
sha?: string
prId?: number | null
}
| {
type: "bitbucket"
workspaceUuid?: string
repoUuid: string
ref?: string | null
sha?: string
prId?: number | null
}
| {
type: "bitbucket"
owner: string
slug: string
ref?: string | null
sha?: string
prId?: number | null
}
| {
type: "custom"
ref: string
sha: string
gitUrl: string
}
| {
type: "github"
ref: string
sha: string
repoId: number
org?: string
repo?: string
}
| {
type: "gitlab"
ref: string
sha: string
projectId: number
}
| {
type: "bitbucket"
ref: string
sha: string
owner?: string
slug?: string
workspaceUuid: string
repoUuid: string
}
친절한 설명은 없지만 대충 적당히 값을 넣어서 보내면 될 것 같아서 이것저것 시도해보다가 성공했습니다.
curl -X POST "https://api.vercel.com/v13/deployments?teamId=<hidden>" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <hidden>" \
-d '{
"name": "your-project-name", #Vercel project 이름
"gitSource": {
"type": "github",
"org": "your-organization-name", #조직 이름
"repo": "your-repository-name", #저장소 이름
"ref": ""
}
}'
teamId
는 Vercel 팀 계정의 콘솔에서 확인할 수 있습니다 (Settings -> General -> Team ID): https://vercel.com/teams/[your-team]/settings
인증토큰은 Vercel 계정 셋팅에서 생성해서 사용할 수 있습니다: https://vercel.com/account/tokens
이 두 가지 값을 Github Organzation의 actions secret에 추가해놓으면 모든 repository의 actions에서 공용으로 사용할 수 있습니다.
request body를 보면 gitSource의 ref가 비어있습니다. ref는 github의 branch나 tag를 의미합니다. 지금처럼 빈 값으로 넣으면 저장소의 default branch에 대해서 preview 배포를 진행합니다.
curl -X POST "https://api.vercel.com/v13/deployments?teamId=<hidden>" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <hidden>" \
-d '{
"name": "your-project-name",
"gitSource": {
"type": "github",
"org": "your-organization-name",
"repo": "your-repository-name",
"ref": ""
},
"target": "production"
}'
production 배포를 진행하려면 동일한 request에 "target": "production" property만 추가하면 됩니다.
curl -X POST "https://api.vercel.com/v13/deployments?teamId=<hidden>" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <hidden>" \
-d '{
"name": "your-project-name", #Vercel project 이름
"gitSource": {
"type": "github",
"org": "your-organization-name", #조직 이름
"repo": "your-repository-name", #저장소 이름
"ref": "feat/VERHACK-1"
}
}'
curl -X POST "https://api.vercel.com/v13/deployments?teamId=<hidden>" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <hidden>" \
-d '{
"name": "your-project-name",
"gitSource": {
"type": "github",
"org": "your-organization-name",
"repo": "your-repository-name",
"ref": "feat/VERHACK-1"
},
"target": "production"
}'
이제 우리에게 필요한 http request spec은 다 확보했습니다. GitHub Actions를 직접 작성해서 ‘PR이 올라왔을 때’, ‘PR에 커밋이 추가될 때’, ‘특정 branch에 커밋이 추가될 때’ 자동으로 위 API를 호출하면… 조직에 천 명 만 명이 있어도 우리는 월 $20로 Vercel을 사용할 수 있게 됩니다. 물론 동시 빌드 개수의 제한이 있지만, 사람 수대로가 아니라 필요한 동시 배포 개수를 기준으로 과금을 하면 훨씬 비용이 적게 듭니다.
your-repository-name 프로젝트에 시험삼아 Vercel 배포용 GitHub Actions를 작성해봤습니다: https://gist.github.com/myeongjae-kim/2b9cdf957813324ea61d344953fe4fe9
name: "Deploy to Vercel"
on:
pull_request:
types:
- opened
- synchronize
push:
branches:
- beta
- prod
# secrets을 직접 사용하지 않고 env를 통해 사용함으로써 이 action이 어떤 secret값을 필요로 하는지 한 눈에 볼 수 있도록 한다.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 기본 제공 secret https://docs.github.com/en/rest/quickstart#using-github-cli-in-github-actions
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} # 조직 공통
VERCEL_DEPLOYMENT_TOKEN: ${{ secrets.VERCEL_DEPLOYMENT_TOKEN }} # 조직 공통
VERCEL_PROJECT_NAME_BETA: ${{ secrets.VERCEL_PROJECT_NAME_BETA }} # 프로젝트별로 repository에서 지정해줘야 한다.
VERCEL_PROJECT_NAME_PROD: ${{ secrets.VERCEL_PROJECT_NAME_PROD }} # 프로젝트별로 repository에서 지정해줘야 한다.
MESSAGE_FAILURE: Vercel 배포에 실패했습니다. Github Actions 로그를 확인해주세요.
MESSAGE_SUCCESS: Vercel 배포 API를 호출했습니다. Vercel 플러그인의 댓글을 기다려주세요.
MESSAGE_SUCCESS_PROD: Vercel 배포 API를 호출했습니다. Preview 배포를 확인하고 이상이 없다면\nInspector에서 \'Promote to Production\'을 수동으로 눌러 실제 환경에 배포하세요.
jobs:
deploy-preview-beta:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- id: deploy
run: |
curl --fail -X POST "https://api.vercel.com/v13/deployments?teamId=${{ env.VERCEL_TEAM_ID }}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ env.VERCEL_DEPLOYMENT_TOKEN }}" \
-d '{
"name": "${{ env.VERCEL_PROJECT_NAME_BETA }}",
"gitSource": {
"type": "github",
"org": "${{ github.event.repository.owner.login }}",
"repo": "${{ github.event.repository.name }}",
"ref": "${{ github.head_ref }}"
}
}'
- name: "Add PR comment when failed"
uses: actions/github-script@v5
if: failure()
with:
github-token: ${{env.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '❌ \'${{ github.workflow }}\' failed: ${{ env.MESSAGE_FAILURE }}'
})
- name: "Add PR comment when success"
uses: actions/github-script@v5
if: success()
with:
github-token: ${{env.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '✅ \'${{ github.workflow }}\' succeeded: ${{ env.MESSAGE_SUCCESS }}'
})
deploy-production-beta:
if: github.ref == 'refs/heads/beta'
runs-on: ubuntu-latest
steps:
- id: deploy
run: |
curl --fail -X POST "https://api.vercel.com/v13/deployments?teamId=${{ env.VERCEL_TEAM_ID }}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ env.VERCEL_DEPLOYMENT_TOKEN }}" \
-d '{
"name": "${{ env.VERCEL_PROJECT_NAME_BETA }}",
"gitSource": {
"type": "github",
"org": "${{ github.event.repository.owner.login }}",
"repo": "${{ github.event.repository.name }}",
"ref": "beta"
},
"target": "production"
}'
- name: "Add commit comment when failed"
uses: actions/github-script@v5
if: failure()
with:
github-token: ${{env.GITHUB_TOKEN}}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: '${{ github.sha }}',
body: '❌ \'${{ github.workflow }}\' failed: ${{ env.MESSAGE_FAILURE }}'
})
- name: "Add commit comment when success"
uses: actions/github-script@v5
if: success()
with:
github-token: ${{env.GITHUB_TOKEN}}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: '${{ github.sha }}',
body: '✅ \'${{ github.workflow }}\' succeeded: ${{ env.MESSAGE_SUCCESS }}'
})
deploy-preview-prod:
if: github.ref == 'refs/heads/prod'
runs-on: ubuntu-latest
steps:
- id: deploy
uses: sergeysova/jq-action@v2
with:
cmd: |
RESPONSE=$(curl --fail -X POST "https://api.vercel.com/v13/deployments?teamId=${{ env.VERCEL_TEAM_ID }}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ env.VERCEL_DEPLOYMENT_TOKEN }}" \
-d '{
"name": "${{ env.VERCEL_PROJECT_NAME_PROD }}",
"gitSource": {
"type": "github",
"org": "${{ github.event.repository.owner.login }}",
"repo": "${{ github.event.repository.name }}",
"ref": "prod"
}
}')
echo $RESPONSE | jq -c '{url, inspectorUrl}'
- name: "Add commit comment when failed"
uses: actions/github-script@v5
if: failure()
with:
github-token: ${{env.GITHUB_TOKEN}}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: '${{ github.sha }}',
body: '❌ \'${{ github.workflow }}\' failed: ${{ env.MESSAGE_FAILURE }}'
})
- name: "Add commit comment when success"
uses: actions/github-script@v5
if: success()
with:
github-token: ${{env.GITHUB_TOKEN}}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: '${{ github.sha }}',
body: '✅ \'${{ github.workflow }}\' succeeded: ${{ env.MESSAGE_SUCCESS_PROD }}\n| Name | Inspect | Preview |\n| :--- | :----- | :------ |\n| **${{ env.VERCEL_PROJECT_NAME_PROD }}** | [Check Inspector](${{ fromJSON(steps.deploy.outputs.value).inspectorUrl }}) | [Visit Preview](https://${{ fromJSON(steps.deploy.outputs.value).url }})'
})
다음 상황에서 actions가 작동합니다.
on:
pull_request:
types:
- opened #PR이 올라왔을 때
- synchronize #PR에 커밋을 추가할 때
push:
branches:
- beta #beta 브랜치에 커밋을 추가할 때
- prod #prod 브랜치에 커밋을 추가할 때
job은 총 3개가 있습니다.
jobs:
deploy-preview-beta:
if: github.event_name == 'pull_request'
...
deploy-production-beta:
if: github.ref == 'refs/heads/beta'
...
deploy-preview-prod:
if: github.ref == 'refs/heads/prod'
...
deploy-preview-beta:
모든 PR과 그 변경사항에 대해서 Vercel beta 프로젝트에 preview 배포를 합니다.deploy-production-beta:
beta 브랜치에 커밋을 추가하면 Vercel beta 프로젝트에 production 배포를 합니다.deploy-preview-prod: prod
브랜치에 커밋을 추가하면 Vercel prod 프로젝트에 preview 배포를 합니다.
Vercel prod프로젝트에 production 배포를 하면 실제 사용자가 사용하는 제품을 배포하는 것이기 때문에, Vercel에 직접 접속해서 preview 배포에 대해 직접 ‘Promote to Production’을 하는 방식으로 배포를 합니다.
# secrets을 직접 사용하지 않고 env를 통해 사용함으로써 이 action이 어떤 secret값을 필요로 하는지 한 눈에 볼 수 있도록 한다.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 조직 공통
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} # 조직 공통
VERCEL_DEPLOYMENT_TOKEN: ${{ secrets.VERCEL_DEPLOYMENT_TOKEN }} # 조직 공통
VERCEL_PROJECT_NAME_BETA: ${{ secrets.VERCEL_PROJECT_NAME_BETA }} # 프로젝트별로 repository에서 지정해줘야 한다.
VERCEL_PROJECT_NAME_PROD: ${{ secrets.VERCEL_PROJECT_NAME_PROD }} # 프로젝트별로 repository에서 지정해줘야 한다.
deploy-vercel.yml
action은 5개의 secret 값을 사용하지만 GITHUB_TOKEN
, VERCEL_TEAM_ID
, VERCEL_DEPLOYMENT_TOKEN
은 조직의 모든 private repository에서 별도의 설정 없이 사용할 수 있습니다. 개별 repository별로 VERCEL_PROJECT_NAME_BETA
, VERCEL_PROJECT_NAME_PROD
만 secret에 추가해서 deploy-vercel.yml
을 변경사항 없이 그대로 사용하면 됩니다.
PR이 올라오면 Vercel plugin에서는 계정이 연동 안됐다고 불평하지만 actions에서 강제로 preview 배포를 진행하게 되고 Vercel plugin이 친절하게 댓글을 달아줍니다.
PR에 커밋을 추가하면 github actions에서 ✅ 'Deploy to Vercel' succeeded: Vercel 배포 API를 호출했습니다. Vercel 플러그인의 댓글을 기다려주세요.
문구의 댓글을 답니다. Vercel plugin은 기존의 댓글을 수정하면서 진행상황을 업데이트합니다.
beta브랜치에 커밋을 추가하면 역시 Vercel plugin은 커밋의 댓글에서 GitHub계정 연동이 안 되어있다고 불평을 합니다. 하지만 강제로 배포하면 역시 친절하게 댓글을 달아줍니다.
beta브랜치의 커밋을 fast-foward로 prod브랜치에 반영한 뒤 push를 하면 이번엔 Vercel의 prod project에 강제 배포를 합니다. production 배포에 대해서는 vercel plugin이 따로 진행상황을 알려주지 않기 때문에 github actions에서 링크가 포함되어 있는 댓글을 추가하도록 했습니다.
신규 프로젝트는 아래의 절차를 따르면 됩니다.
- Repository 생성하고 애플리케이션 코드 추가
- beta, prod branch 설정, beta를 default branch로 설정
- Vercel Github App에 repository 추가
- https://github.com/organizations/your-organization-name/settings/installations 에서 Vercel App의 ‘Configuration’ 버튼 눌러서 신규 repository 추가
- 추가하지 않으면 Vercel에서 repository에 접근할 수 없다.
- Vercel에서 beta, prod 프로젝트 각각 만들기
- 프로젝트 생성하면서 첫 번째 배포를 한다.
- GitHub Repository에 actions용 secret 추가
- 브랜치 추가하고 github actions 커밋 한 뒤 PR올리기
- 배포 잘 되는지 확인
끗