AWS 람다(lambda)

8 분 소요


0. 들어가면서

지금 참여하는 중인 프로젝트는 서버리스(serverless) 아키텍처를 채택 중이어서 클라우드 프론트(cloudfront), S3, API 게이트웨이, 람다(lambda)를 활용 중이다. 프로젝트 초반에 참여한 것은 아니기 때문에 시장에 빠르게 진입할 수 있고, 사용자가 적기 때문에 비용을 절약할 수 있다는 장점 때문에 이런 선택을 한 것 같다. 커리어 대부분을 스프링 프레임워크를 사용한 나로선 많은 공부가 되고 있다. 이번 글은 서버리스 아키텍처에서 백엔드(backend)의 핵심 역할을 하는 AWS 람다에 대해 알아보려 한다.

1. AWS Lambda

간략하게 AWS 람다에 대해 정리해 보았다. AWS 람다는 서버를 직접 프로비저닝(provisioning)하거나 유지 관리할 필요 없이 코드를 실행할 수 있게 해주는 서버리스(Serverless) 컴퓨팅 서비스다. 클라우드 환경에서 운영 체제, 컴퓨팅 용량, 스케일링, 보안 등 인프라의 모든 측면을 AWS가 전적으로 관리하므로, 개발자는 오직 애플리케이션의 비즈니스 로직 작성에만 집중할 수 있다. 간단히 말하면 코드만 업로드하면 AWS 람다가 그 기능을 실행해준다.

Lambda의 핵심 특징은 다음과 같다.

  • 이벤트 기반 실행: 람다는 독립적으로 계속 실행되는 것이 아니라 특정 이벤트가 발생했을 때 트리거되어 작동한다. 예를 들어, API 게이트웨이를 통한 HTTP API 요청, S3 버킷에 파일 업로드, DynamoDB 데이터베이스의 테이블 업데이트 등 다양한 AWS 서비스나 외부의 이벤트에 응답하여 자동으로 코드를 실행한다.
  • 비용 효율성: 프로비저닝 된 서버에 트래픽이 없더라도 비용을 지불하는 방식과 달리, 람다는 코드가 실제로 실행된 시간(1ms 단위)과 요청 수에 대해서만 요금이 청구된다. 코드가 실행되지 않을 때는 지불할 비용이 전혀 없다.
  • 자동 확장성과 고가용성: 들어오는 이벤트나 트래픽의 수에 맞춰 자동으로 컴퓨팅 리소스가 확장(스케일 아웃)된다. 또한, 다중 가용 영역(AZ)에 걸쳐 고가용성과 내결함성이 기본적으로 내장되어 있어 별도의 로드 밸런싱이나 이중화 설계에 신경 쓸 필요가 없다.
  • 다양한 런타임 지원: Node.js, Python, Java, Ruby, Go, Rust, C#(.NET) 등 다양한 프로그래밍 언어를 지원하며, 필요할 경우 사용자가 직접 사용자 지정 런타임(Custom Runtime)을 만들어 사용할 수도 있다.
  • 무상태(Stateless) 아키텍처: Lambda 함수는 개별 요청마다 안전하게 격리된 환경(MicroVM)에서 실행되며, 각 호출은 기본적으로 상태를 공유하지 않는 무상태로 동작한다. 단, 성능 최적화(콜드 스타트 방지)를 위해 AWS는 처리가 끝난 실행 환경을 일정 시간 유지하다가 다음 호출에 재사용(웜 스타트)하기도 한다.

2. Constraints

AWS 람다는 편리하지만, 여러 제약사항이 있다. 람다를 선택하기 전에 비즈니스 요건을 확인하고, 람다가 적합한 기술 스택인지 고려해봐야 한다. 먼저 컴퓨팅 리소스 제약 사항들을 살펴보자.

  • 메모리: 128MB에서 시작하여 최대 10,240MB(10GB)까지 1MB 단위로 할당할 수 있다.
  • CPU: CPU 용량은 사용자가 직접 제어할 수 없으며, 설정된 메모리 크기에 비례하여 자동으로 증가하여 할당된다.
  • 임시 스토리지(/tmp): 함수 실행 시 사용할 수 있는 임시 디스크 공간은 512MB에서 최대 10,240MB(10GB)까지 1MB 단위로 제공된다. 단, 이 공간은 환경이 유지되는 동안에만 일시적으로 보존된다.

