본문 바로가기
서버 구축·실습

Azure VM에서 OpenAI API를 안전하게 연결해 AI 챗봇을 완성하는 방법

by joe2026 2026. 3. 2.
Azure VM 2대로 FE/BE를 분리하고 OpenAI API를 안전하게 연결해 AI 챗봇을 완성하는 전 과정을 정리했습니다. API Key 보호 구조, NSG 포트 설정, IIS Reverse Proxy, Node.js 서버 구성, PM2 배포까지 단계별로 설명합니다.

1. 실습 목적 — 이 글이 다루는 범위

AI 챗봇을 만드는 방법은 많다. 하지만 대부분의 입문 예제는 "작동하는 것"에만 집중하고, 실제 서비스 환경에서 필요한 보안 구조를 다루지 않는다.

이번 실습의 목표는 다음과 같다.

  • Azure에서 VM 2대(FE / BE)를 분리해 역할별 서버 구조 구성하기
  • OpenAI API Key를 브라우저에 노출하지 않고 서버에서만 관리하기
  • Azure 콘솔에서 OpenAI 리소스를 생성하고 모델을 배포하는 절차 이해하기
  • IIS Reverse Proxy를 통해 내부망 서버를 안전하게 연결하기
  • 완성된 구조가 실제로 동작하는 AI 챗봇으로 이어지는 과정 경험하기

실습 후 완성되는 구조는 다음과 같다.

외부 사용자 (브라우저)
  → FE VM — IIS (공인 IP, Reverse Proxy)
    → BE VM — Node.js (사설 IP, 내부망)
      → Azure OpenAI API (GPT 모델)
        → 응답 반환

이 흐름을 처음부터 끝까지 직접 구성해 보는 것이 이번 실습의 핵심이다.


2. 설계 핵심 개념 — 구조를 모르면 오류를 읽을 수 없다

① 공인 IP vs 사설 IP — 가장 먼저 이해해야 할 차이

Azure에서 VM을 생성하면 두 종류의 IP가 부여된다.

구분 형태 예시 접근 범위

공인 IP (Public IP) 20.x.x.x 인터넷 전체에서 접근 가능
사설 IP (Private IP) 172.16.x.x / 10.x.x.x Azure VNet 내부에서만 통신 가능

FE VM은 공인 IP를 가지고 외부에 노출된다. BE VM은 사설 IP만 가지며, 외부 브라우저에서 직접 접근할 수 없다. 이 구조가 보안의 첫 번째 계층이다.

② 왜 프런트에서 직접 OpenAI API를 호출하면 안 되는가

초보 단계에서 가장 많이 저지르는 실수가 여기서 발생한다.

// 절대 하면 안 되는 방식 — API Key가 브라우저에 그대로 노출됨
fetch("https://api.openai.com/v1/chat/completions", {
  headers: {
    "Authorization": "Bearer sk-xxxxxxxxxxxxxxxx"
  }
});

이 코드가 브라우저에서 실행되면, 개발자 도구(F12) → 네트워크 탭을 열면 요청 헤더가 그대로 보인다. sk-로 시작하는 API Key가 화면에 출력된다. URL을 아는 누구에게나 노출되는 셈이다.

올바른 흐름은 다음과 같다.

브라우저 → BE 서버 (API Key 보관) → OpenAI API

브라우저는 BE 서버에만 요청을 보내고, OpenAI API Key는 서버 내부에만 존재한다.

③ Reverse Proxy의 역할

IIS가 설치된 FE VM은 단순히 HTML을 제공하는 것 이상의 역할을 한다. /api로 시작하는 요청을 받아 내부망의 BE 서버로 전달하는 Reverse Proxy 역할을 맡는다.

외부 사용자: fetch("/api/ask")
  → FE VM IIS가 수신 → 172.16.1.4:3000으로 내부 전달
    → BE Node.js 서버 처리 → OpenAI 호출 → 응답 반환

이 구조 덕분에 BE 서버는 인터넷에 직접 노출되지 않는다.

④ BE 서버는 보안 계층이다

BE 서버를 단순 중계자로 보면 이해가 좁다. 이 계층에서 처리할 수 있는 것들이 있다.

  • API Key 보관 및 관리 — 외부 노출 없음
  • 입력 데이터 검증 — 빈 값, 비정상 요청 차단
  • 사용자 인증 — 로그인 여부 확인 추가 가능
  • 요청 속도 제한 — 무분별한 API 호출 방어
  • 사용 로그 — 어떤 요청이 들어왔는지 추적

