인턴때 진행한 NCP Finance 환경에서 Bastion Host를 따로 두지 않고, Cloud Functions을 활용해 CI/CD 파이프라인을 구축한 과정을 공유해보고자 합니다. 어쩌다가 NCP Finance 환경을 사용하게 되신 분들은 도움이 되셨으면 좋겠습니다.
NCP 뿐 만 아니라 private subnet 환경에서 CICD를 구축해보고 싶으신 분들도 한번 참고해보시면 좋을 것 같습니다.
1. 구축 배경
프로젝트의 인프라 환경은 다음과 같은 제약이 있었습니다.
- 환경: NCP Finance
- 네트워크: 모든 애플리케이션 서버는 Private Subnet에 위치
- 접근 제어: 외부 인터넷에서 내부 서버로의 SSH(22번 포트) 접근 불가
보통 이런 환경에서 자동 배포를 구성하려면 두 가지 방법 중 하나를 선택합니다.
- Bastion Host 사용: Public Subnet에 중계 서버를 띄워두고 터널링으로 접근합니다.
- 이 방법이 가장 정석적인 방법이 아닐까 생각합니다. 하지만 비용문제로 인해 사용할 수 없었습니다.
- NCP SourcePipeline 사용: 클라우드 벤더가 제공하는 CI 도구를 사용합니다.
- 문제: 확인해 보니 빌드 환경이 Ubuntu 16.04 기반이었습니다. 최신 라이브러리와 Python 3.10 이상을 사용해야 하는 우리 프로젝트와 호환되지 않았고, 코드 저장소를 별도로 관리해야 하는 번거로움이 있었습니다.
우리는 익숙한 GitHub Actions를 그대로 쓰면서, 추가 비용 없이 내부망에 배포할 방법이 필요했습니다.
2. 해결 아이디어: Serverless를 트리거로 사용하기
해결책으로 떠올린건 Cloud Functions를 활용하는 것이었습니다(Cloud Functions 란?). Cloud Functions는 VPC 내부 리소스에 접근할 권한을 가질 수 있고, 호출될 때만 과금되므로 Bastion 서버보다 훨씬 경제적입니다.
전체적인 아키텍처 흐름은 다음과 같습니다.
GitHub Actions (빌드/Push) -> API Gateway -> Cloud Functions (VPC 내부) -> Private Server (SSH 명령)

