# 从 0 到 1:基于华为云 OBS + 函数工作流打造「无服务器」图片水印服务

举报
yd_230653297 发表于 2026/10/23 00:28:57 2026/10/23
【摘要】 # 从 0 到 1:基于华为云 OBS + 函数工作流打造「无服务器」图片水印服务> 摘要:本文带你用华为云对象存储(OBS)事件通知 + 函数工作流(FunctionGraph),零服务器成本搭建一个「上传即自动加水印」的图片处理服务。全文含可复制代码、部署脚本、成本估算与踩坑记录,适合想入门 Serverless 的开发者。**适用人群**:有 Python 基础、想了解华为云 Serv...

# 从 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)命令参考

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。