튜토리얼: Lambda 함수 URL로 웹훅 엔드포인트 만들기

튜토리얼: Lambda 함수 URL로 웹훅 엔드포인트 만들기 (Tutorial: Creating a webhook endpoint using a Lambda function URL)

이 튜토리얼에서는 Lambda 함수 URL(function URL)로 웹훅(webhook) 엔드포인트를 구현해요. 웹훅은 HTTP를 사용해 애플리케이션 사이에 데이터를 자동으로 보내는 가볍고 이벤트 지향적인 통신 방식이에요. 웹훅으로 다른 시스템에서 일어나는 이벤트에 대한 즉각적인 업데이트를 받을 수 있어요. 예를 들면 웹사이트에 새 고객이 가입하거나, 결제가 처리되거나, 파일이 업로드된 경우죠.

Lambda에서는 웹훅을 Lambda 함수 URL 또는 API Gateway로 구현할 수 있어요. 함수 URL은 고급 인증이나 요청 검증 같은 기능이 필요 없는 단순한 웹훅에 좋은 선택이에요.

팁 어떤 솔루션이 특정 사용 사례에 가장 적합한지 모르겠다면 HTTP 요청으로 Lambda 함수를 호출하는 방법 선택하기를 참고하세요.

전제 조건

이 튜토리얼을 완료하려면 로컬 머신에 Python(버전 3.8 이상) 또는 Node.js(버전 18 이상) 중 하나가 설치돼 있어야 해요.

엔드포인트를 HTTP 요청으로 테스트하기 위해 튜토리얼은 다양한 네트워크 프로토콜로 데이터를 전송하는 데 쓸 수 있는 명령줄 도구인 curl을 사용해요. 아직 설치하지 않았다면 curl 문서를 참조해 도구를 설치하는 방법을 알아보세요.

출처: AWS Lambda 개발자 안내서

본문

Lambda 함수 만들기

먼저 웹훅 엔드포인트에 HTTP 요청이 전송될 때 실행되는 Lambda 함수를 만들어요. 이 예시에서 보내는 애플리케이션은 결제가 제출될 때마다 업데이트를 보내고, HTTP 요청 본문에 결제가 성공했는지를 나타내요. Lambda 함수는 요청을 파싱하고 결제 상태에 따라 조치를 취해요. 이 예시에서 코드는 결제의 주문 ID만 출력하지만, 실제 애플리케이션에서는 주문을 데이터베이스에 추가하거나 알림을 보낼 수도 있어요.

이 함수는 또한 웹훅에 가장 흔히 쓰이는 인증 방법인 해시 기반 메시지 인증(HMAC)을 구현해요. 이 방법에서는 보내는 애플리케이션과 받는 애플리케이션이 비밀 키를 공유해요. 보내는 애플리케이션은 해싱 알고리즘으로 이 키와 메시지 내용을 함께 사용해 고유한 서명을 생성하고, 그 서명을 HTTP 헤더로 웹훅 요청에 포함해요. 받는 애플리케이션은 이 단계를 반복해 비밀 키로 서명을 생성하고, 그 결과 값을 요청 헤더에 담긴 서명과 비교해요. 결과가 일치하면 요청이 합법적인 것으로 간주돼요.

Python 또는 Node.js 런타임 중 하나로 Lambda 콘솔에서 함수를 만들어요.

Lambda 함수 만들기 (Python)

  1. Lambda 콘솔의 Functions 페이지를 열어요.
  2. 다음을 수행해 기본 'Hello world' 함수를 만들어요. Create function을 선택하고, Author from scratch를 선택하며, Function name에 myLambdaWebhook을 입력하고, Runtime에서 python3.14를 선택한 다음 Create function을 선택해요.
  3. Code source 창에서 기존 코드를 다음으로 교체해요.
import json
import hmac
import hashlib
import os

def lambda_handler(event, context):
    # Get the webhook secret from environment variables
    webhook_secret = os.environ['WEBHOOK_SECRET']

    # Verify the webhook signature
    if not verify_signature(event, webhook_secret):
        return {
            'statusCode': 401,
            'body': json.dumps({'error': 'Invalid signature'})
        }

    try:
        # Parse the webhook payload
        payload = json.loads(event['body'])

        # Handle different event types
        event_type = payload.get('type')

        if event_type == 'payment.success':
            # Handle successful payment
            order_id = payload.get('orderId')
            print(f"Processing successful payment for order {order_id}")
            # Add your business logic here
            # For example, update database, send notifications, etc.
        elif event_type == 'payment.failed':
            # Handle failed payment
            order_id = payload.get('orderId')
            print(f"Processing failed payment for order {order_id}")
            # Add your business logic here
        else:
            print(f"Received unhandled event type: {event_type}")

        # Return success response
        return {
            'statusCode': 200,
            'body': json.dumps({'received': True})
        }
    except json.JSONDecodeError:
        return {
            'statusCode': 400,
            'body': json.dumps({'error': 'Invalid JSON payload'})
        }
    except Exception as e:
        print(f"Error processing webhook: {e}")
        return {
            'statusCode': 500,
            'body': json.dumps({'error': 'Internal server error'})
        }

