오픈소스이며 HIPAA 적격인 Twilio 대안

발행: (2026년 1월 7일 오전 05:46 GMT+9)
13 min read
원문: Dev.to

Source: Dev.to
Note: 최신 설정 지침은 저장소의 open-telephony-stack/README.md를 참조하세요.

왜 우리가 이것을 만들었는가

지난 여름 우리는 의료 기관을 위한 AI 음성 에이전트를 구축하고 있었습니다. 우리는 다음이 필요했습니다:

  • 전화를 걸고 받기
  • 실시간으로 오디오 스트리밍
  • HIPAA 준수 유지

Twilio는 명백한 선택처럼 보였지만, 월 $2,000이라는 비즈니스 어소시에이트 계약(BAA) 비용 장벽에 부딪혔습니다. 한 번도 전화를 걸어보지 않은 스타트업에게는 이 가격이 너무 높았습니다.

그래서 우리는 자체 스택을 만들었습니다:

ComponentWhat it does
Asterisk오픈‑소스 PBX (Docker화)
AWS Chime SDKSIP 트렁킹 및 전화번호
FastAPI shim레거시 전화 시스템을 최신 WebSocket API와 연결

그 결과 완전하고 안전한 전화 시스템이 탄생했으며, 인바운드 아웃바운드 전화를 모두 처리할 수 있습니다:

  • AWS Chime Voice Connector(실제 PSTN 번호)를 통해 전화를 수신
  • Asterisk(Docker)에서 SIP/TLS 종료
  • RTP를 통해 오디오를 WebSocket 연결로 브리지
  • base64 μ‑law 오디오를 AI 음성 서버로 스트리밍
  • Twilio‑like WebSocket API 제공 (Twilio Media Streams 모델링)

AI는 여러분이 직접 가져오면 되고, 스택은 전화 인프라만 담당합니다.

Use‑case examples

ScenarioWhy this stack helps
Healthcare AI – Twilio의 BAA 비용 없이 HIPAA 준수가 필요함추가 준수 비용이 없으며, 데이터를 직접 제어합니다
Custom call handling – Twilio가 제한합니다다이얼플랜, 미디어 라우팅 등을 완전하게 제어합니다
Full stack ownership – 모든 레이어를 직접 소유하고 싶음셀프 호스팅, 오픈소스, 벤더 종속 없음
Learning/experimenting – 전화 시스템 내부를 이해하고 싶음PSTN부터 WebSocket까지 엔드‑투‑엔드, 모두 코드로 구현

Consider alternatives if:

  • 사이드 프로젝트에 기본 음성만 필요한다면 (Twilio가 더 쉽습니다).
  • 인프라를 관리하고 싶지 않다면.
  • 특별한 준수 요구사항이 없다면.

Trade‑off: 이 스택을 관리하려면 시간과 지속적인 유지보수가 필요합니다.

서비스 포트

서비스포트프로토콜설명
Asterisk SIP5061TCP/TLSAWS Chime과의 SIP 신호
Asterisk ARI8088HTTPAsterisk REST 인터페이스 (localhost 전용)
Shim server8080HTTPFastAPI 서버, 상태 확인 엔드포인트
RTP media10000‑10299UDPAsterisk와의 오디오 스트림

아키텍처 개요

1. AWS Chime Voice Connector

  • PSTN 게이트웨이. 여기서 전화번호를 프로비저닝합니다.
  • 전화는 SIP/TLS5061 포트에서 들어옵니다.

2. Asterisk PBX (Docker)

  • SIP 시그널링, RTP 미디어, 콜 라우팅을 처리합니다.
  • 기존 다이얼플랜 스크립팅 대신 ARI (Asterisk REST Interface)를 사용합니다.

3. Shim server (FastAPI)

FunctionDetails
ARI WebSocket을 통해 Asterisk에 연결StasisStart 이벤트를 수신
ExternalMedia 채널 생성RTP를 AI 음성 서버와 브리지
20 ms RTP 캔디스 유지WebSocket 지터를 격리
오디오 전달Base64 μ‑law 페이로드를 하위 음성 서버로 전송

4. Your AI voice server

  • Twilio‑호환 WebSocket 미디어 포맷을 지원하는 모든 서버 (예: OpenAI Realtime, AWS Nova Sonic, 커스텀 ASR/TTS).
  • 샘플 구현: open-telephony-stack/src/servers/voice_agent_server.py.

DNS 및 TLS 설정

DNS 레코드 (TLS 이전에 필요)

Record typeNameValueTTL
Asip.yourdomain.com귀하의 Elastic IP (예: 54.123.45.67)300 (또는 기본값)

이 A 레코드를 생성하기 전에:

  • Let’s Encrypt 인증서 요청 (Certbot이 도메인 소유권을 검증)
  • AWS Chime Voice Connector 종료 구성 (Chime이 호스트명을 확인해야 함)
  • pjsip.conf에서 external_signaling_address 설정 (DNS 이름과 일치해야 함)