AI API 앞에 놓이는 통제 지점이 BE 서버다.


3. Azure 콘솔 설정 — 단계별 실습 과정

STEP 1. Azure OpenAI 리소스 생성

Azure Portal에서 검색창에 "Azure OpenAI" 를 입력하고 리소스 만들기로 진입한다.

Azure Portal → 리소스 만들기 → Azure OpenAI 검색 → 만들기

설정 항목 입력값

구독 사용 중인 구독 선택
리소스 그룹 기존 그룹 또는 새로 생성
지역 East US 또는 Sweden Central 권장
이름 고유한 이름 입력 (예: my-openai-resource)
가격 책정 Standard S0 (현재 유일한 옵션)

지역 선택이 중요한 이유: 지역마다 사용 가능한 GPT 모델이 다르다. Korea Central은 지원 모델이 제한적이어서, gpt-4o나 gpt-4를 사용하려면 East US 또는 Sweden Central을 선택해야 하는 경우가 많다.

STEP 2. 모델 배포 (Deployment)

리소스 생성 후 모델 배포 탭으로 이동한다.

생성한 OpenAI 리소스 → 모델 배포 → + 배포 만들기

설정 항목 입력값

모델 gpt-4o 또는 gpt-35-turbo 선택
배포 이름 직접 지정 (예: my-gpt4-deployment)
버전 최신 버전 선택

배포 이름은 코드에서 직접 사용된다. 나중에 .env 파일에 이 이름을 그대로 입력해야 한다. 오타가 나면 404 오류가 발생한다.

STEP 3. API Key와 Endpoint 확인

OpenAI 리소스 → 키 및 엔드포인트 탭

다음 두 가지를 복사해 안전한 곳에 보관한다.

엔드포인트: https://my-openai-resource.openai.azure.com/
키 1:       abc123def456ghi789...

절대 코드에 직접 붙여넣으면 안 된다. 이 값은 .env 파일에만 저장한다.

STEP 4. BE VM — 환경 구성

Linux VM에 SSH로 접속한 뒤 Node.js와 필요 패키지를 설치한다.

# Node.js 설치 (Ubuntu 기준)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# 프로젝트 폴더 생성
mkdir ~/ai-server && cd ~/ai-server
npm init -y
npm install express dotenv @azure/openai

.env 파일 생성

nano .env
# .env 내용
AZURE_OPENAI_ENDPOINT=https://my-openai-resource.openai.azure.com/
AZURE_OPENAI_KEY=abc123def456ghi789...
AZURE_OPENAI_DEPLOYMENT=my-gpt4-deployment
PORT=3000

파일 저장 후 권한 제한

chmod 600 .env

.gitignore 파일도 반드시 생성한다.

echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore

STEP 5. Node.js 서버 코드 작성

nano server.js
// server.js — BE 서버 전체 코드
require('dotenv').config();
const express = require('express');
const { OpenAIClient, AzureKeyCredential } = require("@azure/openai");

const app = express();
app.use(express.json());

// Azure OpenAI 클라이언트 초기화
const client = new OpenAIClient(
  process.env.AZURE_OPENAI_ENDPOINT,
  new AzureKeyCredential(process.env.AZURE_OPENAI_KEY)
);

// 상태 확인 엔드포인트
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', time: new Date().toISOString() });
});

