GitHub Actions + Infisical로 만드는 배포 플로우

2025. 5. 4. 12:08·개발이야기

저는 효율적인 개발 환경을 구축하고 반복적인 작업을 자동화하는 것을 좋아합니다. 처음 개발에 관심을 갖게 된 이유도, 불편하고 번거로운 일을 컴퓨터로 해결할 수 있다는 점에서 매력을 느꼈기 때문입니다.

 

실제로 문제를 해결하기 위해 다양한 서비스를 만들고, 이를 직접 사용하기 위한 배포 방식을 꾸준히 고민해왔습니다. 예를 들어, 알리익스프레스에서 저렴하게 구매한 저전력 컴퓨터인 Intel N100을 설치한 뒤, 집의 NAT 방화벽을 우회하고 보안을 조금이나마 강화하기 위해 월 6,000원 정도의 VPS를 대여해 리버스 프록시를 구축했습니다.

 

이때 집에 있는 컴퓨터에는 직접 접근을 허용하지 않고, 반드시 VPS를 거쳐 접속하도록 구성했으며, 웹 브라우저에서 SSH에 접속할 수 있도록 Apache Guacamole도 함께 설정했습니다.

 

이후 동아리 활동 중 자동 배포가 필요했던 프로젝트에서는 Dokku를 사용해 간단한 PaaS 환경을 구성하고, 자동화된 배포 파이프라인을 구축하는 데 성공했습니다.

 

 

빌드과정과 운영을 분리하고 싶은 욕심

GitHub에서는 공개 리포지토리에 한해 GitHub Actions를 무료로 제공하고 있습니다. 저도 이 무료 자원을 최대한 활용하고자, GitHub Actions 워크플로우를 통해 프로젝트를 자동으로 빌드하고, 빌드 결과물을 Release에 업로드하는 방식을 사용하고 있습니다.

 

처음에는 Release 페이지에서 직접 파일을 다운로드해 배포했지만, 반복적인 작업이 번거롭게 느껴져 이를 자동화하는 간단한 스크립트를 작성했습니다. 덕분에 배포 과정은 다소 간편해졌지만, 여전히 직접 실행해야 한다는 수동적인 요소가 남아 있었습니다.

 

#!/bin/bash

# Stop Docker Compose
docker-compose stop

# Download the JAR file
wget -O handong-feed-0.0.1-SNAPSHOT.jar <https://github.com/junglesub/handong-feed-prototype/releases/latest/download/handong-feed-0.0.1-SNAPSHOT.jar>

# Check if the download was successful
if [ $? -eq 0 ]; then
    echo "Download successful. Restarting Docker Compose..."
    docker-compose restart
else
    echo "Download failed. Docker Compose restart aborted."
    echo "Still restarting because docker-compose was stopped to download."
    docker-compose restart
fi

 

이번 기회를 통해 GitHub Actions에서 빌드가 완료되면 Release를 생성하는 것뿐만 아니라, 자동으로 배포까지 이어지는 워크플로우를 구성해보고자 합니다. 지금까지는 빌드 후 수동으로 배포 스크립트를 실행했지만, 이를 완전히 자동화하여 빌드부터 배포까지의 전 과정을 GitHub Actions로 통합하는 방향을 고민하고 있습니다.

 

Infisical을 활용한 환경변수 관리

 

애플리케이션 개발에서 DB 연결 정보나 시크릿 값(secret)을 안전하게 관리하는 것은 매우 중요합니다. 지금까지는 .env 파일을 노션을 통해 팀원들과 공유해왔고, 개발 서버에서는 docker-compose의 env_file 속성을 이용해 환경 변수를 주입하는 방식으로 관리했습니다.

 

하지만 팀원 간에 서로 다른 시크릿 키를 사용하는 일이 생기면서, 동일한 데이터베이스를 사용하더라도 암호화된 값이 서로 달라져 기능이 정상적으로 동작하지 않는 문제가 발생했습니다. 게다가 앞으로는 stage 환경도 추가될 예정이기에, 공통된 시크릿 값을 안전하게 관리하고 최신 상태를 쉽게 공유할 수 있는 방식이 필요하다고 판단했습니다.

 

