# code/index.pyimportjsonimportlogginglogger=logging.getLogger()defhandler(event,context):"""
FC handler for HTTP trigger.
Args:
event: The HTTP request body (bytes).
context: FC context object with request ID, credentials,
function name, memory limit, etc.
Returns:
dict with statusCode, headers, and body for HTTP response.
"""logger.info(f"Request ID: {context.request_id}")logger.info(f"Function: {context.function.name}")logger.info(f"Memory limit: {context.function.memory_size} MiB")# Parse the event (HTTP trigger sends the request body)try:body=json.loads(event)except(json.JSONDecodeError,TypeError):body={}name=body.get("name","World")response={"statusCode":200,"headers":{"Content-Type":"application/json"},"body":json.dumps({"message":f"Hello, {name}!","request_id":context.request_id})}returnresponse
# s.yamledition:3.0.0name:image-processoraccess:defaultresources:hello:component:fc3props:region:cn-beijingfunctionName:入门示例description:"Simple hello world function"runtime:python3.10handler:index.handlermemorySize:256timeout:30code:./codetriggers:- triggerName:http-triggertriggerType:httptriggerConfig:authType:anonymousmethods:- GET- POST
# code/index.pyimporttimeimportjson# Module-level code runs during cold startINIT_TIME=time.time()print(f"Cold start initialization at {INIT_TIME}")# Expensive imports happen hereimportnumpyasnpfromPILimportImageINIT_DURATION=time.time()-INIT_TIMEprint(f"Init took {INIT_DURATION:.3f}s")defhandler(event,context):start=time.time()# Your business logic hereresult={"cold_start_init_ms":round(INIT_DURATION*1000)}duration=time.time()-startresult["handler_ms"]=round(duration*1000)return{"statusCode":200,"body":json.dumps(result)}
# pre-warm function (called by timer trigger)defhandler(event,context):"""
Do nothing — the point is to create warm instances.
"""return{"statusCode":200,"body":"warm"}
1
2
3
4
5
6
7
# Timer trigger: warm up at 8:55 AM every weekdaytriggers:- triggerName:pre-warmtriggerType:timertriggerConfig:cronExpression:"0 55 8 ? * MON-FRI"enable:true
3. 减小部署包体积
部署包每增加一字节,冷启动时间就可能延长。代码下载阶段往往是最大瓶颈。
1
2
3
4
5
6
7
8
# Bad: 150 MiB package with all of scipy/numpy/pandaspip install -r requirements.txt -t ./code/
# Good: only install what you needpip install Pillow requests -t ./code/ --no-cache-dir
# Better: use layers for shared dependencies# (covered in the next section)
# Bad: connects to DB on every cold start, even if the request# doesn't need itimportpymysqlconn=pymysql.connect(host="...",db="...")# Runs on cold startdefhandler(event,context):cursor=conn.cursor()...# Good: lazy initialization_conn=Nonedefget_connection():global_connif_connisNoneornot_conn.open:_conn=pymysql.connect(host="...",db="...")return_conndefhandler(event,context):conn=get_connection()cursor=conn.cursor()...
# Dockerfile for a Rust-based functionFROM rust:1.78-slim as builderWORKDIR /appCOPY . .RUN cargo build --releaseFROM debian:bookworm-slimCOPY --from=builder /app/target/release/my-function /app/bootstrapEXPOSE 9000CMD["/app/bootstrap"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# s.yaml for custom containerresources:rust-function:component:fc3props:region:cn-beijingfunctionName:rust-processorruntime:custom-containerhandler:not-usedmemorySize:512timeout:60customContainerConfig:image:registry.cn-beijing.aliyuncs.com/my-repo/rust-function:latestport:9000
defhandler(event,context):# Handle CORS preflighthttp_method=event.get("httpMethod","GET")cors_headers={"Access-Control-Allow-Origin":"https://example.com","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Authorization","Access-Control-Max-Age":"86400"}ifhttp_method=="OPTIONS":return{"statusCode":204,"headers":cors_headers,"body":""}# Normal request handlingresult={"message":"Hello from API"}return{"statusCode":200,"headers":{"Content-Type":"application/json",**cors_headers},"body":json.dumps(result)}
# code/index.pyimportjsonimportloggingimportosimportiofromurllib.parseimportunquoteimportoss2fromPILimportImage,ImageDraw,ImageFontlogger=logging.getLogger()logger.setLevel(logging.INFO)# ConfigurationSOURCE_BUCKET=os.environ.get("SOURCE_BUCKET","upload-images")TARGET_BUCKET=os.environ.get("TARGET_BUCKET","processed-images")OSS_ENDPOINT=os.environ.get("OSS_ENDPOINT","https://oss-cn-beijing-internal.aliyuncs.com")WATERMARK_TEXT=os.environ.get("WATERMARK_TEXT","chenk.top")# Resize configurationsSIZES={"large":1200,"medium":600,"thumb":150}defget_oss_client(context,bucket_name):"""Create OSS client using FC's temporary STS credentials."""creds=context.credentialsauth=oss2.StsAuth(creds.access_key_id,creds.access_key_secret,creds.security_token)returnoss2.Bucket(auth,OSS_ENDPOINT,bucket_name)defadd_watermark(image,text):"""Add a semi-transparent text watermark to the bottom-right corner."""draw=ImageDraw.Draw(image)# Use a size proportional to the imagefont_size=max(20,image.width//30)try:font=ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",font_size)except(IOError,OSError):font=ImageFont.load_default()# Calculate position (bottom-right with padding)bbox=draw.textbbox((0,0),text,font=font)text_width=bbox[2]-bbox[0]text_height=bbox[3]-bbox[1]padding=20x=image.width-text_width-paddingy=image.height-text_height-padding# Draw with semi-transparent whitedraw.text((x,y),text,fill=(255,255,255,128),font=font)returnimagedefresize_image(image,max_width):"""Resize image maintaining aspect ratio."""ifimage.width<=max_width:returnimage.copy()ratio=max_width/image.widthnew_height=int(image.height*ratio)returnimage.resize((max_width,new_height),Image.LANCZOS)defprocess_image(source_client,target_client,object_key):"""Download, process, and upload image variants."""logger.info(f"Processing: {object_key}")# Download the original imageresult=source_client.get_object(object_key)image_data=result.read()image=Image.open(io.BytesIO(image_data))# Convert to RGBA for watermark supportifimage.mode!="RGBA":image=image.convert("RGBA")# Extract the filename without extensionbase_name=os.path.splitext(os.path.basename(object_key))[0]results=[]forsize_name,max_widthinSIZES.items():# Resizeresized=resize_image(image,max_width)# Add watermark (skip for thumbnails)ifsize_name!="thumb":resized=add_watermark(resized,WATERMARK_TEXT)# Convert to RGB for JPEG outputrgb_image=resized.convert("RGB")# Save as JPEGjpeg_buffer=io.BytesIO()rgb_image.save(jpeg_buffer,format="JPEG",quality=85,optimize=True)jpeg_buffer.seek(0)jpeg_key=f"resized/{size_name}/{base_name}.jpg"target_client.put_object(jpeg_key,jpeg_buffer.getvalue())results.append({"key":jpeg_key,"format":"jpeg","size":size_name})logger.info(f" Uploaded: {jpeg_key}")# Save as WebPwebp_buffer=io.BytesIO()resized.save(webp_buffer,format="WebP",quality=80)webp_buffer.seek(0)webp_key=f"webp/{size_name}/{base_name}.webp"target_client.put_object(webp_key,webp_buffer.getvalue())results.append({"key":webp_key,"format":"webp","size":size_name})logger.info(f" Uploaded: {webp_key}")returnresultsdefhandler(event,context):"""
EventBridge trigger handler.
Receives CloudEvent when an image is uploaded to OSS.
"""logger.info(f"Event received: {event}")# Parse the CloudEvent from EventBridgeevt=json.loads(event)# Extract bucket and object key from the eventdata=evt.get("data",{})bucket_name=data.get("bucket",{}).get("name",SOURCE_BUCKET)object_key=unquote(data.get("object",{}).get("key",""))ifnotobject_key:logger.error("No object key in event")return{"statusCode":400,"body":"Missing object key"}# Validate file extensionext=os.path.splitext(object_key)[1].lower()ifextnotin(".jpg",".jpeg",".png",".webp"):logger.info(f"Skipping non-image file: {object_key}")return{"statusCode":200,"body":"Skipped: not an image"}# Create OSS clientssource_client=get_oss_client(context,bucket_name)target_client=get_oss_client(context,TARGET_BUCKET)# Process the imagetry:results=process_image(source_client,target_client,object_key)response={"statusCode":200,"body":json.dumps({"source":object_key,"processed":results,"count":len(results)})}logger.info(f"Processed {len(results)} variants for {object_key}")returnresponseexceptExceptionase:logger.error(f"Error processing {object_key}: {str(e)}",exc_info=True)return{"statusCode":500,"body":json.dumps({"error":str(e),"source":object_key})}
# Deploy infrastructure with Terraformcd terraform
terraform init
terraform plan -var="env=dev"terraform apply -var="env=dev" -auto-approve
# Package and deploy the functioncd ../image-pipeline
pip install -r code/requirements.txt -t code/ --no-cache-dir
s deploy
# Test: upload an image to the source bucketaliyun oss cp test-image.jpg \
oss://upload-images-dev/uploads/test-image.jpg \
--endpoint oss-cn-beijing.aliyuncs.com
# Wait a few seconds for the pipeline to processsleep 5# Verify the processed images existaliyun oss ls oss://processed-images-dev/resized/ \
--endpoint oss-cn-beijing.aliyuncs.com
aliyun oss ls oss://processed-images-dev/webp/ \
--endpoint oss-cn-beijing.aliyuncs.com
# Check function logss logs --tail