함수의 처리 시간은 최대 15분(900초)으로 엄격하게 제한된다. 따라서 오랜 시간이 걸리는 장기 실행 프로세스에는 적합하지 않으며, 이 경우 작업을 더 작은 단위로 분할해야 한다. 람다는 실행 라이프사이클이 존재하며, 각 단계별로 시간 제약이 존재한다. 람다의 실행 라이프 사이클은 크게 다음과 같이 나뉜다.

  1. 초기화 단계(Init)
    • 람다가 함수 실행을 위해 환경을 준비하는 첫 단계다. 여기서는 세 가지 주요 작업이 수행된다.
    • 확장(Extensions) 시작
    • 런타임 부트스트랩 (언어 런타임 인터프리터를 메모리에 로드, 런타임 API 연결 설정, 필요한 환경 변수/자격 증명 로드, 함수 코드(.zip 또는 레이어)를 실행 환경으로 로드 등)
    • 개발자가 작성한 핸들러 외부의 정적 코드(핸들러 함수 외부 영역) 실행 - 초기화 작업은 기본적으로 최대 10초 이내에 완료되어야 한다. 단, 미리 환경을 데워두는 ‘프로비저닝 된 동시성’이나 ‘SnapStart’ 기능을 사용할 경우에는 130초 또는 최대 15분까지 허용된다. - 만약 이 단계에서 크래시가 나거나 10초를 초과하면, 람다는 다음번 첫 함수 호출 시에 초기화 단계를 다시 시도합니다.
  2. 호출 단계(Invoke)
    • 실제 비즈니스 로직이 수행되는 단계다. 람다는 런타임과 각 확장 프로그램에 Invoke 이벤트를 전송한다.
    • 사용자가 설정한 함수의 타임아웃 한도(최대 900초/15분) 내에 함수 본연의 코드와 확장 작업이 모두 완료되어야 한다.
    • 별도의 사후 처리 단계가 없으므로 총 실행 시간의 합이 설정된 타임아웃을 초과해서는 안 됩니다.
    • 처리 중에 타임아웃이나 크래시가 발생하면 람다는 실행 환경을 초기화(리셋)하여 다음 요청에 대비한다. 리셋될 때 런타임과 확장은 셧다운 되지만, /tmp 디렉터리의 임시 파일은 지워지지 않고 유지된다.
  3. 종료 단계(Shutdown)
    • 유지보수나 장기간 미사용으로 인해 람다가 실행 환경을 완전히 파기하기 직전의 단계다. 하나의 호출(Invoke)이 성공적으로 완료되었다고 해서 람다가 곧바로 종료 단계로 넘어가는 것은 아니다. 람다는 런타임과 확장의 처리가 끝나면 성능 향상을 위해 실행 환경의 상태를 동결(Freeze)한 채로 일정 시간 유지한다.
    • 등록된 확장 프로그램에 Shutdown 이벤트를 보내어 최종적인 정리(Cleanup) 작업을 할 수 있는 기회를 제공한다.
    • 전체 종료 단계는 최대 2초로 엄격하게 제한된다. 런타임이나 확장이 2초 내에 응답하지 않으면 람다는 강제 종료(SIGKILL) 신호를 보내 환경을 강제로 꺼버린다.
https://joudwawad.medium.com/aws-lambda-architecture-deep-dive-bef856b9b2c4


