Quay lại

Hướng dẫn tạo Lambda Layer cho Pillow với AVIF Support Chuyên mục Devops    2025-08-27    2 Lượt xem    0 Lượt thích    comment-3 Created with Sketch Beta. 0 Bình luận

# Hướng dẫn tạo Lambda Layer cho Pillow với AVIF Support

## Tổng quan
Hướng dẫn này mô tả cách tạo AWS Lambda Layer cho thư viện Pillow với hỗ trợ AVIF format, sử dụng Docker để đảm bảo tương thích với môi trường Lambda Python 3.12.

## Vấn đề thường gặp
- **ImportError**: `cannot import name '_imaging' from 'PIL'` - Do thiếu binary dependencies
- **Platform mismatch**: Cài đặt trên local machine không tương thích với Lambda runtime
- **AVIF support**: Cần plugin bổ sung `pillow-avif-plugin`

## Giải pháp: Sử dụng Docker

### Bước 1: Tạo thư mục làm việc
```bash
mkdir pillow-layer
cd pillow-layer
```

### Bước 2: Sử dụng Docker để cài đặt dependencies
```bash
# Sử dụng official AWS Lambda Python 3.12 runtime image
docker run --rm -v $(pwd):/var/task --entrypoint="" \
  public.ecr.aws/lambda/python:3.12 \
  pip install --target /var/task/python Pillow pillow-avif-plugin
```

**Giải thích lệnh:**
- `--rm`: Tự động xóa container sau khi chạy xong
- `-v $(pwd):/var/task`: Mount thư mục hiện tại vào container
- `--entrypoint=""`: Override entrypoint mặc định
- `public.ecr.aws/lambda/python:3.12`: Official AWS Lambda Python 3.12 image
- `--target /var/task/python`: Cài đặt vào thư mục python/

### Bước 3: Kiểm tra cài đặt
```bash
# Kiểm tra các file binary quan trọng
find python/PIL/ -name "*imaging*" -o -name "*.so" | head -10

# Kết quả mong đợi:
# python/PIL/_imaging.cpython-312-x86_64-linux-gnu.so
# python/PIL/_avif.cpython-312-x86_64-linux-gnu.so
# python/PIL/_webp.cpython-312-x86_64-linux-gnu.so
# ...
```

### Bước 4: Tạo file zip cho Lambda Layer
```bash
zip -r pillow_layer.zip python/
```

### Bước 5: Kiểm tra kích thước và hash
```bash
ls -la pillow_layer.zip
sha256sum pillow_layer.zip
```

## Tích hợp với Terraform

### Cấu hình Lambda Layer
```hcl
resource "aws_lambda_layer_version" "pillow_layer" {
  filename         = "${path.module}/pillow_layer.zip"
  layer_name       = "${var.project_name}-${var.environment}-pillow-layer"
  source_code_hash = filebase64sha256("${path.module}/pillow_layer.zip")

  compatible_runtimes = ["python3.12"]
  description         = "Pillow and pillow-avif-plugin for image processing"
}
```

### Sử dụng Layer trong Lambda Function
```hcl
resource "aws_lambda_function" "image_processing" {
  # ... other configuration ...
  
  layers = [aws_lambda_layer_version.pillow_layer.arn]
  runtime = "python3.12"
  
  # ... other configuration ...
}
```

## Script tự động hóa

### Tạo script build_pillow_layer.sh
```bash
#!/bin/bash

# Tạo thư mục làm việc
rm -rf pillow-layer
mkdir pillow-layer
cd pillow-layer

echo "🐳 Building Pillow layer using Docker..."

# Sử dụng Docker để cài đặt dependencies
docker run --rm -v $(pwd):/var/task --entrypoint="" \
  public.ecr.aws/lambda/python:3.12 \
  pip install --target /var/task/python Pillow pillow-avif-plugin

echo "📦 Creating zip file..."

# Tạo file zip
zip -r ../modules/image_processing/pillow_layer.zip python/

echo "✅ Layer created successfully!"

# Hiển thị thông tin
echo "📊 Layer information:"
ls -la ../modules/image_processing/pillow_layer.zip
echo "🔐 SHA256: $(sha256sum ../modules/image_processing/pillow_layer.zip | cut -d' ' -f1)"

# Kiểm tra các file quan trọng
echo "🔍 Important files:"
find python/PIL/ -name "_imaging.cpython-312-x86_64-linux-gnu.so" -o \
                 -name "_avif.cpython-312-x86_64-linux-gnu.so" -o \
                 -name "_webp.cpython-312-x86_64-linux-gnu.so"

cd ..
rm -rf pillow-layer

echo "🚀 Ready to deploy with Terraform!"
```

### Cách sử dụng script
```bash
chmod +x build_pillow_layer.sh
./build_pillow_layer.sh
```