시크릿 관리와 관련된 SecOps(보안 운영) 분야를 조사한 결과, 여러 유료 서비스가 존재했지만 실습과 테스트가 중심이었던 저희 팀의 상황에 맞춰, 무료로 사용할 수 있는 플랫폼인 Infisical을 선택하게 되었습니다.

 

Self Hosted PaaS

서버 구성상 직접적인 포트 노출을 최소화하고, HTTP Reverse Proxy를 통해서만 통신해야 하는 환경이었습니다. 기존에는 Jenkins를 사용해 리포지토리 푸시를 감지하고, 코드를 클론한 뒤 추가 파일을 삽입하고, Dokku 서버로 푸시하여 Dokku에서 빌드 및 배포를 수행하는 구조를 사용했습니다.

 

그러나 이번에는 GitHub Actions에서 단순히 JAR 파일을 빌드하고, 해당 결과물만 서버에 전달하는 구조를 원했습니다. 이를 위해 다양한 Self-Hosted PaaS 솔루션을 조사해보았습니다.

  • Dokku: 기본적으로 git push 기반의 빌드/배포 방식이며, Dockerfile을 통한 유연한 설정도 가능하지만, 환경 변수 설정과 서버 설정이 복잡해 요구하는 단순성에 맞지 않았습니다.
  • Coolify: Dokku와 유사한 구조지만, 멀티 서버 및 오토스케일링, GUI 지원 등 관리 편의성이 뛰어납니다. 다만, 상대적으로 무겁고 리소스 요구량이 높아 가벼운 자동화를 원했던 이번 요구에는 적합하지 않았습니다.
  • CapRover: 매우 간편하게 서비스를 배포할 수 있는 솔루션이나, Docker Swarm을 필수로 요구하는 점이 문제였습니다. 기존에 운영 중인 Docker 컨테이너들과 충돌 가능성이 있어, 단일 머신을 CapRover 전용으로 사용할 수 있을 때 적합합니다. (포트 관련 개선 이슈)
  • WatchTower, Diun: 새 이미지를 감지해 자동 배포할 수 있는 툴이지만, Webhook을 지원하지 않아 즉각적인 반영이 어려운 점이 아쉬웠습니다. 대부분 cron 기반 주기적 감지 방식입니다.

이처럼 각 솔루션은 장단점이 명확하며, 전체 서비스를 자동화하는 목적이라면 적합할 수 있지만, 저는 docker-compose 기반으로 다양한 서비스를 직접 운영 중이기 때문에, 일부 서비스만 선택적으로 자동화하는 방식을 원했습니다. 결과적으로 위 솔루션들은 현재 환경과 요구사항에 완전히 들어맞지 않았습니다.

 

직접 만들기로 결정하다

최종적으로, 저의 요구사항에 정확히 부합하는 솔루션이 존재하지 않았기 때문에, 직접 경량화된 배포 자동화 시스템을 만들기로 결정했습니다.

 

전체 플로우는 다음과 같습니다:

  • GitHub Actions에서 프로젝트를 JAR 파일로 빌드
  • 결과물을 기반으로 Docker 이미지를 생성하여 GitHub Container Registry(ghcr.io)에 배포
  • 배포가 완료되면, 구성한 간단한 웹 서버에 Webhook 요청을 전송
  • 웹 서버는 해당 요청을 받아 Docker 이미지를 pull한 뒤 컨테이너를 재시작하여 최신 버전으로 반영

이렇게 구성함으로써, GitHub에서 푸시 → 자동 빌드/배포 → 서버 반영까지 전체 과정이 자동화되었고, 복잡한 PaaS 없이도 원하는 수준의 배포 흐름을 직접 구축할 수 있었습니다.

 

[1단계] 깃허브 액션

새로 제작한 Dockerfile

FROM amazoncorretto:17-alpine

ARG JAR_FILE