마지막으로 배포 패키지에 대한 사이즈 제약이 있다. 배포를 위한 .zip 아카이브 파일의 최대 크기는 50MB이다. 함수 코드 및 추가된 모든 레이어의 압축을 푼 후의 총 용량은 250MB를 초과할 수 없다. 더 큰 패키지가 필요한 경우 도커(docker) 등 컨테이너 이미지를 활용할 수 있으며, 이 경우 최대 10GB 크기까지 지원된다. 단일 람다 함수에 연결할 수 있는 레이어는 최대 5개까지만 허용된다.

3. AWS Lambda Architecture

AWS 람다의 아키텍처를 살펴보자. 우선 람다는 가상 머신(VM, Virtual Machine)에서 실행된다. 이 가상 머신을 MicroVM이라고 한다. MicroVM 내부를 보면 다음과 같은 구조를 갖는다.

  • 실행 환경 (Execution Environment)
    • MicroVM 내부에서 생성되며, 람다 함수가 실제로 실행될 때 필요한 리소스(메모리, 실행 시간 등)를 관리하는 안전하고 격리된 런타임 환경이다.
    • 이 환경 내에서 함수의 런타임과 등록된 확장(Extensions)들은 개별 프로세스로 실행되지만, 권한, 자격 증명, 환경 변수 및 리소스를 서로 공유한다. 512MB에서 10GB 사이의 용량을 가진 /tmp 임시 디스크 공간도 제공된다.
    • 함수 실행이 끝나면 람다는 오버헤드를 줄이기 위해 환경을 즉시 파기하지 않고 상태를 동결한 채로 유지하여 다음 호출 시 재사용(Warm Start)한다.
    • 재사용 시 핸들러 외부에 선언된 변수나 /tmp 디렉터리의 데이터가 그대로 보존되어 성능 최적화에 기여한다. 하지만 환경은 주기적으로 파기되므로 코드는 근본적으로 무상태(Stateless)로 작성되어야 한다.
  • 람다 런타임 (Lambda Runtime)
    • 실행 환경 내부에서 동작하며, 람다 서비스(혹은 데이터 플레인)와 개발자가 작성한 함수 코드 사이에서 호출 이벤트, 컨텍스트 정보, 응답을 중계하는 역할을 한다.
    • Node.js, Python, Java, Go 등 다양한 프로그래밍 언어를 지원(nodejs20.x, python3.13 등)하며, 필요한 경우 사용자가 직접 사용자 지정 런타임을 구성할 수 있다.
    • 런타임은 HTTP 기반의 런타임 API를 사용하여 람다 서비스와 통신한다. 이 API를 통해 다음 호출 이벤트를 가져오고(GET), 실행이 완료되면 응답을 반환(POST)하며, 실행 중 발생한 오류를 보고한다.
    • 이 런타임 환경과 최소화 된 게스트 OS 커널 등은 다른 함수와 공유되지 않고 오직 해당 함수 전용으로 제공됩니다
  • 함수 코드(Function Code)
    • 개발자가 작성하여 람다 실행 환경으로 가져오는 실제 비즈니스 로직 및 워크로드를 의미한다. 런타임이 이벤트를 받아주면 함수 코드가 이를 넘겨받아 실제 비즈니스 로직을 수행한다.
    • 함수 코드는 필요한 라이브러리 및 종속성과 함께 .zip 파일이나 컨테이너 이미지와 같은 배포 패키지(소스 번들)로 묶여 람다 환경에 업로드됩니다
https://joudwawad.medium.com/aws-lambda-architecture-deep-dive-bef856b9b2c4


AWS 람다 워커(Lambda Worker)는 수많은 MicroVM을 관리하고 호스팅하는 백엔드의 Amazon EC2 스팟(spot) 인스턴스다. 데이터 플레인(Data Plane)에서 함수가 호출될 때 이 워커에 실행 환경이 할당되어 코드가 처리된다. 각 워커는 최대 14시간의 임대 수명(lease lifetime)을 가지며, 이 수명에 도달하면 더 이상 새로운 함수 호출이 라우팅되지 않고 내부의 MicroVM들과 함께 안전하게 종료된다.

