첫 번째 서버리스 함수 생성하기

첫 번째 서버리스 함수 생성하기

이 코드 예제에서는 Lambda 함수를 만들고 호출하며 CloudWatch 로그를 확인하는 방법을 보여드려요.

출처: 문서

본문

  • Lambda용 IAM 역할 생성
  • 함수 코드 생성
  • Lambda 함수 생성
  • Lambda 함수 테스트
  • 리소스 정리

Bash (AWS CLI with Bash script)

Note GitHub에 더 많은 내용이 있어요. 전체 예제와 설정·실행 방법은 Sample developer tutorials 저장소에서 확인할 수 있어요.

이 스크립트는 Lambda 함수를 만들고, 테스트 이벤트로 호출하며, CloudWatch 로그를 조회하고, 모든 리소스를 정리해요. 기본 런타임은 Python 3.13이며 Node.js 22.x도 선택할 수 있어요.

#!/bin/bash
# AWS Lambda - Create Your First Function
# This script creates a Lambda function, invokes it with a test event,
# views CloudWatch logs, and cleans up all resources.
#
# Source: https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html
#
# Resources created:
# - IAM role (Lambda execution role with basic logging permissions)
# - Lambda function (Python 3.13 or Node.js 22.x runtime)
# - CloudWatch log group (created automatically by Lambda on invocation)

set -eE -o pipefail

###############################################################################
# Setup
###############################################################################

UNIQUE_ID=$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' ')
FUNCTION_NAME="my-lambda-function-${UNIQUE_ID}"
ROLE_NAME="lambda-execution-role-${UNIQUE_ID}"
LOG_GROUP_NAME="/aws/lambda/${FUNCTION_NAME}"

TEMP_DIR=$(mktemp -d)
readonly TEMP_DIR
LOG_FILE="${TEMP_DIR}/lambda-gettingstarted.log"

exec > >(tee -a "$LOG_FILE") 2>&1

declare -a CREATED_RESOURCES

###############################################################################
# Helper functions
###############################################################################

cleanup_resources() {
	# Disable error trap to prevent recursion during cleanup
	trap - ERR
	set +eE

	echo ""
	echo "Cleaning up resources..."
	echo ""

	for ((i=${#CREATED_RESOURCES[@]}-1; i>=0; i--)); do
		local RESOURCE="${CREATED_RESOURCES[$i]}"
		local TYPE="${RESOURCE%%:*}"
		local NAME="${RESOURCE#*:}"

		case "$TYPE" in
		log-group)
			echo "Deleting CloudWatch log group: ${NAME}"
			aws logs delete-log-group \
				--log-group-name "$NAME" 2>&1 || echo " WARNING: Could not delete log group ${NAME}."
			;;
		lambda-function)
			echo "Deleting Lambda function: ${NAME}"
			aws lambda delete-function \
				--function-name "$NAME" 2>&1 || echo " WARNING: Could not delete Lambda function ${NAME}."
			echo " Waiting for function deletion to complete..."
			local DELETE_WAIT=0
			while aws lambda get-function --function-name "$NAME" > /dev/null 2>&1; do
				sleep 2
				DELETE_WAIT=$((DELETE_WAIT + 2))
				if [ "$DELETE_WAIT" -ge 60 ]; then
					echo " WARNING: Timed out waiting for function deletion."
					break
				fi
			done
			;;
		iam-role-policy)
			local ROLE_PART="${NAME%%|*}"
			local POLICY_PART="${NAME#*|}"
			echo "Detaching policy from role: ${ROLE_PART}"
			aws iam detach-role-policy \
				--role-name "$ROLE_PART" \
				--policy-arn "$POLICY_PART" 2>&1 || echo " WARNING: Could not detach policy from role ${ROLE_PART}."
			;;
		iam-role)
			echo "Deleting IAM role: ${NAME}"
			aws iam delete-role \
				--role-name "$NAME" 2>&1 || echo " WARNING: Could not delete IAM role ${NAME}."
			;;
		esac
	done

	if [ -d "$TEMP_DIR" ]; then
		rm -rf "$TEMP_DIR"
	fi

	echo ""
	echo "Cleanup complete."
}