// AI 질의응답 엔드포인트
app.post('/api/ask', async (req, res) => {
  const { prompt } = req.body;

  // 입력 검증
  if (!prompt || prompt.trim() === '') {
    return res.status(400).json({ error: "질문을 입력해주세요." });
  }
  if (prompt.length > 1000) {
    return res.status(400).json({ error: "질문이 너무 깁니다. 1000자 이하로 입력해주세요." });
  }

  try {
    const result = await client.getChatCompletions(
      process.env.AZURE_OPENAI_DEPLOYMENT,
      [
        { role: "system", content: "당신은 도움이 되는 AI 어시스턴트입니다." },
        { role: "user", content: prompt }
      ],
      { maxTokens: 800, temperature: 0.7 }
    );

    const answer = result.choices[0].message.content;
    res.json({ answer });

  } catch (err) {
    console.error("OpenAI 오류:", err.message);

    // 오류 유형별 응답 분리
    if (err.statusCode === 401) {
      res.status(500).json({ error: "API 인증에 실패했습니다. 키를 확인하세요." });
    } else if (err.statusCode === 429) {
      res.status(429).json({ error: "요청이 너무 많습니다. 잠시 후 다시 시도해주세요." });
    } else {
      res.status(500).json({ error: "응답 처리 중 문제가 발생했습니다." });
    }
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
  console.log(`BE 서버 실행 중: 포트 ${PORT}`);
});

서버 동작 확인

node server.js
# 출력: BE 서버 실행 중: 포트 3000

# 다른 터미널에서 내부 테스트
curl -X POST http://localhost:3000/api/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt":"안녕하세요"}'
# {"answer":"안녕하세요! 무엇을 도와드릴까요?"}

STEP 6. PM2로 서버 상시 실행

SSH 세션이 끊겨도 서버가 계속 실행되도록 PM2를 설정한다.

sudo npm install -g pm2

# 서버 시작
pm2 start server.js --name "ai-server"

# 재부팅 후 자동 시작 등록
pm2 startup
# → 출력되는 명령어를 복사해서 한 번 더 실행 (필수)
pm2 save

# 상태 확인
pm2 status
pm2 logs ai-server

STEP 7. Azure NSG 포트 설정

Azure Portal → BE VM → 네트워킹 → 인바운드 포트 규칙 추가

VM 포트 소스 목적

FE VM 80, 443 Any 외부 웹 접근 허용
BE VM 3000 VirtualNetwork 내부망 통신만 허용

BE VM의 3000 포트는 반드시 VirtualNetwork 소스로만 허용한다. Any로 열면 외부에서 BE 서버에 직접 접근할 수 있게 되어 보안 구조가 무너진다.

STEP 8. FE VM — IIS Reverse Proxy 설정

FE VM(Windows)에서 IIS Manager를 열고 다음 모듈을 설치한다.

  • URL Rewrite — IIS 확장 모듈
  • ARR (Application Request Routing) — Reverse Proxy 처리

설치 후 FE VM의 웹 루트 폴더에 web.config 파일을 생성하거나 수정한다.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <!-- /api 요청은 BE 서버로 전달 -->
        <rule name="Proxy to Backend API" stopProcessing="true">
          <match url="^api/(.*)" />
          <action type="Rewrite"
                  url="http://172.16.1.4:3000/api/{R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

ARR 활성화 확인

IIS Manager → 서버 선택 → Application Request Routing Cache
→ 우측 Server Proxy Settings → Enable proxy 체크 → 적용

STEP 9. 프런트엔드 HTML 작성

FE VM의 IIS 웹 루트(C:\inetpub\wwwroot\)에 index.html을 생성한다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>Azure AI 챗봇</title>
  <style>
    body { font-family: sans-serif; max-width: 700px; margin: 60px auto; padding: 0 20px; }
    h1 { font-size: 1.4rem; color: #333; }
    textarea {
      width: 100%; height: 80px; padding: 10px;
      border: 1px solid #ccc; border-radius: 6px;
      font-size: 1rem; resize: vertical; box-sizing: border-box;
    }
    button {
      margin-top: 10px; padding: 10px 24px;
      background: #0078d4; color: white;
      border: none; border-radius: 6px;
      font-size: 1rem; cursor: pointer;
    }
    button:disabled { background: #aaa; cursor: not-allowed; }
    #result {
      margin-top: 24px; padding: 16px;
      background: #f5f5f5; border-radius: 6px;
      white-space: pre-wrap; line-height: 1.7;
      min-height: 60px; font-size: 0.95rem;
    }
    .error { color: #c00; }
  </style>
</head>
<body>
  <h1>Azure OpenAI 챗봇</h1>
  <textarea id="prompt" placeholder="질문을 입력하세요..."></textarea>
  <br>
  <button id="sendBtn" onclick="sendQuestion()">질문하기</button>
  <div id="result">응답이 여기에 표시됩니다.</div>

  <script>
    async function sendQuestion() {
      const prompt = document.getElementById('prompt').value.trim();
      const resultDiv = document.getElementById('result');
      const btn = document.getElementById('sendBtn');

      if (!prompt) {
        resultDiv.innerHTML = '<span class="error">질문을 입력해주세요.</span>';
        return;
      }

      btn.disabled = true;
      resultDiv.textContent = '응답을 기다리는 중...';

      try {
        // 상대경로 사용 — IIS가 내부 BE 서버로 전달
        const response = await fetch('/api/ask', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ prompt })
        });

        const data = await response.json();

        if (!response.ok) {
          resultDiv.innerHTML = `<span class="error">오류: ${data.error}</span>`;
        } else {
          resultDiv.textContent = data.answer;
        }

      } catch (err) {
        resultDiv.innerHTML = '<span class="error">서버와 연결할 수 없습니다.</span>';
      } finally {
        btn.disabled = false;
      }
    }

    // Enter + Ctrl 단축키 지원
    document.getElementById('prompt').addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && e.ctrlKey) sendQuestion();
    });
  </script>