레코드를 추가한 후에는 전파가 완료될 때까지 기다리세요 (몇 분에서 48시간). 다음 명령으로 확인합니다:

dig sip.yourdomain.com
# or
nslookup sip.yourdomain.com

Let’s Encrypt를 이용한 TLS

  • Certbot은 EC2 인스턴스에서 실행되며 포트 80에 바인딩됩니다.
  • 인증서는 sip.yourdomain.com에 대해 발급됩니다.
  • Asterisk는 Docker 볼륨 마운트를 통해 /etc/letsencrypt/live/...에서 인증서를 읽습니다.
  • 갱신 훅은 인증서가 교체될 때 Asterisk를 재로드합니다.
  • Chime은 Let’s Encrypt의 CA 루트와 비교하여 인증서를 검증합니다 – 자체 서명 인증서 없음, 수동 갱신 필요 없음, 예상치 못한 만료 없음.

전화 흐름 (누군가 번호를 걸면 무슨 일이 일어나는지)

  1. Caller가 AWS Chime 전화번호를 다이얼합니다.

  2. Chime이 SIP INVITE를 귀하의 Asterisk 서버(TLS:5061)에 보냅니다.

  3. Asteriskextensions.conf에서 전화를 매칭합니다

    Answer()
    Stasis(voice-agent)
  4. ARI가 WebSocket을 통해 shim 서버에 StasisStart 이벤트를 보냅니다.

  5. Shim server가 다음 단계를 수행합니다:

    a. 음성 서버에 WebSocket을 엽니다.
    b. ARI 믹싱 브리지를 생성합니다.
    c. PSTN 채널을 브리지에 추가합니다.
    d. RTP용 UDP 포트(10000‑10299)를 할당합니다(실시간 통화마다 고유 포트를 사용합니다).
    e. 해당 포트를 가리키는 ExternalMedia 채널을 생성합니다.
    f. ExternalMedia 채널을 브리지에 추가합니다.

  6. 오디오 흐름:

    PSTN ↔ Bridge ↔ ExternalMedia ↔ Shim (RTP) ↔ Voice Server (WSS)
  7. 통화 종료 (발신자가 전화를 끊거나 또는 AI가 ARI 도구 호출을 통해 통화를 종료함):

    • ARI가 ChannelHangupRequest / ChannelDestroyed를 보냅니다.
    • Shim이 정리합니다: WebSocket을 닫고, 브리지를 삭제하고, RTP 포트를 해제합니다.

구성 파일

All config files live under deployment/asterisk-server/asterisk-config/. The Docker container mounts this directory.

pjsip.conf – SIP 트렁크 구성

가장 중요한 파일입니다. It defines the SIP trunk to AWS Chime, including transport settings, TLS certificates, inbound/outbound endpoints, and the external_signing_address that must match the DNS name you created.

(The rest of the repository contains additional config files, Docker Compose files, and example scripts.)

Source:

AWS Chime SDK + Asterisk Shim – Quick‑Start Guide

아래는 원본 마크다운을 정리한 버전입니다. 모든 헤딩, 코드 블록, 표 및 글머리표는 가독성을 위해 포맷팅했으며, 원본 내용은 그대로 유지했습니다.


Overview

FilePurpose
pjsip.confSIP 전송, TLS 설정 및 Chime Voice Connector 호스트.
extensions.conf최소 다이얼플랜 – 호출을 ARI Stasis 애플리케이션(voice‑agent)으로 라우팅.
ari.confAsterisk REST Interface(ARI) 인증 정보.
http.confARI용 내장 HTTP 서버(localhost에 바인드되어 보안 강화).
rtp.confRTP 미디어 스트림을 위한 UDP 포트 범위(기본 10000‑10299).
modules.conf필요한 모듈만 로드: PJSIP, ARI, μ‑law 코덱.

Notes

  • external_signaling_address는 DNS 이름 TLS 인증서와 일치해야 합니다.
  • local_net은 NAT 처리 시 Asterisk가 “내부”와 “외부”를 구분하도록 알려줍니다.
  • verify_server=no는 Chime이 클라이언트 인증서를 제공하지 않기 때문입니다.
  • cert/key 파일은 TLS 핸드셰이크 중 Asterisk가 Chime에 제공하는 인증서입니다.

Prerequisites

  • AWS 계정
  • EC2 인스턴스 (권장 t3.medium 이상, Amazon Linux 2023)
  • Elastic IP – EC2 VM에 연결 (Chime Voice Connector는 고정 IP가 필요합니다)
  • 도메인 이름 – Elastic IP를 가리키는 A 레코드 포함
  • DockerDocker Compose가 인스턴스에 설치되어 있어야 함

Configure a Chime Voice Connector

  1. AWS Chime SDK 콘솔을 엽니다.
  2. Voice Connector를 생성하거나 기존 것을 편집합니다.