handle_error() {
	echo ""
	echo "==========================================="
	echo "ERROR: Script failed at $1"
	echo "==========================================="
	echo ""
	if [ ${#CREATED_RESOURCES[@]} -gt 0 ]; then
		echo "Attempting to clean up ${#CREATED_RESOURCES[@]} resource(s)..."
		cleanup_resources
	fi
	exit 1
}

trap 'handle_error "line $LINENO"' ERR

wait_for_resource() {
	local DESCRIPTION="$1"
	local COMMAND="$2"
	local TARGET_VALUE="$3"
	local TIMEOUT=300
	local ELAPSED=0
	local INTERVAL=5

	echo "Waiting for ${DESCRIPTION}..."
	while true; do
		local RESULT
		RESULT=$(eval "$COMMAND" 2>&1) || true
		if echo "$RESULT" | grep -q "$TARGET_VALUE"; then
			echo " ${DESCRIPTION} is ready."
			return 0
		fi
		if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
			echo "ERROR: Timed out waiting for ${DESCRIPTION} after ${TIMEOUT} seconds."
			return 1
		fi
		sleep "$INTERVAL"
		ELAPSED=$((ELAPSED + INTERVAL))
	done
}

validate_input() {
	local input="$1"
	local pattern="$2"
	if ! [[ "$input" =~ $pattern ]]; then
		echo "ERROR: Invalid input: $input"
		return 1
	fi
	return 0
}

###############################################################################
# Region pre-check
###############################################################################

CONFIGURED_REGION=$(aws configure get region 2>/dev/null || true)
if [ -z "$CONFIGURED_REGION" ] && [ -z "$AWS_DEFAULT_REGION" ] && [ -z "$AWS_REGION" ]; then
	echo "ERROR: No AWS region configured."
	echo "Run 'aws configure set region <region>' or export AWS_DEFAULT_REGION."
	exit 1
fi

###############################################################################
# Runtime selection
###############################################################################

echo ""
echo "==========================================="
echo "AWS Lambda - Create Your First Function"
echo "==========================================="
echo ""
echo "Select a runtime for your Lambda function:"
echo " 1) Python 3.13"
echo " 2) Node.js 22.x"
echo ""
echo "Using default: Python 3.13"
RUNTIME_CHOICE="1"

case "$RUNTIME_CHOICE" in
1)
	RUNTIME="python3.13"
	HANDLER="lambda_function.lambda_handler"
	CODE_FILE="lambda_function.py"
	cat > "${TEMP_DIR}/${CODE_FILE}" << 'PYTHON_EOF'
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
	if not isinstance(event, dict) or 'length' not in event or 'width' not in event:
		raise ValueError('Event must contain length and width')
	try:
		length = float(event['length'])
		width = float(event['width'])
		if length < 0 or width < 0:
			raise ValueError('Length and width must be non-negative')
		area = calculate_area(length, width)
		print(f'The area is {area}')
		logger.info(f'CloudWatch logs group: {context.log_group_name}')
		return json.dumps({'area': area})
	except (TypeError, ValueError) as e:
		logger.error(f'Error processing input: {str(e)}')
		raise
def calculate_area(length, width):
	return length * width
PYTHON_EOF
	echo "Selected runtime: Python 3.13"
	;;
2)
	RUNTIME="nodejs22.x"
	HANDLER="index.handler"
	CODE_FILE="index.mjs"
	cat > "${TEMP_DIR}/${CODE_FILE}" << 'NODEJS_EOF'