</body>
</html>

4. 실습 중 발생한 문제와 해결 (Q&A)

Q1. Azure OpenAI 리소스를 만들려는데 "액세스가 제한되어 있습니다" 메시지가 나온다.

Azure OpenAI는 일반 Azure 서비스와 달리 별도 신청이 필요한 경우가 있다. 무료 체험 구독이나 학생 구독은 접근이 제한될 수 있다. https://aka.ms/oai/access에서 액세스를 신청하면 통상 1~2 영업일 내에 승인된다. 유료 구독으로 전환 후 재시도하면 대부분 해결된다.


Q2. 모델 배포 탭에서 GPT-4 모델이 목록에 나타나지 않는다.

지역(Region)마다 지원하는 모델이 다르다. Korea Central은 지원 모델이 제한적이다. OpenAI 리소스를 생성할 때 지역을 East US 또는 Sweden Central로 선택한 뒤 다시 시도하면 대부분 해결된다. 이미 잘못된 지역으로 생성했다면 리소스를 삭제하고 다시 만들어야 한다.


Q3. Node.js 서버를 실행했는데 ENOENT: .env 오류가 발생한다.

실행 위치와 .env 파일 위치가 다른 경우에 발생한다. node server.js를 실행하는 디렉토리와 .env 파일이 있는 디렉토리가 동일해야 한다.

# 현재 위치 확인
pwd

# .env 파일이 존재하는지 확인 (숨김 파일이므로 -a 옵션 필요)
ls -a

# 예: /home/azureuser/ai-server 에서 실행하고, 같은 폴더에 .env 존재해야 함

Q4. curl 테스트는 성공하는데 브라우저에서는 응답이 안 온다.

이 경우 대부분 IIS Reverse Proxy 설정 문제다. 다음 순서로 확인한다.

  1. IIS Manager에서 ARR Enable proxy가 체크되어 있는지 확인
  2. web.config의 BE IP 주소가 실제 BE VM의 사설 IP와 일치하는지 확인
  3. BE VM의 NSG에서 FE VM이 3000 포트로 접근할 수 있는지 확인
  4. FE VM에서 직접 curl 테스트: curl http://172.16.1.4:3000/api/health

Q5. API 호출 시 401 Unauthorized 오류가 발생한다.

API Key가 잘못되었거나 .env 파일이 제대로 로드되지 않은 경우다.

# .env가 제대로 로드되는지 임시로 확인
node -e "require('dotenv').config(); console.log(process.env.AZURE_OPENAI_KEY)"
# 키 값이 출력되면 정상, undefined이면 .env 경로 문제

Azure Portal에서 키를 재확인하고, 공백이나 줄바꿈 없이 복사했는지도 점검한다.


Q6. API 호출 시 404 Not Found 오류가 발생한다.

Deployment 이름이 코드와 실제 Azure 포털의 배포 이름이 다른 경우에 발생한다.

# .env의 DEPLOYMENT 이름 확인
cat .env | grep DEPLOYMENT

# Azure Portal → OpenAI 리소스 → 모델 배포 탭에서 실제 이름과 대조

배포 이름은 대소문자를 구분한다. My-GPT4와 my-gpt4는 다른 이름이다.


Q7. VM을 재시작했더니 서버가 꺼져 있다.

PM2 자동 시작 등록이 완료되지 않은 경우다. pm2 startup 명령어를 실행하면 화면에 추가로 실행해야 할 명령어가 출력된다. 그 명령어를 복사해서 한 번 더 실행해야 자동 시작이 등록된다.

pm2 startup
# 출력 예시:
# [PM2] To setup the Startup Script, copy/paste the following command:
# sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u azureuser --hp /home/azureuser