3. 구축 과정
Step 1. 배포 대상 서버(VM) 설정
먼저 배포 대상 서버에 배포 스크립트(deploy.sh)를 작성해 둡니다. 외부에서 복잡한 명령어를 주입하는 것보다, 내부 스크립트를 실행만 하는 방식이 관리하기 편하고 오류 가능성도 적습니다.
무엇보다 외부에서 명령어를 직접 운영서버에 injection 할 경우 보안적으로 위협이 된다고 생각했기에 Cloudfunctions는 최대한 단순한 행위만 할 수 있도록 구성했습니다.
/home/ubuntu/deploy.sh
#!/bin/bash
set -e
# 1. 최신 이미지 Pull
docker pull <레지스트리 주소>
# 2. 기존 컨테이너 종료 및 정리
echo "Stopping containers..."
cd <docker compose 파일 위치>
docker compose down
# 3. 서비스 재시작
echo "Deploying new version..."
docker compose up -d
# 4. 불필요한 이미지 정리
docker image prune -a -f
Step 2. Cloud Functions 생성
Cloud Functions가 Private Server와 통신하려면 반드시 동일한 VPC에 배치되어야 합니다.
- Subnet 생성: Cloud Functions용 Subnet을 생성할 때, 배포 대상 서버와 통신 가능한 대역(Private)을 선택합니다.
- Action 생성: Python 등의 런타임을 선택하고 코드를 작성합니다. 이 함수는 SSH로 대상 서버에 접속해 sh deploy.sh를 실행하는 역할만 수행합니다.
설정 체크포인트:
- VPC: Target Server와 동일한 VPC
- ACG 설정: 배포 대상 VM의 AGC에서 Cloud Functions의 주소로 22번 포트를 열어주어야 합니다.
cloudfunctions-vpc<VPC_ID> 형식으로, Cloud Functions를 만들었다면 자동완성 추천이 나오는 걸로 설정해줍니다. - Zone은 KR-2로 설정합니다.
Step3. Action 코드 작성 및 패키징
SSH 접속을 위해 Python의 paramiko 라이브러리를 사용합니다. NCP Cloud Functions는 기본 라이브러리 외의 외부 라이브러리(pip install 필요 항목)를 사용할 경우, 반드시 로컬에서 패키징하여 Zip 파일로 업로드해야 합니다.
디렉토리 구조:
deploy-function/
├── __main__.py # 메인 실행 코드
├── requirements.txt # 의존성 목록
└── package/ # pip install로 설치된 라이브러리 폴더
__main__.py 작성 예시:
import sys
import os
# 패키지 경로 추가
sys.path.append(os.path.join(os.path.dirname(__file__), 'package'))
import paramiko
def main(args):
host = "10.0.x.x" # Target Server의 Private IP
port = 22
username = "root"
password = args.get("SERVER_PASSWORD") # 혹은 SSH Key 방식 사용
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
# 1. SSH 접속
client.connect(host, port=port, username=username, password=password)
# 2. 배포 스크립트 실행
stdin, stdout, stderr = client.exec_command("sh /home/ubuntu/deploy.sh")
# 3. 결과 리턴
result = stdout.read().decode()
error = stderr.read().decode()
if error:
return {"payload": {"status": "error", "log": error}}
return {"payload": {"status": "success", "log": result}}
except Exception as e:
return {"error": str(e)}
finally:
client.close()
Step 4. API Gateway 연결: 외부 트리거 생성
Cloud Functions를 만들었다면, 이제 GitHub Actions가 이 함수를 호출할 수 있도록 해야합니다.
API Gateway 콘솔에서 처음부터 세팅할 수도 있지만, Cloud Functions의 [트리거] 탭을 이용하면 훨씬 간편하게 연동할 수 있습니다.
1. 트리거 생성 (Cloud Functions Console)
작성한 Action의 상세 화면에서 [트리거] 탭으로 이동해 + 트리거 추가 버튼을 클릭합니다.
- 타입: API Gateway
- 동작: 신규 생성 (기존에 만들어둔 Product가 있다면 선택해도 됩니다)
- 옵션:
- 인증: '사용(API Key 필요)' 체크
- 스테이지: v1 또는 real 등 식별 가능한 이름 입력
2. 경로(Path) 설정과 /{type+}
URL 경로를 설정할 때 주의할 점이 있습니다. 단순히 /deploy로 끝내는 것이 아니라, 뒤에 /{type+}를 붙여주어야합니다.
- 설정 예시: /deploy/{type+}
- 이유: API Gateway가 Cloud Functions로 데이터를 넘길 때, JSON 포맷(Body)을 온전히 전달받기 위한 예약어 설정입니다. 이 설정을 하지 않으면 GitHub Actions에서 보낸 Payload가 함수 내부로 제대로 전달되지 않는 경우가 발생할 수 있습니다.
3. API 배포 (Publish)
- NCP 콘솔 > API Gateway 메뉴로 이동합니다.
- 방금 생성된 Product를 선택하고 [API 배포] 버튼을 클릭합니다.
- 앞서 설정한 스테이지를 선택하여 배포를 완료합니다.
4. API Key 및 URL 확인
GitHub Actions에 등록할 두 가지 정보를 확보합니다.
- Invoke URL: API Gateway 콘솔의 [스테이지] 메뉴에서 배포된 URL을 확인합니다.
- 형식: https://{random-id}.apigw.ntruss.com/deploy/v1/json
- (참고: /{type+}를 설정했으므로 호출 시 끝에 /json 등을 명시해야 데이터가 정상 바인딩됩니다.)
- API Key:
- [API Gateway > API Keys] 메뉴로 이동하여 API Key 생성을 클릭합니다.
- 생성된 키를 복사해 둡니다.
- API Key들은 별도 탭에서 삭제도 가능하므로, 혹시 유출되었다면, 변경 혹은 삭제 후 재생성을 할 수 있습니다.
Step 4. API Gateway 및 GitHub Actions 연동
Cloud Functions는 기본적으로 내부망에 숨어있기 때문에, 외부에서 호출할 수 있도록 API Gateway와 연결합니다. 그리고 GitHub Actions 워크플로우 마지막 단계에서 이 API를 호출하도록 설정합니다.
.github/workflows/deploy.yml
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Trigger Deployment via API
run: |
curl -X POST \
-H "x-ncp-apigw-api-key: ${{ secrets.NCP_APIGW_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{}' \
${{ secrets.NCP_APIGW_API_URL }}
보안을 위해 API Key와 URL은 GitHub Secrets에 저장하여 관리합니다.
4. 마무리
정석적인 방법의 인프라 구축과는 조금 거리가 있어보였지만, 제약된 상황에서 얻은 성과는 다음과 같았습니다.
- 비용 절감: 상시 운영해야 하는 Bastion 서버 비용을 아끼고, 배포 시에만 실행되는 Serverless 비용으로 운영이 가능합니다.
- 개발 생산성: SourcePipeline의 제약에서 벗어나, 최신 Ubuntu 환경의 GitHub Actions를 자유롭게 사용할 수 있었습니다.
금융 클라우드나 보안이 엄격한 환경에서 CI/CD 구축을 고민하고 있다면, 별도의 서버 증설 없이 Serverless 리소스를 활용해 연결 고리를 만들어보는 방식을 추천합니다.