export const handler = async (event, context) => {
	if (!event || typeof event.length !== 'number' || typeof event.width !== 'number') {
		throw new Error('Event must contain numeric length and width');
	}
	if (event.length < 0 || event.width < 0) {
		throw new Error('Length and width must be non-negative');
	}
	const area = event.length * event.width;
	console.log(`The area is ${area}`);
	console.log('CloudWatch log group: ', context.logGroupName);
	return JSON.stringify({area});
};
NODEJS_EOF
	echo "Selected runtime: Node.js 22.x"
	;;
*)
	echo "ERROR: Invalid choice. Please enter 1 or 2."
	exit 1
	;;
esac

###############################################################################
# Step 1: Create IAM execution role
###############################################################################

echo ""
echo "==========================================="
echo "Step 1: Create IAM execution role"
echo "==========================================="
echo ""

TRUST_POLICY='{
	"Version":"2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": {
				"Service": "lambda.amazonaws.com"
			},
			"Action": "sts:AssumeRole"
		}
	]
}'

echo "Creating IAM role: ${ROLE_NAME}"
ROLE_OUTPUT=$(aws iam create-role \
	--role-name "$ROLE_NAME" \
	--assume-role-policy-document "$TRUST_POLICY" \
	--query 'Role.Arn' \
	--output text 2>&1)

if ! validate_input "$ROLE_OUTPUT" "^arn:aws:iam::[0-9]+:role/"; then
	echo "ERROR: Failed to create IAM role"
	exit 1
fi

echo "$ROLE_OUTPUT"
ROLE_ARN="$ROLE_OUTPUT"
CREATED_RESOURCES+=("iam-role:${ROLE_NAME}")
echo "Role ARN: ${ROLE_ARN}"

aws iam tag-role \
	--role-name "$ROLE_NAME" \
	--tags Key=project,Value=doc-smith Key=tutorial,Value=lambda-gettingstarted

echo ""
echo "Attaching AWSLambdaBasicExecutionRole policy..."
aws iam attach-role-policy \
	--role-name "$ROLE_NAME" \
	--policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 2>&1
CREATED_RESOURCES+=("iam-role-policy:${ROLE_NAME}|arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole")
echo "Policy attached."

# IAM roles can take a few seconds to propagate
echo "Waiting for IAM role to propagate..."
sleep 10

###############################################################################
# Step 2: Create Lambda function
###############################################################################

echo ""
echo "==========================================="
echo "Step 2: Create Lambda function"
echo "==========================================="
echo ""

echo "Creating deployment package..."
ORIGINAL_DIR=$(pwd)
cd "$TEMP_DIR" || exit 1
zip -j function.zip "$CODE_FILE" > /dev/null 2>&1 || {
	echo "ERROR: Failed to create deployment package"
	exit 1
}
cd "$ORIGINAL_DIR" || exit 1

if [ ! -f "${TEMP_DIR}/function.zip" ]; then
	echo "ERROR: Deployment package creation failed"
	exit 1
fi

echo "Creating Lambda function: ${FUNCTION_NAME}"
echo " Runtime: ${RUNTIME}"
echo " Handler: ${HANDLER}"
echo ""

CREATE_OUTPUT=$(aws lambda create-function \
	--function-name "$FUNCTION_NAME" \
	--runtime "$RUNTIME" \
	--role "$ROLE_ARN" \
	--handler "$HANDLER" \
	--architectures x86_64 \
	--zip-file "fileb://${TEMP_DIR}/function.zip" \
	--tags project=doc-smith,tutorial=lambda-gettingstarted \
	--query '[FunctionName, FunctionArn, Runtime, State]' \
	--output text 2>&1)

if [ -z "$CREATE_OUTPUT" ]; then
	echo "ERROR: Failed to create Lambda function"
	exit 1
fi

echo "$CREATE_OUTPUT"
CREATED_RESOURCES+=("lambda-function:${FUNCTION_NAME}")

wait_for_resource "Lambda function to become Active" \
	"aws lambda get-function-configuration --function-name ${FUNCTION_NAME} --query State --output text" \
	"Active"

###############################################################################
# Step 3: Invoke the function
###############################################################################