# 위 출력 명령어를 그대로 복사해서 실행 후
pm2 save

Q8. BE VM의 3000 포트를 Any로 열었는데 문제가 되는가?

보안 구조가 무너진다. BE 서버는 사설 IP로만 존재하는 것이 목적인데, 3000 포트를 Any로 개방하면 해당 포트에 공인 IP를 따로 부여하지 않아도 내부 경로를 통한 접근 시도에 노출될 수 있다. NSG 소스를 반드시 VirtualNetwork로 제한한다.

Azure Portal → BE VM → 네트워킹 → 인바운드 포트 규칙
소스: VirtualNetwork  ← Any 대신 이것으로 설정
대상 포트: 3000
프로토콜: TCP

5. 최종 정리 — 완성된 구조가 의미하는 것

이번 실습을 통해 구성한 구조와 배운 내용을 정리하면 다음과 같다.

완성된 전체 흐름

사용자 브라우저
  │  fetch('/api/ask')  ← 상대경로, OpenAI 주소나 키 없음
  ▼
FE VM — IIS (공인 IP)
  │  web.config Rewrite → http://172.16.1.4:3000/api/ask
  ▼
BE VM — Node.js (사설 IP, 내부망 전용)
  │  process.env.AZURE_OPENAI_KEY  ← 키는 서버 안에서만
  ▼
Azure OpenAI API (GPT 모델)
  │  AI 응답 생성
  ▼
사용자 화면에 텍스트 출력

이 실습에서 이해한 것들

공인 IP와 사설 IP의 차이는 단순한 주소 형식의 문제가 아니라, 네트워크 접근 범위와 보안 설계의 기초다. BE 서버를 사설 IP 전용으로 유지하는 것이 보안의 출발점이다.

IIS Reverse Proxy는 프런트 서버를 단일 외부 진입점으로 만들고, 내부 서버를 숨기는 역할을 한다. 이 패턴은 Nginx, Apache, Azure API Gateway에서도 동일하게 적용된다.

API Key를 .env에 보관하고 서버에서만 참조하는 방식은 클라우드 서비스를 운영할 때 가장 기본적인 자격증명 관리 원칙이다. 코드에 키를 직접 작성하거나 프런트에 노출하는 것은 어떤 이유로도 정당화되지 않는다.

BE 서버는 AI API를 호출하는 통로인 동시에, 입력 검증·인증·속도 제한·로그를 담당하는 보안 계층이다. 이 계층이 없으면 AI 서비스는 동작하더라도 운영할 수 없는 상태가 된다.

실제 서비스 관점에서 이 구조의 가치

이번 실습에서 구성한 패턴은 단순한 챗봇 예제에 그치지 않는다. 프런트 서버를 통한 단일 진입점, 내부망 서버 격리, 서버 측 API Key 관리는 실제 웹 서비스가 채택하는 기본 아키텍처와 동일하다. 이 구조를 한 번 직접 구성해본 경험이 이후 더 복잡한 시스템을 설계할 때 판단의 기준이 된다.


전체 체크리스트

Azure 설정
□ Azure OpenAI 리소스 생성 완료 (지역: East US 또는 Sweden Central)
□ GPT 모델 배포 완료 및 배포 이름 메모
□ Endpoint + API Key 복사 (Portal → 키 및 엔드포인트)

BE VM 설정
□ Node.js 설치 완료
□ .env 파일 생성 (Endpoint, Key, Deployment 이름 입력)
□ .gitignore에 .env 등록
□ server.js 작성 및 curl 내부 테스트 통과
□ PM2 설치 → pm2 start → pm2 startup → pm2 save
□ NSG 인바운드 3000 포트: 소스 VirtualNetwork으로 제한

FE VM 설정
□ IIS 설치 완료
□ URL Rewrite 모듈 설치
□ ARR 설치 및 Enable proxy 활성화
□ web.config에 /api → BE 사설 IP:3000 Rewrite 규칙 작성
□ index.html 배포 (fetch('/api/ask') 상대경로 사용 확인)

최종 확인
□ 브라우저에서 FE 공인 IP로 접속 → 챗봇 화면 표시
□ 질문 입력 → AI 응답 출력
□ 브라우저 개발자 도구(F12) 네트워크 탭 → API Key 노출 없음 확인
□ VM 재시작 후 서버 자동 실행 확인

소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 클라우드학습기