Article

AWS Lambda에 C 라이브러리 의존성 포함시키기

도입

AWS Lambda 함수를 개발할 때 흔히 마주치는 문제가 있습니다. 로컬 컴퓨터(맥이나 윈도우)에서는 정상 동작하지만, Linux 기반의 Lambda 환경에서는 실패하는 경우입니다. 특히 jpegoptim, imagemagick 같은 외부 도구를 사용할 때 더 심합니다.

원인은 간단합니다. 이런 도구들은 C 언어로 컴파일된 바이너리로, 각각 많은 동적 라이브러리(.so 파일) 의존성을 가집니다. 도구 자체만 Lambda에 포함시키면 안 되고, 그 도구가 필요로 하는 모든 라이브러리도 함께 포함시켜야 합니다.

Lambda 함수 로컬 테스트하기

배포 전에 로컬 환경에서 정확하게 테스트해야 합니다. Lambda는 Linux 기반이므로 Docker를 이용해서 재현합니다.

Lambda 환경 Docker로 시뮬레이션

# Node.js 18 Lambda 환경 실행
docker run -it -p 8080:8080 -v $PWD:/var/task public.ecr.aws/lambda/nodejs:18 app.handler

# 로컬에서 테스트
curl localhost:8080/2015-03-31/functions/function/invocations \
  -d '{"payload": true}'

Python이라면 public.ecr.aws/lambda/python:3.11 이런 식으로 바꾸면 됩니다.

동적 라이브러리 의존성 파악하기

예를 들어 이미지 최적화를 위해 jpegoptim이 필요하다고 하겠습니다. 먼저 필요한 모든 라이브러리를 파악해야 합니다.

Ubuntu Docker에서 확인

Lambda 환경과 동일한 Linux 환경을 준비합니다.

# Ubuntu 최신 버전 실행
docker run -it ubuntu:latest /bin/bash

# 컨테이너 내부에서 실행
apt update
apt install jpegoptim

# jpegoptim이 의존하는 모든 라이브러리 확인
ldd $(which jpegoptim)

출력 예시:

linux-vdso.so.1 (0x00007fffbffdc000)
libjpeg.so.8 => /lib/x86_64-linux-gnu/libjpeg.so.8 (0x00007f1234567000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1234389000)
... (더 많은 라이브러리들)

의존성 처리 방식

여기서 두 가지 선택지가 있습니다:

방식장점단점
동적 라이브러리 함께 패킹간단하고 안정적파일이 많을 수 있음
정적 링크로 재컴파일파일 수 최소화복잡, 빌드 실패 가능

동적 라이브러리 방식이 더 안전하고 실무적입니다. 문제는 때로 수백 개의 라이브러리를 수동으로 처리해야 한다는 것입니다.

Exodus로 자동화하기

exodus는 바이너리와 그 의존성을 자동으로 모아주는 Python 도구입니다.

Exodus 설치 및 실행

Ubuntu Docker 내부에서:

# Python과 pip 설치
apt install -y python3 python3-pip

# exodus 설치
pip3 install --user exodus-bundler

# PATH에 추가
export PATH="${PATH}:${HOME}/.local/bin"

# jpegoptim의 모든 의존성을 자동 수집 및 압축
exodus -t -o jpegoptim.tar.gz $(which jpegoptim)

이 명령은 jpegoptim.tar.gz 파일을 생성하는데, 여기에 jpegoptim 바이너리와 모든 필요한 라이브러리가 포함되어 있습니다.

압축 파일을 로컬로 복사

Docker 컨테이너에서 생성된 파일을 호스트 머신으로 복사합니다:

# 다른 터미널에서 컨테이너 ID 확인
docker ps

# 파일 복사
docker cp <CONTAINER_ID>:/root/jpegoptim.tar.gz .

# 압축 해제
tar xzf jpegoptim.tar.gz

압축을 풀면 exodus 폴더가 생성되고, 그 안의 bin 폴더에 실행 가능한 바이너리들이 있습니다.

exodus/
├── bin/
│   └── jpegoptim    # 실행 가능한 바이너리
└── lib/
    ├── libjpeg.so.8
    ├── libc.so.6
    └── ... (모든 라이브러리들)

Lambda 패키징

이제 Lambda 배포 패키지를 구성합니다:

lambda-package/
├── app.py           # Lambda 함수 코드
├── requirements.txt  # Python 의존성
└── bin/
    └── jpegoptim    # exodus가 수집한 바이너리와 라이브러리들

Python 코드에서 호출할 때:

import subprocess
import os

def lambda_handler(event, context):
    # 현재 디렉토리의 bin 폴더에서 jpegoptim 찾기
    jpegoptim_path = os.path.join(os.getcwd(), 'bin', 'jpegoptim')
    
    # 라이브러리 경로 설정
    lib_path = os.path.join(os.getcwd(), 'lib')
    
    env = os.environ.copy()
    env['LD_LIBRARY_PATH'] = lib_path
    
    # jpegoptim 실행
    result = subprocess.run(
        [jpegoptim_path, '--version'],
        env=env,
        capture_output=True,
        text=True
    )
    
    return {
        'statusCode': 200,
        'body': result.stdout
    }

주의사항

  • 아키텍처 일치: exodus는 실행하는 OS와 아키텍처에 맞춰 의존성을 수집합니다. Lambda는 항상 Linux/x86_64이므로 반드시 Linux Docker에서 실행하세요.
  • 버전 호환성: 호스트 Ubuntu 버전과 Lambda 런타임의 glibc 버전이 맞지 않으면 실패할 수 있습니다. 가능하면 ubuntu:20.04 같은 안정적인 버전을 사용하세요.
  • 용량 확인: 패키징 후 전체 크기가 Lambda 제한(250MB)을 초과하지 않는지 확인하세요.

마치며

Lambda에서 외부 도구를 사용하려면 단순히 바이너리만 포함하면 안 됩니다. C 라이브러리 의존성을 정확히 파악하고 함께 배포해야 합니다. Docker로 로컬 테스트하고 exodus로 의존성을 자동 수집하면, “로컬에서는 되는데 Lambda에서는 안 되는” 좌절스러운 상황을 피할 수 있습니다.

댓글