def verify_signature(event, webhook_secret):
    """ Verify the webhook signature using HMAC """
    try:
        # Get the signature from headers
        signature = event['headers'].get('x-webhook-signature')
        if not signature:
            print("Error: Missing webhook signature in headers")
            return False

        # Get the raw body (return an empty string if the body key doesn't exist)
        body = event.get('body', '')

        # Create HMAC using the secret key
        expected_signature = hmac.new(
            webhook_secret.encode('utf-8'),
            body.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()

        # Compare the expected signature with the received signature to authenticate the message
        is_valid = hmac.compare_digest(signature, expected_signature)
        if not is_valid:
            print(f"Error: Invalid signature. Received: {signature}, Expected: {expected_signature}")
            return False

        return True
    except Exception as e:
        print(f"Error verifying signature: {e}")
        return False
  1. DEPLOY 섹션에서 Deploy를 선택해 함수 코드를 업데이트해요.

Lambda 함수 만들기 (Node.js)

  1. Lambda 콘솔의 Functions 페이지를 열어요.
  2. 다음을 수행해 기본 'Hello world' 함수를 만들어요. Create function을 선택하고, Author from scratch를 선택하며, Function name에 myLambdaWebhook을 입력하고, Runtime에서 nodejs24.x를 선택한 다음 Create function을 선택해요.
  3. Code source 창에서 기존 코드를 다음으로 교체해요.
import crypto from 'crypto';

export const handler = async (event, context) => {
  // Get the webhook secret from environment variables
  const webhookSecret = process.env.WEBHOOK_SECRET;

  // Verify the webhook signature
  if (!verifySignature(event, webhookSecret)) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: 'Invalid signature' })
    };
  }

  try {
    // Parse the webhook payload
    const payload = JSON.parse(event.body);

    // Handle different event types
    const eventType = payload.type;

    switch (eventType) {
      case 'payment.success': {
        // Handle successful payment
        const orderId = payload.orderId;
        console.log(`Processing successful payment for order ${orderId}`);
        // Add your business logic here
        // For example, update database, send notifications, etc.
        break;
      }
      case 'payment.failed': {
        // Handle failed payment
        const orderId = payload.orderId;
        console.log(`Processing failed payment for order ${orderId}`);
        // Add your business logic here
        break;
      }
      default:
        console.log(`Received unhandled event type: ${eventType}`);
    }

    // Return success response
    return {
      statusCode: 200,
      body: JSON.stringify({ received: true })
    };
  } catch (error) {
    if (error instanceof SyntaxError) {
      // Handle JSON parsing errors
      return {
        statusCode: 400,
        body: JSON.stringify({ error: 'Invalid JSON payload' })
      };
    }
    // Handle all other errors
    console.error('Error processing webhook:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' })
    };
  }
};

// Verify the webhook signature using HMAC
const verifySignature = (event, webhookSecret) => {
  try {
    // Get the signature from headers
    const signature = event.headers['x-webhook-signature'];
    if (!signature) {
      console.log('No signature found in headers:', event.headers);
      return false;
    }

    // Get the raw body (return an empty string if the body key doesn't exist)
    const body = event.body || '';

    // Create HMAC using the secret key
    const hmac = crypto.createHmac('sha256', webhookSecret);
    const expectedSignature = hmac.update(body).digest('hex');

    // Compare expected and received signatures
    const isValid = signature === expectedSignature;
    if (!isValid) {
      console.log(`Invalid signature. Received: ${signature}, Expected: ${expectedSignature}`);
      return false;
    }

    return true;
  } catch (error) {
    console.error('Error during signature verification:', error);
    return false;
  }
};
  1. DEPLOY 섹션에서 Deploy를 선택해 함수 코드를 업데이트해요.

비밀 키 만들기

Lambda 함수가 웹훅 요청을 인증하려면 호출 애플리케이션과 공유하는 비밀 키를 사용해요. 이 예시에서 키는 환경 변수에 저장돼요. 프로덕션 애플리케이션에서는 비밀번호 같은 민감한 정보를 함수 코드에 넣지 마세요. 대신 AWS Secrets Manager 시크릿을 만들고, AWS Parameters and Secrets Lambda 확장을 사용해 Lambda 함수에서 자격 증명을 검색하세요.

웹훅 비밀 키 만들고 저장하기

  1. 암호학적으로 안전한 난수 생성기로 길고 무작위한 문자열을 생성해요. Python이나 Node.js의 다음 코드 조각으로 32자 시크릿을 생성하고 출력하거나, 원하는 다른 방법을 사용해도 돼요.