SettingValue
Hostsip.yourdomain.com
Port5061
ProtocolTLS
  1. Voice Connector 호스트명을 메모합니다 – pjsip.conf에 필요합니다.

Obtain a TLS Certificate (Let’s Encrypt)

# Install certbot
sudo yum install -y certbot

# Request a certificate (port 80 must be open)
sudo certbot certonly --standalone \
  --preferred-challenges http \
  -d sip.yourdomain.com \
  --agree-tos -m your@email.com

# Enable automatic renewal
sudo systemctl enable --now certbot-renew.timer

# Create a renewal hook that reloads Asterisk inside Docker
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-asterisk.sh > /dev/null

이 저장소에는 Lambda 함수도 포함되어 있어, AWS가 AMAZON, EC2, CHIME_VOICECONNECTOR 서비스에 대한 새로운 IP 범위를 발표할 때마다 보안 그룹을 자동으로 업데이트합니다.

Deploy the Asterisk Server (Docker)

# Change to the Docker deployment directory
cd deployment/asterisk-server

# -------------------------------------------------
# Edit the configuration files as needed:
#   - pjsip.conf : domain, cert paths, voice‑connector host
#   - ari.conf   : secure ARI username/password
#   - rtp.conf   : adjust port range if required
# -------------------------------------------------

# Start the Asterisk container
docker-compose up -d

# Follow the logs
docker logs -f asterisk-server

# Open an interactive Asterisk CLI
docker exec -it asterisk-server asterisk -rvvvvv

Prepare the Shim Server

Create an .env file

cat > .env <<'EOF'
ARI_BASE=http://127.0.0.1:8088/ari
ARI_USER=ariuser
ARI_PASS=your-secure-password-here
ARI_APP=voice-agent
EXTERNAL_MEDIA_HOST=127.0.0.1
ECS_MEDIA_WSS_URL=wss://your-voice-server.internal/voice/voice
RTP_PORT_START=10000
RTP_PORT_END=10299
EOF

Build & Run the Shim

# Build the shim Docker image
docker build -t asterisk-shim -f deployment/shim-server/Dockerfile .

# Run the shim (host network so it can bind to the RTP ports)
docker run -d --env-file .env --network host --name asterisk-shim asterisk-shim

# Verify the shim health endpoint
curl http://localhost:8080/health

Test the End‑to‑End Flow

  1. AWS Chime 전화 번호(Voice Connector에 할당된 번호)로 전화를 겁니다.
  2. 전화가 Asterisk에 도달하면 voice-agent ARI 애플리케이션이 호출을 받아 Shim을 통해 미디어를 라우팅합니다.
  3. 로그와 ARI 대시보드에서 이벤트 흐름을 확인하고, 필요에 따라 다이얼플랜이나 Shim 설정을 조정합니다.

다음 파트에서는 Shim 서버의 상세 구현고급 트러블슈팅 방법을 다룹니다. 계속해서 읽어 주세요.

Source:

  1. 로그를 확인하세요:
# Asterisk logs (SIP/RTP activity)
docker logs -f asterisk-server

# Shim server logs (session lifecycle)
docker logs -f asterisk-shim

다음과 유사한 항목이 표시됩니다:

INVITE received
CallSession created
ExternalMedia channel established

WebSocket API (Shim ↔ Voice Server)

이 API는 Twilio Media Streams를 그대로 반영합니다 – 동일한 이벤트 구조와 μ‑law 오디오 포맷을 사용합니다.

오디오 포맷

속성
코덱μ‑law (PCMU)
샘플 레이트8000 Hz
프레임 크기160 bytes (20 ms)
인코딩Base64

이벤트 페이로드

start (shim → voice server)

{
  "event": "start",
  "start": {
    "streamSid": "unique-stream-id",
    "callSid": "asterisk-channel-id",
    "customParameters": {
      "source": "asterisk-shim",
      "format": "ulaw"
    }
  }
}

media (양방향)

{
  "event": "media",
  "streamSid": "unique-stream-id",
  "media": {
    "payload": "base64-encoded-ulaw-audio",
    "timestamp": 1234
  }
}

clear (voice server → shim)

{ "event": "clear" }

mark (양방향)

{
  "event": "mark",
  "streamSid": "unique-stream-id",
  "mark": { "name": "responsePart" }
}

stop (양쪽 모두)

{
  "event": "stop",
  "streamSid": "unique-stream-id"
}

샘플 구현

리포지토리에는 최소 예제인 **voice_agent_server.py**가 포함되어 있습니다. 이 예제는 다음을 시연합니다:

  • 위의 WebSocket 이벤트 처리
  • 실시간 오디오 처리
Back to Blog

관련 글

더 보기 »

왜 우리는 모니터링 통계를 공개했는가

대부분의 모니터링 서비스는 숫자를 숨깁니다. 우리는 반대로 하기로 했습니다. 여기에서 Boop이 현재 어떻게 수행되고 있는지 정확히 볼 수 있습니다 – 분당 체크 수, 지역…