람다 워커에서 MicroVM을 실행시키는 것은 Firecracker라는 아마존이 Rust 언어로 직접 개발한 오픈 소스 가상화 기술이다. Firecracker는 가상 머신 모니터(VMM)로, 모든 Lambda 함수를 구동하는 핵심 엔진이다. KVM(커널 기반 가상 머신)을 활용해 하드웨어 가상화를 지원하며 수 밀리초 만에 부팅된다. 다중 테넌트 환경에서의 철저한 보안과 격리를 위해, 각 MicroVM은 오직 단일 함수 호출에만 독점적으로 할당된다.

https://joudwawad.medium.com/aws-lambda-architecture-deep-dive-bef856b9b2c4


AWS Lambda를 호출하는 방식은 다양하지만, 서비스 이면의 핵심적인 내부 아키텍처는 일관되게 유지된다. 모든 람다 호출의 중심이자 진입점 역할은 프런트엔드 서비스(Frontend Service)가 담당한다. 함수가 호출되면 프런트엔드 서비스는 요청을 관리하고 적절한 데이터 플레인 서비스로 전달하여 실행 프로세스를 시작하며, 호출 방식(동기식/비동기식)에 따라 처리 흐름이 달라진다.

  • 동기식 호출 (Synchronous Invocation): 프런트엔드 서비스가 즉각적인 처리를 위해 요청을 MicroVM으로 직접 라우팅한다.
  • 비동기식 호출 (Asynchronous Invocation): 프런트엔드 서비스가 요청을 즉시 처리하지 않고 Lambda 내부 대기열(Internal queue)에 보관한다. 이러한 내부 큐잉 메커니즘은 대기 중인 이벤트를 사용 가능한 MicroVM에 효율적으로 분배하는 역할을 하며, 트래픽이 몰리거나 수요가 급증하는 시기에도 원활하게 로드 밸런싱을 유지하고 성능을 최적화할 수 있다.
https://joudwawad.medium.com/aws-lambda-architecture-deep-dive-bef856b9b2c4


추가적으로 Kinesis, DynamoDB Streams와 같은 이벤트 소스 매핑(Event Source Mapping) 환경에서는 이 프런트엔드 서비스를 호출하기 위한 별도의 내부 구성 요소가 존재한다. 내부의 폴링 컨슈머(Polling Consumer)가 레코드를 지속적으로 읽어오고, 어그리게이터(Aggregator)가 크기나 시간 등의 기준에 맞춰 데이터를 배치 형태로 묶는다. 조건이 충족되면 인보커 프로세스(Invoker Process)가 프런트엔드 서비스에 동기식 호출을 시작하여, 최종적으로 MicroVM 내부에서 함수 코드가 실행되도록 유도한다.

https://joudwawad.medium.com/aws-lambda-architecture-deep-dive-bef856b9b2c4


MicroVM에 격리되어 실행되는 사용자 지정 함수람다 서비스(데이터 플레인)와 통신하기 위해 람다 런타임 HTTP API 엔드포인트를 활용한다. 런타임은 AWS_LAMBDA_RUNTIME_API 환경 변수에서 API 엔드포인트를 가져와 통신을 구성한다. 런타임 API는 주로 다음과 같은 핵심 HTTP 메서드를 통해 상호작용한다.

  • Next invocation (GET)
    • 런타임이 람다 서비스에 다음 호출 이벤트를 요청할 때 사용한다.
    • 응답에는 트리거된 이벤트 데이터를 담은 JSON 페이로드와 호출을 추적하기 위한 Request ID가 포함된다.
    • 람다가 런타임을 부트스트랩한 후 반환할 이벤트가 생길 때까지 런타임 프로세스가 몇 초간 동결(Freeze)된 상태로 대기할 수도 있다.
  • Invocation response (POST)
    • 함수의 실행이 완료된 후, 런타임이 람다 서비스로 성공적인 실행 결과(응답)를 반환할 때 사용한다.
    • 동기식 호출인 경우, 이 응답이 호출한 클라이언트에게 최종적으로 전달된다.
  • Initialization error (POST)
    • 함수 로드 실패 등 초기화(Init) 단계에서 런타임이 발생한 에러를 람다 서비스에 보고할 때 사용한다.
  • Invocation error (POST)
    • 이벤트 데이터 파싱 오류 등 함수 호출(Invoke) 처리 중 발생한 에러를 람다 서비스에 보고할 때 사용한다.