Python — 시크릿 생성 예시 코드:

import secrets
webhook_secret = secrets.token_urlsafe(32)
print(webhook_secret)

Node.js — 시크릿 생성 예시 코드(ES 모듈 형식):

import crypto from 'crypto';

let webhookSecret = crypto.randomBytes(32).toString('base64');
console.log(webhookSecret);
  1. 생성한 문자열을 함수의 환경 변수로 저장해요. 함수의 Configuration 탭에서 Environment variables를 선택하고, Edit을 선택한 다음, Add environment variable을 선택하고, Key에 WEBHOOK_SECRET을 입력하고, Value에 이전 단계에서 생성한 시크릿을 입력한 뒤 Save를 선택해요.

이 시크릿은 튜토리얼의 나중에 함수를 테스트할 때 다시 써야 하니 지금 메모해 두세요.

함수 URL 엔드포인트 만들기

Lambda 함수 URL로 웹훅용 엔드포인트를 만들어요. NONE 인증 유형으로 공개 접근이 있는 엔드포인트를 만들기 때문에, URL을 아는 사람이라면 누구나 함수를 호출할 수 있어요. 함수 URL 접근 제어에 대해 자세히 알아보려면 Lambda 함수 URL 접근 제어하기를 참고하세요. 웹훅에 더 고급 인증 옵션이 필요하다면 API Gateway 사용을 고려해보세요.

함수 URL 엔드포인트 만들기

  1. 함수의 Configuration 탭에서 Function URL을 선택해요.
  2. Create function URL을 선택해요.
  3. Auth type에서 NONE을 선택해요.
  4. Save를 선택해요.

방금 만든 함수 URL의 엔드포인트가 Function URL 창에 표시돼요. 튜토리얼의 나중에 쓰려고 엔드포인트를 복사해 두세요.

콘솔에서 함수 테스트하기

URL 엔드포인트로 HTTP 요청을 사용해 함수를 호출하기 전에 콘솔에서 테스트해서 코드가 예상대로 동작하는지 확인해요.

콘솔에서 함수를 검증하려면 먼저 튜토리얼 앞부분에서 생성한 시크릿으로 다음 테스트 JSON 페이로드를 사용해 웹훅 서명을 계산해요.

{
  "type": "payment.success",
  "orderId": "1234",
  "amount": "99.99"
}

여러분 시크릿으로 웹훅 서명을 계산하려면 다음 Python 또는 Node.js 코드 예시 중 하나를 사용해요.

웹훅 서명 계산하기 (Python)

  1. 다음 코드를 calculate_signature.py라는 파일로 저장해요. 코드의 웹훅 시크릿을 여러분 값으로 바꿔요.
import secrets
import hmac
import json
import hashlib

webhook_secret = "arlbSDCP86n_1H90s0fL_Qb2NAHBIBQOyGI0X4Zay4M"

body = json.dumps({"type": "payment.success", "orderId": "1234", "amount": "99.99"})
signature = hmac.new(
    webhook_secret.encode('utf-8'),
    body.encode('utf-8'),
    hashlib.sha256
).hexdigest()

print(signature)
  1. 코드를 저장한 같은 디렉터리에서 다음 명령을 실행해 서명을 계산해요. 코드가 출력하는 서명을 복사해요.
python calculate_signature.py

웹훅 서명 계산하기 (Node.js)

  1. 다음 코드를 calculate_signature.mjs라는 파일로 저장해요. 코드의 웹훅 시크릿을 여러분 값으로 바꿔요.
import crypto from 'crypto';

const webhookSecret = "arlbSDCP86n_1H90s0fL_Qb2NAHBIBQOyGI0X4Zay4M"
const body = "{\"type\": \"payment.success\", \"orderId\": \"1234\", \"amount\": \"99.99\"}";

let hmac = crypto.createHmac('sha256', webhookSecret);
let signature = hmac.update(body).digest('hex');
console.log(signature);
  1. 코드를 저장한 같은 디렉터리에서 다음 명령을 실행해 서명을 계산해요. 코드가 출력하는 서명을 복사해요.
node calculate_signature.mjs

이제 콘솔에서 테스트 HTTP 요청으로 함수 코드를 테스트할 수 있어요.

콘솔에서 함수 테스트하기

  1. 함수의 Code 탭을 선택해요.
  2. TEST EVENTS 섹션에서 Create new test event를 선택해요.
  3. Event Name에 myEvent를 입력해요.
  4. 기존 JSON을 삭제하고 다음을 Event JSON 창에 붙여넣어요. 웹훅 서명을 이전 단계에서 계산한 값으로 바꿔요.
{
  "headers": {
    "Content-Type": "application/json",
    "x-webhook-signature": "2d672e7a0423fab740fbc040e801d1241f2df32d2ffd8989617a599486553e2a"
  },
  "body": "{\"type\": \"payment.success\", \"orderId\": \"1234\", \"amount\": \"99.99\"}"
}
  1. Save를 선택해요.
  2. Invoke를 선택해요. 다음 해와 비슷한 출력이 보여야 해요.

