# 从 0 到 1:基于华为云 OBS + 函数工作流打造「无服务器」图片水印服务
# 从 0 到 1:基于华为云 OBS + 函数工作流打造「无服务器」图片水印服务
> 摘要:本文带你用华为云对象存储(OBS)事件通知 + 函数工作流(FunctionGraph),零服务器成本搭建一个「上传即自动加水印」的图片处理服务。全文含可复制代码、部署脚本、成本估算与踩坑记录,适合想入门 Serverless 的开发者。
**适用人群**:有 Python 基础、想了解华为云 Serverless 的开发者
**预计阅读**:15 分钟
**涉及服务**:对象存储 OBS、函数工作流 FunctionGraph、统一身份认证 IAM
---
## 一、为什么做这件事
很多团队都遇到过这样的需求:
- 用户上传的原图需要自动打上版权水印;
- 图片量波动极大,白天几百张、凌晨几张;
- 不想为了偶尔的峰值长期养一台服务器。
这正是一个典型的 **事件驱动 + 无服务器** 场景。华为云 OBS 支持在对象上传时触发事件通知,FunctionGraph 负责执行处理逻辑,二者组合后你只需为「实际调用次数」付费。
最终实现的效果是:
```
用户上传 photo.jpg → OBS(源桶) → 事件通知 → FunctionGraph → 水印图 → OBS(目标桶)
```
---
## 二、整体架构
| 组件 | 作用 | 关键配置 |
|---|---|---|
| OBS 源桶 | 接收用户上传的原图 | 开启事件通知 |
| FunctionGraph 函数 | 下载原图、加水印、回传 | 触发器绑定 OBS 源桶 |
| OBS 目标桶 | 存放加水印后的图片 | 供 CDN / 业务读取 |
| IAM 委托 | 让函数拥有访问 OBS 的权限 | 最小权限:仅目标桶 PutObject |
> 设计要点:**源桶与目标桶分离**,避免函数对同一对象写回造成事件递归(否则会出现「水印触发水印」的死循环)。
---
## 三、函数代码
函数入口文件 `index.py`:
```python
# -*- coding: utf-8 -*-
import os
import io
import json
import logging
from obs import ObsClient
from PIL import Image, ImageDraw, ImageFont
logger = logging.getLogger()
logger.setLevel(logging.INFO)
WATERMARK_TEXT = os.environ.get("WM_TEXT", "Huawei Cloud Demo")
TARGET_BUCKET = os.environ.get("TARGET_BUCKET")
FONT_SIZE = int(os.environ.get("FONT_SIZE", "36"))
def _watermark(img: Image.Image) -> Image.Image:
base = img.convert("RGBA")
overlay = Image.new("RGBA", base.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
try:
font = ImageFont.truetype("DejaVuSans-Bold.ttf", FONT_SIZE)
except OSError:
font = ImageFont.load_default()
w, h = base.size
pad = 20
text_w, text_h = draw.textbbox((0, 0), WATERMARK_TEXT, font=font)[2:4]
pos = (w - text_w - pad, h - text_h - pad)
draw.text(pos, WATERMARK_TEXT, font=font, fill=(255, 255, 255, 160))
return Image.alpha_composite(base, overlay).convert("RGB")
def handler(event, context):
logger.info("event=%s", json.dumps(event))
records = event.get("Records", [])
for rec in records:
s3 = rec.get("s3", {})
bucket = s3.get("bucket", {}).get("name")
key = s3.get("object", {}).get("key")
if not bucket or not key:
continue
ak = context.getAccessKey()
sk = context.getSecretKey()
endpoint = f"obs.{context.getRegion()}.myhuaweicloud.com"
client = ObsClient(access_key_id=ak, secret_access_key=sk, server=endpoint)
resp = client.getObject(bucket, key)
raw = resp.body.buffer if hasattr(resp.body, "buffer") else resp.body.read()
marked = _watermark(Image.open(io.BytesIO(raw)))
out = io.BytesIO()
marked.save(out, format="JPEG", quality=90)
client.putContent(TARGET_BUCKET, key, out.getvalue(),
headers={"Content-Type": "image/jpeg"})
logger.info("watermarked %s -> %s/%s", key, TARGET_BUCKET, key)
client.close()
return "ok"
```
依赖文件 `requirements.txt`:
```
esdk-obs-python==3.23.3
Pillow==10.3.0
```
> **踩坑提示 1**:FunctionGraph 的 Python 运行时默认不含 `esdk-obs-python` 与 `Pillow`,需要把依赖一起打包成 ZIP 上传,或在函数配置页上传「依赖包」。
> **踩坑提示 2**:`context.getAccessKey()/getSecretKey()` 返回的是临时委托凭证,务必在函数上绑定 IAM 委托,不要在代码里硬编码 AK/SK。
---
## 四、部署步骤
### 1. 创建两个桶
```bash
# 使用华为云 CLI(hcloud)
hcloud OBS CreateBucket --bucket=my-src-bucket --location=cn-north-4
hcloud OBS CreateBucket --bucket=my-dst-bucket --location=cn-north-4
```
### 2. 创建函数
```bash
hcloud FunctionGraph CreateFunction \
--func_name=image_watermark \
--runtime=Python3.9 \
--handler=index.handler \
--code_type=zip \
--func_code.file=./image_watermark.zip \
--memory_size=256 \
--timeout=60
```
### 3. 绑定 OBS 触发器
在函数「触发器」页新增 **OBS 触发器**,选择源桶 `my-src-bucket`,事件选择 `ObjectCreated:Put`。
### 4. 配置委托与环境变量
- 为函数绑定包含 `obs:object:GetObject`、`obs:object:PutObject` 的委托;
- 添加环境变量 `TARGET_BUCKET=my-dst-bucket`、`WM_TEXT=你的水印文字`。
### 5. 验证
```bash
hcloud OBS PutObject --bucket=my-src-bucket --key=demo.jpg --file=./demo.jpg
# 几秒后检查目标桶
hcloud OBS ListObjects --bucket=my-dst-bucket
```
---
## 五、成本估算
按每月处理 10 万张 1MB 图片估算(华北-北京四,价格以官网为准):
| 项目 | 用量 | 说明 |
|---|---|---|
| FunctionGraph 调用 | 10 万次 | 每月前 100 万次免费额度内为 0 |
| 函数执行时长 | 约 200 GB·s | 视免费额度而定 |
| OBS 存储 | 约 200 GB | 源图 + 水印图 |
| OBS 请求 | 40 万次 | GET + PUT |
对中小业务而言,**相比常驻一台 2U4G 云服务器,成本通常可下降 60% 以上**,且完全没有运维负担。
---
## 六、可扩展方向
1. **图片压缩 / 格式转换**:在同一函数里加一步 `thumbnail()` 即可;
2. **多规格输出**:为移动端、Web 端分别生成不同尺寸;
3. **改为异步 + 消息队列**:大图处理耗时长时,用 DMS 异步解耦,避免超时;
4. **接入 CDN**:目标桶绑定 CDN 后直接对外提供加速访问。
---
## 七、小结
本文用「OBS 事件通知 + FunctionGraph」实现了一个可上线级别的无服务器图片水印服务,核心代码不到 60 行:
- 事件驱动,按量付费,闲时不花钱;
- 源桶/目标桶隔离,规避事件递归;
- 通过 IAM 委托下发临时凭证,避免密钥硬编码。
如果你正在评估 Serverless 的落地场景,图片处理是一个非常合适的起点。欢迎在评论区交流你遇到的触发器和权限问题。
**参考链接**
- 对象存储 OBS 事件通知文档
- 函数工作流 FunctionGraph Python 开发指南
- 华为云 CLI(hcloud)命令参考
- 点赞
- 收藏
- 关注作者
评论(0)