## Triển khai với Terraform

### Cập nhật Layer
```bash
# Apply chỉ layer (nhanh hơn)
terraform apply -target=module.image_processing.aws_lambda_layer_version.pillow_layer -auto-approve

# Cập nhật Lambda function để sử dụng layer mới
terraform apply -target=module.image_processing.aws_lambda_function.image_processing -auto-approve
```

## Kiểm tra hoạt động

### Test Lambda function
```bash
# Upload test image
aws s3 cp test-image.jpg s3://your-bucket/images/original/test-image.jpg

# Kiểm tra logs
aws logs describe-log-streams \
  --log-group-name "/aws/lambda/your-function-name" \
  --order-by LastEventTime --descending --limit 1

# Xem chi tiết logs
aws logs get-log-events \
  --log-group-name "/aws/lambda/your-function-name" \
  --log-stream-name "STREAM_NAME_FROM_ABOVE"
```

### Kết quả mong đợi trong logs
```
[INFO] Processing image: images/original/test-image.jpg
[INFO] Original image size: 800x600
[INFO] Processing size l: 800x600
[INFO] Uploaded: images/processed/test-image/l/test-image.avif (1467 bytes)
[INFO] Uploaded: images/processed/test-image/l/test-image.webp (4446 bytes)
[INFO] Uploaded: images/processed/test-image/l/test-image.jpg (14889 bytes)
```

## Troubleshooting

### Lỗi thường gặp và cách khắc phục

#### 1. ImportError: cannot import name '_imaging'
**Nguyên nhân**: Thiếu binary dependencies hoặc sai platform
**Giải pháp**: Sử dụng Docker với official Lambda image

#### 2. Layer quá lớn (>50MB unzipped)
**Nguyên nhân**: Cài đặt quá nhiều dependencies
**Giải pháp**: Chỉ cài đặt Pillow và pillow-avif-plugin

#### 3. AVIF format không được hỗ trợ
**Nguyên nhân**: Thiếu pillow-avif-plugin
**Giải pháp**: Đảm bảo cài đặt cả hai packages

#### 4. Terraform không detect thay đổi layer
**Nguyên nhân**: File zip có cùng tên nhưng nội dung khác
**Giải pháp**: Terraform sử dụng `source_code_hash` để detect thay đổi

## Phiên bản đã test thành công

- **Python Runtime**: 3.12
- **Pillow**: 11.3.0
- **pillow-avif-plugin**: 1.5.2
- **Docker Image**: public.ecr.aws/lambda/python:3.12
- **Layer Size**: ~11MB (compressed)

## Lưu ý quan trọng

1. **Luôn sử dụng Docker** với official AWS Lambda image để đảm bảo tương thích
2. **Kiểm tra binary files** sau khi cài đặt để đảm bảo có đủ dependencies
3. **Test trên Lambda** trước khi deploy production
4. **Backup layer cũ** trước khi cập nhật
5. **Monitor CloudWatch logs** để phát hiện lỗi sớm

## Kết quả đạt được

- ✅ AVIF format support hoạt động hoàn hảo
- ✅ Giảm 90%+ kích thước file so với JPEG
- ✅ Xử lý nhanh: ~1.2s cho 9 variants
- ✅ Memory usage thấp: 128MB/1GB
- ✅ Tương thích với WebP và JPEG

---

**Tác giả**: Sondh3  
**Ngày tạo**: 27/08/2025  
**Phiên bản**: 1.0  
**Status**: ✅ Tested & Working

build_pillow_layer.sh

#!/bin/bash

# Script tự động tạo Lambda Layer cho Pillow với AVIF support
# Sử dụng Docker để đảm bảo tương thích với AWS Lambda Python 3.12

set -e  # Exit on any error

echo "🚀 Starting Pillow Layer build process..."

# Kiểm tra Docker
if ! command -v docker &> /dev/null; then
    echo "❌ Docker is required but not installed. Please install Docker first."
    exit 1
fi

# Tạo thư mục làm việc
echo "📁 Creating working directory..."
rm -rf pillow-layer
mkdir pillow-layer
cd pillow-layer

echo "🐳 Building Pillow layer using AWS Lambda Python 3.12 Docker image..."
echo "   This ensures compatibility with Lambda runtime environment"

# Sử dụng Docker để cài đặt dependencies với chính xác runtime Lambda
docker run --rm -v $(pwd):/var/task --entrypoint="" \
  public.ecr.aws/lambda/python:3.12 \
  pip install --target /var/task/python Pillow pillow-avif-plugin

echo "🔍 Verifying installation..."

# Kiểm tra các file binary quan trọng
IMAGING_FILE=$(find python/PIL/ -name "_imaging.cpython-312-x86_64-linux-gnu.so" | head -1)
AVIF_FILE=$(find python/PIL/ -name "_avif.cpython-312-x86_64-linux-gnu.so" | head -1)
WEBP_FILE=$(find python/PIL/ -name "_webp.cpython-312-x86_64-linux-gnu.so" | head -1)