https://joudwawad.medium.com/aws-lambda-architecture-deep-dive-bef856b9b2c4

4. Lambda Layers

AWS 람다 레이어(Lambda Layer)는 람다 함수에 필요한 추가적인 코드나 데이터(라이브러리 종속성, 사용자 지정 런타임, 구성 파일 등)를 담고 있는 .zip 파일 형태의 아카이브(archive)다. 람다 함수에 레이어를 추가하면, 실행 시 람다 실행 환경 내부의 /opt 디렉터리에 해당 파일들이 자동으로 압축 해제되어 마운트(mount)된다. 람다 레이어를 사용하면 여러 가지 이점을 얻을 수 있다.

  • 동일한 라이브러리나 공통 코드를 여러 람다 함수에서 공유하여 사용할 수 있으므로 코드의 중복을 막고 관리 효율성을 높일 수 있다.
  • 크기가 큰 종속성 패키지를 레이어로 분리하면, 실제 함수의 배포 패키지가 가벼워져 업로드 및 배포 시간이 단축된다.
  • 애플리케이션의 핵심 비즈니스 로직과 종속성(라이브러리)을 분리하여, 함수 코드를 작고 목적에 집중하게 만들 수 있다.
  • 람다 배포 패키지 크기가 너무 크면 AWS 콘솔의 내장 코드 에디터를 사용할 수 없지만, 무거운 패키지를 레이어로 덜어내면 에디터를 통해 빠르게 코드를 수정하고 테스트할 수 있다.
  • AWS 환경에 임베드된 기본 SDK 버전은 예고 없이 변경될 수 있으나, 필요한 특정 버전을 레이어에 포함하면 SDK 버전을 안전하게 고정할 수 있다.
https://joudwawad.medium.com/aws-lambda-architecture-deep-dive-bef856b9b2c4


위에서 제약사항을 언급한 것처럼 하나의 람다 함수에는 최대 5개의 레이어까지만 추가할 수 있다. 함수 코드와 모든 레이어의 압축 해제 후 총합 크기는 최대 250MB를 넘을 수 없다. 레이어는 한 번 생성되면 변경할 수 없는(Immutable) 스냅샷 형태로 버전 관리가 이루어진다. 패키지를 수정해야 할 경우 새로운 zip 파일을 올려 새 버전을 생성해야 한다.

파이썬(Python)의 경우 압축 파일 내 최상위 폴더를 반드시 python으로 지정해야 런타임(PYTHONPATH)에서 정상적으로 라이브러리를 인식할 수 있다. 추가된 모든 레이어는 /opt라는 동일한 디렉터리 경로에 압축이 풀리므로, 만약 서로 다른 레이어에 이름이 같은 파일이 존재한다면, 설정된 순서에 따라 나중에 호출된 레이어가 이전 파일을 덮어쓰게 되므로 순서 지정에 주의해야 한다.

람다 함수는 Amazon Linux에서 실행되므로, 레이어에 포함되는 패키지들은 반드시 리눅스와 호환되는 형태로 빌드되어야 한다. GoRust 같은 컴파일 언어는 종속성을 포함해 하나의 실행 파일로 제공하는 것이 성능상 유리하다. 이들 언어에서 레이어를 통해 종속성을 분리할 경우, 초기화 단계에서 어셈블리를 수동으로 로드해야 하므로 오히려 콜드 스타트 지연 시간이 길어질 수 있어 권장되지 않는다.

REFERENCE

댓글남기기