echo ""
echo "==========================================="
echo "Step 3: Invoke the function"
echo "==========================================="
echo ""

TEST_EVENT='{"length": 6, "width": 7}'
echo "Invoking function with test event: ${TEST_EVENT}"
echo ""

echo "$TEST_EVENT" > "${TEMP_DIR}/test-event.json"

if ! validate_input "$TEST_EVENT" '"length": [0-9]+, "width": [0-9]+'; then
	echo "ERROR: Invalid test event format"
	exit 1
fi

INVOKE_OUTPUT=$(aws lambda invoke \
	--function-name "$FUNCTION_NAME" \
	--payload "fileb://${TEMP_DIR}/test-event.json" \
	--cli-read-timeout 30 \
	"${TEMP_DIR}/response.json" 2>&1)
echo "$INVOKE_OUTPUT"

if [ ! -f "${TEMP_DIR}/response.json" ]; then
	echo "ERROR: No response file generated"
	exit 1
fi

RESPONSE=$(cat "${TEMP_DIR}/response.json")
echo ""
echo "Function response: ${RESPONSE}"
echo ""

if echo "$INVOKE_OUTPUT" | grep -qi "functionerror"; then
	echo "WARNING: Function returned an error."
fi

###############################################################################
# Step 4: View CloudWatch logs
###############################################################################

echo ""
echo "==========================================="
echo "Step 4: View CloudWatch Logs"
echo "==========================================="
echo ""

echo "Log group: ${LOG_GROUP_NAME}"
echo ""

echo "Waiting for CloudWatch logs to be available..."

LOG_STREAMS=""
for i in $(seq 1 6); do
	LOG_STREAMS=$(aws logs describe-log-streams \
		--log-group-name "$LOG_GROUP_NAME" \
		--order-by LastEventTime \
		--descending \
		--query 'logStreams[0].logStreamName' \
		--output text 2>/dev/null) || true
	if [ -n "$LOG_STREAMS" ] && [ "$LOG_STREAMS" != "None" ]; then
		break
	fi
	LOG_STREAMS=""
	sleep 5
done

if [ -n "$LOG_STREAMS" ] && [ "$LOG_STREAMS" != "None" ]; then
	echo "Latest log stream: ${LOG_STREAMS}"
	echo ""
	echo "--- Log events ---"
	LOG_EVENTS=$(aws logs get-log-events \
		--log-group-name "$LOG_GROUP_NAME" \
		--log-stream-name "$LOG_STREAMS" \
		--query 'events[].message' \
		--output text 2>&1) || true
	echo "$LOG_EVENTS"
	echo "--- End of log events ---"
else
	echo "No log streams found yet. Logs may take a moment to appear."
	echo "You can view them in the CloudWatch console:"
	echo " Log group: ${LOG_GROUP_NAME}"
fi

CREATED_RESOURCES+=("log-group:${LOG_GROUP_NAME}")

aws logs tag-log-group \
	--log-group-name "$LOG_GROUP_NAME" \
	--tags project=doc-smith,tutorial=lambda-gettingstarted

###############################################################################
# Summary and cleanup
###############################################################################

echo ""
echo "==========================================="
echo "SUMMARY"
echo "==========================================="
echo ""
echo "Resources created:"
echo " IAM role: ${ROLE_NAME}"
echo " Lambda function: ${FUNCTION_NAME}"
echo " CloudWatch logs: ${LOG_GROUP_NAME}"
echo ""
echo "==========================================="
echo "CLEANUP"
echo "==========================================="
echo ""
echo "Cleaning up all created resources..."
cleanup_resources

echo ""
echo "Done."

API 상세는 AWS CLI Command Reference의 다음 항목을 참고해요.

  • AttachRolePolicy, CreateFunction, CreateRole, DeleteFunction, DeleteLogGroup, DeleteRole, DescribeLogStreams, DetachRolePolicy, GetFunction, GetFunctionConfiguration, GetLogEvents, Invoke

더 알아보기 (Learn more)