개념과 호스팅 플랜을 짚고, 스토리지에 올라온 위험 파일을 자동으로 지우는 함수를 처음부터 끝까지 만들어본 기록
서버리스(Serverless)라는 단어를 처음 들었을 때 필자는 진짜로 서버가 한 대도 없는 구조인 줄 알았다. 그런데 이름이 좀 과장됐다. 서버가 사라진 게 아니라, 너무 잘 숨겨져 있어서 쓰는 사람이 서버의 존재나 개수를 신경 쓸 필요가 없어진 것이다. 추상화가 극단까지 올라간 형태라고 보면 된다.
예전에는 코드를 짜는 개발자와 서버를 24시간 3교대로 지키는 운영자가 따로 있었다. 그러다 클라우드로 넘어오면서 인프라 관리를 대형 벤더가 떠안았고, 그 결과 개발자가 필요한 코드만 올리면 되는 환경이 됐다. 서버리스는 그 흐름의 끝쪽에 있는 모델이다.
동작 원리 세 가지
- 함수 단위 배포 — 프로그램을 통째로 올리지 않는다. 기능을 함수(Function) 단위로 쪼개 짧고 단순한 것 하나만 구현해 올린다.
- 이벤트 기반 실행 — 평소엔 잠들어 있다가 요청·파일 업로드·DB 변화 같은 이벤트가 생기면 깨어난다. 함수를 깨우는 행위를 트리거(Trigger)라고 부른다.
- 사용한 만큼만 과금 — 요청이 올 때만 자원을 동적으로 받고 끝나면 회수한다. 아무도 안 쓰면 비용은 0이다. 사실상 이 한 줄이 서버리스 장점의 대부분이다.
세 회사, 거의 같은 서비스
대표 서비스는 세 개고, 기능만 보면 거의 비슷하다. 진짜 차이는 각자가 속한 클라우드 생태계에 있다.
서비스제공사특징
| Azure Functions | Microsoft | Cosmos DB·Service Bus 등 애저 PaaS와 긴밀하게 통합. 한글 검색은 애저 함수 앱 |
| AWS Lambda | Amazon | S3·DynamoDB 등 AWS 인프라 연동에 강함. Go·Ruby까지 추가 지원 |
| Google Cloud Functions | Cloud Storage·Pub/Sub 이벤트에 반응시키기 좋게 설계됨 |
결국 서버리스 하나만 보고 고르는 게 아니라, 어느 클라우드 생태계 안에서 일할지가 선택을 결정한다.
Azure Functions — 트리거와 바인딩
트리거는 HTTP 요청, 타이머, 파일 업로드, 큐, Event Grid·Event Hub 등 다양하다. 그중 큐(Queue) 패턴이 제일 와닿았다. 웹 서버가 초당 1000건을 받는데 뒤의 DB가 초당 100건밖에 못 쓰면 데이터가 새어나간다. 그래서 중간에 메시지 큐를 두고 순서대로 쌓아두면, Function이 큐에 데이터가 들어오는 이벤트를 감지해서 DB가 소화할 속도에 맞춰 하나씩 안전하게 밀어 넣는다.
연결에는 바인딩(Binding)이 있다. 입력 바인딩은 다른 서비스에서 데이터를 받아오는 연결, 출력 바인딩은 결과를 내보내는 연결이다. 핵심은 이걸 쓰면 서비스 간 연결 코드를 직접 안 짜도 된다는 것. 코드가 짧아지고 실수할 여지도 준다.
호스팅 플랜 — 콜드 스타트 vs 돈
플랜 선택은 따지고 보면 전부 콜드 스타트(Cold Start)와 비용 사이 어디에 자리를 잡느냐의 문제다.
플랜상태콜드 스타트비용
| Consumption(사용량) | 요청 시 활성화 | 있음 | 쓴 만큼 |
| App Service | 항상 켜짐 | 없음 | 고정(상시 발생) |
| Flex 사용량 | 일부 사전 프로비전 | 거의 없음 | 효율적 |
| Premium | 전용 자원 상시 | 없음 | 비쌈 |
그리고 모든 서비스에는 SLA(서비스 수준 협약)가 붙는다. Azure Functions는 기본 99.95%를 보장한다. 1년에 0.05% 정도의 다운타임만 허용한다는 뜻인데, 하필 그 순간이 내 서비스가 필요할 때일 수 있으니 같은 함수를 이중화하거나 다른 지역에도 배포해두는 우회를 아키텍처 단에서 고민해야 한다.
실습: 스토리지에 올라온 위험 파일 자동 삭제
여기서부터가 이번 주의 본론이다. 시나리오는 단순하다. 스토리지에 파일이 업로드되면 위험한 파일인지 판단하고, 위험하면 바로 삭제한다. 트리거는 Blob Trigger를 쓴다. 단계별로 쌓아 올려보자.
0. 위험 파일을 어떻게 판단할까
위험 파일은 대체로 실행 가능한 것들이다. .exe 실행 파일, .bat .vbs 같은 스크립트가 여기 들어간다. 감지 방법은 두 가지다.
- 확장자 검사 — 가장 간단하다. 그런데 이름만 바꾸면 위장이 가능하다.
- 헤더(메타데이터) 검사 — 파일 내부 앞부분 바이트를 직접 읽어 위장한 실행 파일을 잡는다.
그래서 실습에서는 확장자(1차) → 헤더(2차) 순서로 두 번 거른다.
1. 함수 앱 & 타겟 스토리지 만들기
Azure 포털에서 함수 앱을 검색해 새로 만든다. 요금제는 사용량(Consumption), 런타임 스택은 Python, 지역은 코리아 센트럴로 잡았다.
여기서 헷갈리기 쉬운 부분 하나. 함수 앱이 구동에 쓰는 스토리지와, 실제로 파일을 올려두고 감시할 타겟 스토리지는 별개다. 타겟용 스토리지 계정을 따로 만들고(중복 옵션은 최소인 LRS), 그 안에 Blob 컨테이너 mycontainer를 생성한다.
2. VS Code에서 Blob Trigger 프로젝트 생성
포털 편집기로도 되긴 하지만 라이브러리를 붙이기 시작하면 금방 답답해진다. 실습은 VS Code로 진행했다. 명령 팔레트(Ctrl+Shift+P)에서 프로젝트를 만든다.
Azure Functions: Create New Project
→ Language : Python
→ Model : v2
→ Version : 3.11
→ Template : Azure Blob Storage trigger
→ Name : BlobTrigger
→ Path : mycontainer
CLI로는 func init AppDev --python → func new --template "Azure Blob Storage trigger" 와 같다.
이러면 기본 골격이 잡힌다. 트리거가 제대로 걸리는지부터 확인하는 게 먼저다.
import azure.functions as func
import logging
app = func.FunctionApp()
@app.blob_trigger(arg_name="myblob",
path="mycontainer/{name}",
connection="target_storage")
def BlobTrigger(myblob: func.InputStream):
logging.info(f"파일 감지: {myblob.name}, 크기: {myblob.length} bytes")
3. 스토리지 연결 (local.settings.json)
위 코드의 connection="target_storage"가 실제 어떤 스토리지를 가리키는지 알려줘야 한다. 포털의 타겟 스토리지 → 보안 + 네트워킹 > 액세스 키에서 연결 문자열을 복사해 로컬 설정에 넣는다.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=helloaifuncstorage;AccountKey=...;EndpointSuffix=core.windows.net",
"FUNCTIONS_WORKER_RUNTIME": "python",
"target_storage": "DefaultEndpointsProtocol=https;AccountName=mytargetstorage;AccountKey=...;EndpointSuffix=core.windows.net"
}
}
local.settings.json — AzureWebJobsStorage는 함수 구동용, target_storage는 우리가 감시할 스토리지다.
이 상태에서 F5로 실행하고 포털의 mycontainer에 파일을 올리면 터미널에 "파일 감지" 로그가 찍힌다. 여기까지가 토대다.
4. 1차 — 확장자 검사
Blob 경로는 컨테이너이름/파일명 형태로 넘어온다. os 라이브러리로 파일명과 확장자만 떼어내 비교한다.
import os
restricted_extensions = ['.exe', '.bat', '.vbs']
@app.blob_trigger(arg_name="myblob",
path="mycontainer/{name}",
connection="target_storage")
def BlobTrigger(myblob: func.InputStream):
name = os.path.basename(myblob.name) # 경로에서 파일명만
ext = os.path.splitext(name)[1].lower() # 확장자만, 소문자로
if ext in restricted_extensions:
logging.warning(f"위험 확장자 감지: {name}")
# → 여기서 삭제 (6단계에서 연결)
위협이 잡히면 일반 로그가 아니라 경고(Warning) 레벨로 띄운다. 터미널에서 노란색으로 떠서 눈에 잘 띈다.
5. 2차 — 파일 헤더 검사
확장자는 virus.exe를 report.txt로 이름만 바꾸면 1차를 그냥 통과한다. 그래서 파일 앞 4바이트를 읽어 실제 내용을 본다. 앞 2바이트가 바이너리 b'MZ'로 시작하면 확장자와 무관하게 윈도우 실행 파일이다. 운영체제가 실행 파일을 판별하는 방식 그대로다.
header = myblob.read(4) # 앞 4바이트 읽기
if header[:2] == b'MZ': # MZ로 시작 → 실행 파일
logging.warning(f"실행 파일 헤더(MZ) 감지: {name}")
# → 삭제
디버깅 중 브레이크 포인트를 걸고 변수에 마우스를 올리면 헤더가 실제로 b'MZ\x90\x00' 형태로 들어와 있는 걸 눈으로 확인할 수 있다. Step Over / Step Into로 한 줄씩 따라가 보면 감이 확실히 잡힌다.
6. 위협 파일 삭제 (delete_blob)
삭제는 azure-storage-blob 패키지의 BlobServiceClient로 한다. 먼저 requirements.txt에 패키지를 추가한다.
azure-functions
azure-storage-blob
그다음 컨테이너 이름과 파일 이름을 받아 삭제하는 함수를 따로 만든다. 연결 문자열은 환경 변수에서 가져오고, 파일 작업은 언제든 깨질 수 있으니 try-except로 감싼다.
from azure.storage.blob import BlobServiceClient
def delete_blob(container, blob_name):
conn = os.getenv('target_storage') # 환경 변수에서
service = BlobServiceClient.from_connection_string(conn)
blob_client = service.get_blob_client(container=container, blob=blob_name)
try:
blob_client.delete_blob()
logging.warning(f"위협 파일 삭제 완료: {blob_name}")
except Exception as e:
logging.error(f"삭제 실패: {e}")
완성된 function_app.py
두 검사와 삭제를 합치면 이렇게 된다. 감지되면 delete_blob을 부르고, 더 진행할 이유가 없으니 return으로 빠져나온다.
import os
import logging
import azure.functions as func
from azure.storage.blob import BlobServiceClient
app = func.FunctionApp()
restricted_extensions = ['.exe', '.bat', '.vbs']
def delete_blob(container, blob_name):
conn = os.getenv('target_storage')
service = BlobServiceClient.from_connection_string(conn)
blob_client = service.get_blob_client(container=container, blob=blob_name)
try:
blob_client.delete_blob()
logging.warning(f"위협 파일 삭제 완료: {blob_name}")
except Exception as e:
logging.error(f"삭제 실패: {e}")
@app.blob_trigger(arg_name="myblob",
path="mycontainer/{name}",
connection="target_storage")
def BlobTrigger(myblob: func.InputStream):
name = os.path.basename(myblob.name)
ext = os.path.splitext(name)[1].lower()
# 1차: 확장자 검사
if ext in restricted_extensions:
logging.warning(f"위험 확장자 감지: {name}")
delete_blob("mycontainer", name)
return
# 2차: 헤더 검사 (확장자 위장 대비)
header = myblob.read(4)
if header[:2] == b'MZ':
logging.warning(f"실행 파일 헤더(MZ) 감지: {name}")
delete_blob("mycontainer", name)
return
logging.info(f"안전한 파일: {name}")
로컬에서 F5로 돌려놓고 test.exe를 업로드하면, 트리거 → 헤더 감지 → 삭제까지 로그로 쭉 찍히고 컨테이너에서 파일이 사라진다.
7. 클라우드 배포와 환경 변수 — 여기서 한 번 막혔다
VS Code의 Azure 확장에서 함수 앱을 우클릭해 Deploy to Function App을 누르면 프로젝트가 zip으로 묶여 배포된다. 그런데 로컬에선 잘 돌던 게 클라우드에선 동작을 안 했다. 원인은 단순했다.
local.settings.json은 배포할 때 같이 올라가지 않는다. 그래서 클라우드에서는 포털의 함수 앱 → 설정 > 환경 변수로 들어가 코드가 쓰는 키를 직접 등록해줘야 한다.
이름 : target_storage
값 : DefaultEndpointsProtocol=https;AccountName=mytargetstorage;AccountKey=...;EndpointSuffix=core.windows.net
이걸 넣고 저장한 뒤 다시 파일을 올리니 그제서야 클라우드에서도 함수가 트리거되고 위협 파일이 삭제됐다. 배포 후엔 포털에서 코드 편집이 막히니 수정은 VS Code로 돌아와서 해야 한다는 점도 같이 챙겨두면 좋다.
정리하면
서버리스는 Azure Functions 하나를 가리키는 말이 아니라, 추상화가 높은 여러 서비스를 묶는 사고방식에 가깝다. Functions로 이벤트를 받고, 큐로 흐름을 조절하고, Blob에 저장하고, Logic App으로 알림을 보내는 식으로 레고 블록처럼 쌓아 올리면 인프라를 직접 만지지 않고도 꽤 단단한 시스템이 나온다.
이론만 들었으면 그냥 흘려보냈을 텐데, 위험 파일 삭제 함수를 처음부터 끝까지 굴려보니 트리거-바인딩-환경 변수가 어떻게 맞물리는지 비로소 손에 잡혔다. 특히 환경 변수에서 한 번 막혀본 게 제일 오래 남을 것 같다.
'볼봇의 코딩 생활 > 웹 세계' 카테고리의 다른 글
| [클라우드응용SW개발] 컨테이너 배포와 Azure AI 서비스 (0) | 2026.06.08 |
|---|---|
| [클라우드응용SW개발] Azure 와 Streamlit 으로 ToDo 앱 만들기 (0) | 2026.04.21 |
| [클라우드응용SW개발] 관계형 DB부터 Cosmos DB까지 (0) | 2026.04.07 |
| [클라우드응용SW개발] Azure 에서 해보는 클라우드 인프라 (0) | 2026.03.24 |
| Centos 6 nginx 와 php 연동 하기 (0) | 2019.03.27 |