if [[ -z "$IMAGING_FILE" ]]; then
    echo "❌ Missing _imaging binary file. Installation failed."
    exit 1
fi

if [[ -z "$AVIF_FILE" ]]; then
    echo "❌ Missing _avif binary file. AVIF support not available."
    exit 1
fi

if [[ -z "$WEBP_FILE" ]]; then
    echo "❌ Missing _webp binary file. WebP support not available."
    exit 1
fi

echo "✅ All required binary files found:"
echo "   📄 _imaging: $IMAGING_FILE"
echo "   📄 _avif: $AVIF_FILE" 
echo "   📄 _webp: $WEBP_FILE"

echo "📦 Creating zip file..."

# Tạo file zip cho Terraform
zip -r ../modules/image_processing/pillow_layer.zip python/ > /dev/null

echo "✅ Layer created successfully!"

# Hiển thị thông tin chi tiết
echo ""
echo "📊 Layer Information:"
LAYER_FILE="../modules/image_processing/pillow_layer.zip"
LAYER_SIZE=$(ls -lh "$LAYER_FILE" | awk '{print $5}')
LAYER_HASH=$(sha256sum "$LAYER_FILE" | cut -d' ' -f1)

echo "   📁 File: $LAYER_FILE"
echo "   📏 Size: $LAYER_SIZE"
echo "   🔐 SHA256: $LAYER_HASH"

# Kiểm tra kích thước (Lambda layer limit là 50MB unzipped)
UNZIPPED_SIZE=$(unzip -l "$LAYER_FILE" | tail -1 | awk '{print $1}')
UNZIPPED_MB=$((UNZIPPED_SIZE / 1024 / 1024))

echo "   📦 Unzipped size: ${UNZIPPED_MB}MB (limit: 50MB)"

if [[ $UNZIPPED_MB -gt 50 ]]; then
    echo "⚠️  Warning: Layer size exceeds 50MB limit!"
else
    echo "✅ Layer size is within limits"
fi

# Cleanup
cd ..
rm -rf pillow-layer

echo ""
echo "🎯 Next Steps:"
echo "   1. Deploy layer: terraform apply -target=module.image_processing.aws_lambda_layer_version.pillow_layer -auto-approve"
echo "   2. Update function: terraform apply -target=module.image_processing.aws_lambda_function.image_processing -auto-approve"
echo "   3. Test with: aws s3 cp test-image.jpg s3://your-bucket/images/original/"
echo ""
echo "🚀 Ready to deploy with Terraform!"

 

import json
import boto3
import os
import urllib.parse
from PIL import Image, ImageOps
import pillow_avif
import io
import logging

# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Initialize S3 client
s3_client = boto3.client('s3')

# Configuration from environment variables
PROCESSED_BUCKET = os.environ['PROCESSED_BUCKET']
SIZES = json.loads(os.environ['SIZES'])  # {"s": 360, "m": 720, "l": 1280}
FORMATS = os.environ['FORMATS'].split(',')  # ["avif", "webp", "jpg", "png"]
QUALITY_AVIF = int(os.environ.get('QUALITY_AVIF', '50'))
QUALITY_WEBP = int(os.environ.get('QUALITY_WEBP', '80'))
QUALITY_JPEG = int(os.environ.get('QUALITY_JPEG', '85'))
QUALITY_PNG = int(os.environ.get('QUALITY_PNG', '9'))  # PNG compression level (0-9)