Python:

Status: Succeeded
Test Event Name: myEvent
Response:
{
  "statusCode": 200,
  "body": "{\"received\": true}"
}
Function Logs:
START RequestId: 50cc0788-d70e-453a-9a22-ceaa210e8ac6 Version: $LATEST
Processing successful payment for order 1234
END RequestId: 50cc0788-d70e-453a-9a22-ceaa210e8ac6
REPORT RequestId: 50cc0788-d70e-453a-9a22-ceaa210e8ac6 Duration: 1.55 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 36 MB Init Duration: 136.32 ms

Node.js:

Status: Succeeded
Test Event Name: myEvent
Response:
{
  "statusCode": 200,
  "body": "{\"received\":true}"
}
Function Logs:
START RequestId: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4 Version: $LATEST
2025-01-10T18:05:42.062Z e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4 INFO Processing successful payment for order 1234
END RequestId: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4
REPORT RequestId: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4 Duration: 60.10 ms Billed Duration: 61 ms Memory Size: 128 MB Max Memory Used: 72 MB Init Duration: 174.46 ms
Request ID: e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4

HTTP 요청으로 함수 테스트하기

curl 명령줄 도구로 웹훅 엔드포인트를 테스트해요.

HTTP 요청으로 함수 테스트하기

  1. 터미널이나 셸 프로그램에서 다음 curl 명령을 실행해요. URL을 여러분 함수 URL 엔드포인트의 값으로, 웹훅 서명을 여러분 비밀 키로 계산한 서명으로 바꿔요.
curl -X POST https://ryqgmbx5xjzxahif6frvzikpre0bpvpf.lambda-url.us-west-2.on.aws/ \
  -H "Content-Type: application/json" \
  -H "x-webhook-signature: d5f52b76ffba65ff60ea73da67bdf1fc5825d4db56b5d3ffa0b64b7cb85ef48b" \
  -d '{"type": "payment.success", "orderId": "1234", "amount": "99.99"}'

다음 출력이 보여야 해요.

{"received": true}
  1. 함수가 페이로드를 올바르게 파싱했는지 확인하려고 함수의 CloudWatch 로그를 검사해요. Amazon CloudWatch 콘솔에서 Logs group 페이지를 열고, 함수의 로그 그룹(/aws/lambda/myLambdaWebhook)을 선택한 다음, 가장 최근 로그 스트림을 선택해요. 함수 로그에 다음 비슷한 출력이 보여야 해요.

Python:

Processing successful payment for order 1234

Node.js:

2025-01-10T18:05:42.062Z e54fe6c7-1df9-4f05-a4c4-0f71cacd64f4 INFO Processing successful payment for order 1234
  1. 코드가 잘못된 서명을 감지하는지 다음 curl 명령으로 확인해요. URL을 여러분 함수 URL 엔드포인트로 바꿔요.
curl -X POST https://ryqgmbx5xjzxahif6frvzikpre0bpvpf.lambda-url.us-west-2.on.aws/ \
  -H "Content-Type: application/json" \
  -H "x-webhook-signature: abcdefg" \
  -d '{"type": "payment.success", "orderId": "1234", "amount": "99.99"}'

다음 출력이 보여야 해요.

{"error": "Invalid signature"}

리소스 정리하기

이제 이 튜토리얼을 위해 만든 리소스를 계속 유지하고 싶지 않다면 삭제할 수 있어요. 더 이상 사용하지 않는 AWS 리소스를 삭제하면 AWS 계정에 불필요한 요금이 부과되는 것을 막을 수 있어요.

Lambda 함수를 삭제하려면

  1. Lambda 콘솔의 Functions 페이지를 열어요.
  2. 만든 함수를 선택해요.
  3. Actions, Delete를 선택해요.
  4. 텍스트 입력 필드에 confirm을 입력하고 Delete를 선택해요.

콘솔에서 Lambda 함수를 만들 때 Lambda가 함수용 실행 역할(execution role)도 만들었어요.

실행 역할을 삭제하려면

  1. IAM 콘솔의 Roles 페이지를 열어요.
  2. Lambda가 만든 실행 역할을 선택해요. 역할 이름 형식은 myLambdaWebhook-role-<random string>이에요.
  3. Delete를 선택해요.
  4. 텍스트 입력 필드에 역할 이름을 입력하고 Delete를 선택해요.

더 알아보기 (Learn more)