# 필요한 패키지 설치 및 infisical 설치
RUN apk add --no-cache bash curl \\
    && curl -1sLf '<https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh>' | bash \\
    && apk add --no-cache infisical \\
    && apk del curl bash \\
    && rm -rf /var/cache/apk/*

# Timezone 설정 (이미지에 반영)
ENV TZ=Asia/Seoul

# jar 파일을 컨테이너에 복사
COPY ${JAR_FILE} /runme.jar

# 포트 노출
EXPOSE 8080

# 비특권 사용자 생성 및 디렉토리 권한 설정
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN mkdir -p /app/logs && chown -R appuser:appgroup /app

# 비특권 사용자로 전환
USER appuser

# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \\
  CMD wget -q --spider <http://localhost:8080/api/health> || exit 1

# infisical run을 통해 서비스 시작
CMD ["sh", "-c", "infisical run --projectId=${INFISICAL_PROJECT_ID} --domain=${INFISICAL_DOMAIN} --env=${INFISICAL_ENV} -- java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -jar /runme.jar"]

 

필요 환경변수 - 이후 이미지를 실행할 때 필요

 

- INFISICAL_TOKEN
- INFISICAL_PROJECT_ID
- INFISICAL_DOMAIN
- INFISICAL_ENV

 

💡TIP: GitHub Actions에 적용하기 전에, docker build --build-arg JAR_FILE=the.jar -t handong-feed:latest . 명령어를 이용해 로컬에서 미리 이미지를 빌드하고 테스트함으로써 빠르게 작업을 진행할 수 있었습니다.

 

로컬 빌드한 이미지 실행하기

docker run --rm -e INFISICAL_TOKEN="token" -e INFISICAL_PROJECT_ID="project_id" -e INFISICAL_DOMAIN="domain" -e INFISICAL_ENV=prod handong-feed:latest

 

추가한 Github Actions

docker:
  runs-on: ubuntu-latest
  needs: [setup, build] # 이전 Job이 끝난 후에 실행

  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 1

    # artifact 다운로드
    - name: Download JAR artifact
      uses: actions/download-artifact@v4
      with:
        name: spring-boot-jar
        path: ./

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to GitHub Container Registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    # 빌드 생성 - 이전 Job Steps 에서 생성한 artifact 사용.
    - name: Build and Push Docker image
      uses: docker/build-push-action@v5
      # if: ${{ !env.ACT }}
      with:
        context: .
        file: ./Dockerfile
        push: true
        platforms: linux/amd64,linux/arm64
        tags: |
          ghcr.io/${{ github.REPOSITORY_OWNER }}/handong-feed-app:latest
          ghcr.io/${{ github.repository_owner }}/handong-feed-app:${{ needs.setup.outputs.current_date }}
          ghcr.io/${{ github.repository_owner }}/handong-feed-app:build-${{ github.run_number }}
        labels: |
          org.opencontainers.image.source=${{ github.SERVER_URL	}}/${{ github.REPOSITORY }}
          org.opencontainers.image.revision=${{ github.SHA }}
        build-args: |
          JAR_FILE=./handong-feed-0.0.1-SNAPSHOT.jar

ACT 를 사용하여 로컬에서 테스트해본 모습

 

GitHub Container Registry 설정

GitHub Actions를 통해 이미지를 성공적으로 배포했다면, 해당 리포지토리에서 바로 확인 가능한 경우도 있지만, 저는 Repository와 Registry 간 연결이 바로 이루어지지 않아 약간의 추가 설정이 필요했습니다.

 

이미지는 GitHub의 Packages 페이지 (배포한 계정의 프로필 → Packages) 에서 확인할 수 있으며, 필요하다면 "Repository Source"를 수동으로 추가해줘야 정상적으로 연결됩니다.

 

처음에는 ACT를 사용해 로컬에서 GitHub Actions 워크플로우를 테스트했기 때문에, 이미지가 정상적으로 업로드되지 않았던 것으로 보입니다. 이 경우, 한 번은 로컬에서 직접 push를 수행한 뒤, Settings > Packages > Manage Actions Access에서 해당 리포지토리에 대한 액세스 권한을 부여해주면, 이후에는 GitHub Actions에서도 정상적으로 배포가 가능합니다.

 

추가로, 이후에 Repository와 Registry 연결을 할 경우 업로드된 패키지는 비공개(private)로 설정되어 있으며, 공개(public)로 전환하면 비로그인 사용자도 Docker Hub처럼 이미지를 검색 및 다운로드할 수 있습니다.

 

 

[2단계] 홈서버에서 이미지 실행

이미 GitHub Actions를 통해 Docker 이미지를 빌드하고 GitHub Container Registry(ghcr.io)에 업로드하는 과정까지 완료했습니다.

 

이제 남은 작업은 업로드된 이미지를 홈서버에서 받아 실행하는 것입니다.

내부 컨테이너를 제어하는 API - DCA (Docker Control API)

홈서버에서는 GitHub Actions에서 배포된 이미지를 자동으로 반영하기 위해, 도커 소켓을 직접 활용하여 내부 컨테이너를 제어하는 API를 구현했습니다.

 

Docker의 Unix Socket을 공유하면 외부에서 컨테이너 상태를 조회하거나 직접 제어하는 것이 가능하지만, 이는 곧 루트 권한 수준의 권한이 노출될 수 있기 때문에, 보안을 고려해 API의 기능은 최소한으로만 구성했습니다.

 

제가 만든 DCA(Docker Control API)는 간단한 REST API로, 요청을 받으면 해당 컨테이너를 중지하고 최신 이미지를 pull한 뒤, 다시 컨테이너를 실행하는 작업을 수행합니다.

 

API 요청 예시는 다음과 같습니다:

POST /update?id={myApp}
Headers:
  X-API-Key: {32 byte API key}
Body:
  {
	  "gh": {
	    "commitSha": "COMMITSHA",
	    "githubRepo": "REPO"
	  }
  }

 

이 방식 덕분에, GitHub Actions에서 빌드 완료 후 Webhook을 통해 해당 API를 호출하면, 서버에서 자동으로 최신 버전의 컨테이너를 반영할 수 있게 되었습니다.

 

가장 먼저, 도커 소켓을 활용해 내부 컨테이너를 제어할 수 있는 API를 만들었습니다. 도커 소켓을 공유하면 다른 컨테이너의 상태를 조회하거나 제어할 수 있기 때문에, 불필요한 권한 노출을 막기 위해 설계는 최대한 단순하고 최소한으로 유지했습니다.

 

config.yml 예시

yaml
복사편집
keys:
  myApp1:  # 클라이언트에서 요청 시 사용하는 ID
    container_name: containerA  # 실제 도커 컨테이너 이름
    secret_key: abc123...       # API 인증용 키 (현재는 평문 저장)

 

⚠️ 현재는 시범 적용을 위한 상태로, secret_key를 평문으로 저장하고 있습니다.

다음 버전에서는 해시화된 형태로 저장하고, 요청 시 입력받은 키와 비교하는 방식으로 보안을 강화할 예정입니다.

 

 

최종 결과:

 

여담: Elysia를 사용해보다 (elysiajs.com)

최근 즐겨보는 유튜브 개발자 분을 통해 Elysia.js라는 웹 프레임워크를 접하게 되었습니다. 해당 프레임워크의 개발자 분을 살펴보니, 단순한 코드 외에도 개성 넘치는 깃허브 프로필과 공식 문서에서 볼 수 있는 개성 있는 VSCode 환경 에서 남다름을 느껴졌습니다.

 

Elysia는 Express와 달리, method chaining으로 모든 라우팅과 미들웨어를 구성하는 방식이 인상적이었고, 입출력 타입 검증(type validation)이 기본적으로 통합되어 있다는 점도 매우 편리하게 느껴졌습니다. 또한, 로직을 명확히 분리할 수 있는 구조도 새로웠습니다.

 

무엇보다 아직은 비주류 프레임워크다 보니 자료가 많지 않고, GPT를 통한 코드 힌트도 제한적이어서, 오히려 2020년 처음 프로그래밍을 배울 때의 '스스로 부딪히는 재미'를 다시 느낄 수 있었습니다. 하지만, 프로젝트 마감이 2주밖에 남지 않은 상황에서 익숙하지 않은 프레임워크를 선택한 건 다소 무모했던 선택이었던 것 같습니다 😅

 

현재 amd64 기반 서버 환경에서는 config.yml이 핫리로드되지 않는 등의 문제도 있어 개선이 필요하지만, 핵심 기능은 정상 작동 중입니다. 이후에는 Elysia를 더 깊이 써보고, 어떤 시행착오가 있었는지를 나누는 글도 정리해보겠습니다.

 

👉 Docker-CTRL-API (DCA) 프로젝트 보기: https://github.com/junglesub/docker-ctrl-api

 

[3단계] GitHub Actions에서 CURL 요청 보내기

이제 약 일주일간 이어진 자동화 작업의 마지막 단계입니다.

앞서 1단계에서 작성한 GitHub Actions 워크플로우의 마지막에는, Docker Control API(DCA)를 호출해 서버의 컨테이너를 자동으로 업데이트하는 요청이 포함되어야 합니다.

 

이를 위해 GitHub Actions 내에서 curl 명령어를 사용해 HTTP POST 요청을 보내고 있으며,

요청에 필요한 DCA 엔드포인트 주소 및 인증 키는 Infisical에서 직접 Secrets로 불러와 사용하는 방식으로 구성했습니다.

 

덕분에 민감한 정보는 안전하게 보호하면서도, 빌드 완료 후 자동 배포까지 하나의 워크플로우로 통합할 수 있게 되었습니다.

 

참고문서: https://infisical.com/docs/documentation/platform/identities/oidc-auth/github

 

GitHub에 OIDC(OpenID Connect)를 이용한 인증 방식을 설정해보았습니다. OIDC는 처음 접하는 개념이었는데, 흔히 사용하는 "Google로 로그인하기" 기능이 바로 이 프로토콜을 활용한 예시라는 점이 인상 깊었습니다. 그동안 OAuth로 로그인 기능을 구현한다고만 생각했는데, 실제로는 OAuth는 권한 위임(Authorization)에 초점을 둔 프로토콜이고, 인증(Authentication)을 포함하는 것은 OIDC라는 사실을 이번에 명확히 알게 되었습니다. 용어의 정확한 의미를 이해하고 나니, GitHub에서는 OIDC를 어떤 방식으로 활용해 인증을 처리하는지 더 깊이 공부해볼 필요성을 느꼈습니다.

출처: 최동근 - [OIDC]란 무엇일까?

 

추가된 Workflow Job

trigger-deploy:
  runs-on: ubuntu-latest
  needs: docker # 도커 작업이 완료 되면 실행
  permissions:
    id-token: write # OIDC 토큰을 사용하여 Infisical에 인증
    statuses: write # GitHub 상태 API에 대한 쓰기 권한

  steps:
    - name: Import Environment Variables from Infisical
      uses: Infisical/secrets-action@v1.0.7
      with:
        method: "oidc"
        env-slug: "prod"
        domain: "${{ secrets.INFISICAL_DOMAIN }}"
        project-slug: "github-actions"
        identity-id: "${{ secrets.INFISICAL_IDENTITY }}"
        secret-path: "/Handong-App-Deploy"

    - name: Send Deploy Request to DCA
      run: |
        curl --fail --request POST \\
          --url "$DCA_ADDR" \\
          --header "content-type: application/json" \\
          --header "X-API-Key: $DCA_SECRET" \\
          --data "{\\"gh\\": {\\"commitSha\\": \\"$GITHUB_SHA\\", \\"githubRepo\\": \\"$GITHUB_REPOSITORY\\"}}"
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

 

INFISICAL_DOMAIN과 INFISICAL_IDENTITY 변수는 추후 재사용을 위해 Organization 환경 변수로 등록해두었습니다.

 

다른 프로젝트에 적용할 때는 project-slug와 secret-path만 수정하면 되고, DCA에 요청을 보낼 때는 Infisical에서 자동으로 불러온 값을 사용할 수 있도록 설정했습니다.

 

이로써, GitHub Actions 기반의 완전 자동 배포 플로우가 구축된 것을 확인할 수 있었습니다.

 

마무리

지금까지는 도커 이미지를 만들더라도 로컬에서만 사용을 했었는데, 이번 기회를 통해서 Registry 로 배포하여 풀 받아 사용하는 것을 적용했습니다. 처음에는 기존에 있는 이미지들을 풀만 받아서 사용하다가, 직접 만들어보니 도커에 대한 이해도가 조금 더 상향한 것 같습니다. 아직 개선해야할 점이 많이 남았지만 - 한번 자동 배포를 성공하니 앞으로 조금 편해지지 않을까 기대해봅니다.

 

지금까지는 Docker 이미지를 빌드해도 로컬 환경에서만 테스트하는 데 그쳤습니다. 하지만 이번 프로젝트를 통해, 빌드된 이미지를 GitHub Container Registry로 배포하고, 서버에서는 이를 pull해 자동으로 재배포하는 구조를 완성할 수 있었습니다.

 

기존에는 남이 만든 이미지를 받아 쓰는 데 그쳤지만, 직접 이미지를 만들고 배포해보면서 Docker에 대한 이해도도 훨씬 깊어진 것 같습니다. 물론 아직 개선할 점은 많지만, 한 번 자동 배포를 성공시켜본 경험이 앞으로의 개발 환경을 훨씬 더 편하게 만들어줄 것이라는 기대를 가지게 되었습니다.

 

앞으로의 개선 계획

🔧 Docker-CTRL-API (DCA)

  • GitHub App 연동
    여러 리포지토리에서도 손쉽게 사용할 수 있도록, DCA를 GitHub App 형태로 연동할 예정입니다.
  • Secret Key 보안 강화
    현재는 평문으로 저장 중인 API 키를 해시화하여 저장하고 비교하는 방식으로 전환할 계획입니다.
  • 실행 시 설정 리로딩 문제 해결
    매 요청 시 config.yml이 재로딩되지 않는 문제의 원인을 분석하고, 설정 핫리로드 기능을 안정화할 예정입니다.
  • 무중단 배포(Rolling Deployment)
    기존 컨테이너를 중단하지 않고 새 컨테이너를 점진적으로 교체하는 방식의 롤링 배포 기능을 실험해볼 계획입니다.
  • Commit Status 오류 수정
    failed → pending 상태로 되돌아가는 GitHub commit status 관련 오류를 해결할 예정입니다.
  • GitHub Actions 템플릿화
    다른 프로젝트에서도 쉽게 도입할 수 있도록, DCA에 맞춘 GitHub Actions 템플릿을 제작하여 배포할 계획입니다.

'개발이야기' 카테고리의 다른 글

브라우저에 이미지 캐싱하기 - Cache Storage와 crossorigin  (2) 2025.03.08
나에게 자신감을 주었던 프로젝트 - 공구조회 사이트  (2) 2025.02.03
'개발이야기' 카테고리의 다른 글
  • 브라우저에 이미지 캐싱하기 - Cache Storage와 crossorigin
  • 나에게 자신감을 주었던 프로젝트 - 공구조회 사이트
정글잠수함
정글잠수함
컴퓨터와 함께하며 배운 내용들과 일지를 저장하는 공간입니다.
  • 정글잠수함
    컴돌이의 컴퓨터 이야기
    정글잠수함
    • 분류 전체보기 (9)
      • 쉬어가는 시간 (0)
      • 대학시간 기여일지 (1)
      • 개발이야기 (3)
        • 공구조회사이트 (0)
        • HUT 개발일지 (4)
      • 버그해결 (1)
  • 인기 글

  • 최근 글

  • 태그

    ACT
    service worker
    Flutter
    프로즌인사이드
    opaque
    handong feed
    불투명
    HUT
    github
    대학시간
    프런트엔드
    플러터
    react
    frontend
    프론트엔드
    docker
    겨울왕국
    github action
    nodejs
    sequelize
    캐시
    express
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
정글잠수함
GitHub Actions + Infisical로 만드는 배포 플로우
상단으로

티스토리툴바