def lambda_handler(event, context):
    """
    Lambda function to process uploaded images:
    1. Download original image from S3
    2. Generate multiple sizes (S/M/L)
    3. Convert to multiple formats (AVIF/WebP/JPEG/PNG)
    4. Upload processed images to processed bucket
    """
    
    try:
        # Parse S3 event
        for record in event['Records']:
            # Get bucket and object key
            bucket = record['s3']['bucket']['name']
            key = urllib.parse.unquote_plus(record['s3']['object']['key'], encoding='utf-8')
            
            logger.info(f"Processing image: {key} from bucket: {bucket}")
            
            # Skip if not in images/original/ folder
            if not key.startswith('images/original/'):
                logger.info(f"Skipping {key} - not in images/original/ folder")
                continue
            
            # Extract filename without extension
            filename = os.path.basename(key)
            name_without_ext = os.path.splitext(filename)[0]
            
            # Download original image
            try:
                response = s3_client.get_object(Bucket=bucket, Key=key)
                image_content = response['Body'].read()
            except Exception as e:
                logger.error(f"Error downloading {key}: {str(e)}")
                continue
            
            # Open image with PIL
            try:
                with Image.open(io.BytesIO(image_content)) as img:
                    # Store original mode for PNG transparency handling
                    original_mode = img.mode
                    has_transparency = img.mode in ('RGBA', 'LA', 'P') and ('transparency' in img.info or img.mode == 'RGBA')
                    
                    # Keep original image for PNG (preserves transparency)
                    original_img = img.copy()
                    
                    # Convert to RGB for other formats (AVIF/WebP/JPEG)
                    if img.mode in ('RGBA', 'LA', 'P'):
                        # Create white background for non-PNG formats
                        background = Image.new('RGB', img.size, (255, 255, 255))
                        if img.mode == 'P':
                            img = img.convert('RGBA')
                        background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
                        rgb_img = background
                    elif img.mode != 'RGB':
                        rgb_img = img.convert('RGB')
                    else:
                        rgb_img = img.copy()
                    
                    # Auto-orient image based on EXIF data
                    rgb_img = ImageOps.exif_transpose(rgb_img)
                    original_img = ImageOps.exif_transpose(original_img)
                    
                    original_width, original_height = rgb_img.size
                    logger.info(f"Original image size: {original_width}x{original_height}")
                    
                    # Process each size
                    for size_name, target_width in SIZES.items():
                        # Calculate new dimensions maintaining aspect ratio
                        if original_width <= target_width:
                            # If original is smaller than target, use original size
                            new_width = original_width
                            new_height = original_height
                            resized_rgb_img = rgb_img.copy()
                            resized_original_img = original_img.copy()
                        else:
                            # Resize maintaining aspect ratio
                            aspect_ratio = original_height / original_width
                            new_width = target_width
                            new_height = int(target_width * aspect_ratio)
                            resized_rgb_img = rgb_img.resize((new_width, new_height), Image.Resampling.LANCZOS)
                            resized_original_img = original_img.resize((new_width, new_height), Image.Resampling.LANCZOS)
                        
                        logger.info(f"Processing size {size_name}: {new_width}x{new_height}")
                        
                        # Process each format
                        for format_name in FORMATS:
                            try:
                                # Create output buffer
                                output_buffer = io.BytesIO()
                                
                                # Set format-specific parameters
                                if format_name == 'avif':
                                    resized_rgb_img.save(
                                        output_buffer, 
                                        format='AVIF',
                                        quality=QUALITY_AVIF,
                                        speed=6  # Balance between speed and compression
                                    )
                                    content_type = 'image/avif'
                                    file_extension = 'avif'
                                elif format_name == 'webp':
                                    resized_rgb_img.save(
                                        output_buffer,
                                        format='WebP',
                                        quality=QUALITY_WEBP,
                                        method=6,  # Better compression
                                        optimize=True
                                    )
                                    content_type = 'image/webp'
                                    file_extension = 'webp'
                                elif format_name == 'jpg':
                                    resized_rgb_img.save(
                                        output_buffer,
                                        format='JPEG',
                                        quality=QUALITY_JPEG,
                                        optimize=True,
                                        progressive=True  # Progressive JPEG for better UX
                                    )
                                    content_type = 'image/jpeg'
                                    file_extension = 'jpg'
                                elif format_name == 'png':
                                    # Use original image to preserve transparency
                                    resized_original_img.save(
                                        output_buffer,
                                        format='PNG',
                                        compress_level=QUALITY_PNG,  # 0-9, higher = better compression
                                        optimize=True
                                    )
                                    content_type = 'image/png'
                                    file_extension = 'png'
                                
                                # Create S3 key for processed image
                                processed_key = f"images/processed/{name_without_ext}/{size_name}/{name_without_ext}.{file_extension}"
                                
                                # Upload to processed bucket
                                output_buffer.seek(0)
                                s3_client.put_object(
                                    Bucket=PROCESSED_BUCKET,
                                    Key=processed_key,
                                    Body=output_buffer.getvalue(),
                                    ContentType=content_type,
                                    CacheControl='public, max-age=31536000',  # 1 year cache
                                    Metadata={
                                        'original-key': key,
                                        'size': size_name,
                                        'format': format_name,
                                        'width': str(new_width),
                                        'height': str(new_height)
                                    }
                                )
                                
                                logger.info(f"Uploaded: {processed_key} ({len(output_buffer.getvalue())} bytes)")
                                
                            except Exception as e:
                                logger.error(f"Error processing {format_name} format for size {size_name}: {str(e)}")
                                continue
                    
            except Exception as e:
                logger.error(f"Error processing image {key}: {str(e)}")
                continue
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Images processed successfully',
                'processed_count': len(event['Records'])
            })
        }
        
    except Exception as e:
        logger.error(f"Unexpected error: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps({
                'error': str(e)
            })
        }

Bình luận (0)

Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough

Bài viết liên quan

Learning English Everyday