My open-source Rust CLI, clawmacdo, originally only supported DigitalOcean. Last week, I added full Tencent Cloud support — and I want to share how I designed the multi-cloud architecture, implemented TC3-HMAC-SHA256 signing in Rust, and kept the provisioning pipeline provider-agnostic.
The Goal
One CLI that deploys an AI assistant stack to any cloud provider:
# DigitalOcean (existing)
clawmacdo deploy --provider=digitalocean --do-token=xxx
# Tencent Cloud (new!)
clawmacdo deploy --provider=tencent --tencent-secret-id=xxx --tencent-secret-key=xxx
Same 16-step automated deployment. Same result. Different cloud.
The Architecture: CloudProvider Trait
The key insight was that only the infrastructure layer changes per provider. The SSH provisioning pipeline (install Node.js, configure OpenClaw, start the gateway) is identical — it just needs an IP address to SSH into.
So I created a CloudProvider trait:
#[async_trait]
pub trait CloudProvider: Send + Sync {
async fn upload_ssh_key(&self, name: &str, public_key: &str) -> Result<KeyInfo>;
async fn delete_ssh_key(&self, key_id: &str) -> Result<()>;
async fn create_instance(&self, params: CreateInstanceParams) -> Result<InstanceInfo>;
async fn wait_for_active(&self, instance_id: &str, timeout: u64) -> Result<InstanceInfo>;
async fn delete_instance(&self, instance_id: &str) -> Result<()>;
async fn list_instances(&self, tag: &str) -> Result<Vec<InstanceInfo>>;
}
Both DoClient and TencentClient implement this trait. The deploy command just dispatches:
pub async fn run(params: DeployParams) -> Result<DeployRecord> {
match resolve_provider(¶ms.provider)? {
CloudProviderType::DigitalOcean => run_do(params).await,
CloudProviderType::Tencent => run_tencent(params).await,
}
}
Steps 1-4 are provider-specific (create keys, create instance). Steps 5-16 are shared (SSH in, provision, start gateway). Clean separation.
Implementing TC3-HMAC-SHA256 in Rust
Tencent Cloud uses a custom request signing scheme called TC3-HMAC-SHA256. Every API request must be signed with a 4-layer HMAC chain. Here is how it works:
Date Key: HMAC-SHA256("TC3" + SecretKey, date)
Service Key: HMAC-SHA256(dateKey, service)
Signing Key: HMAC-SHA256(serviceKey, "tc3_request")
Signature: HMAC-SHA256(signingKey, stringToSign)
In Rust, using the hmac and sha2 crates:
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
type HmacSha256 = Hmac<Sha256>;
fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key).expect("key");
mac.update(data);
mac.finalize().into_bytes().to_vec()
}
// Build the 4-layer signing key
let secret_date = hmac_sha256(
format!("TC3{}", secret_key).as_bytes(),
date.as_bytes(),
);
let secret_service = hmac_sha256(&secret_date, service.as_bytes());
let secret_signing = hmac_sha256(&secret_service, b"tc3_request");
let signature = hex::encode(
hmac_sha256(&secret_signing, string_to_sign.as_bytes())
);
The Authorization header then looks like:
TC3-HMAC-SHA256 Credential=AKIDxxx/2026-03-05/cvm/tc3_request,
SignedHeaders=content-type;host, Signature=abc123...
This was the trickiest part — getting the canonical request format exactly right. One extra newline or wrong header order and the signature fails silently.
Tencent Cloud API Surface
I implemented these Tencent Cloud APIs:
CVM (Cloud Virtual Machine):
-
RunInstances— Create instances with cloud-init, SSH keys, tags -
DescribeInstances— Poll status, get public IP, list by tag -
TerminateInstances— Destroy instances
KeyPair:
-
ImportKeyPair— Upload SSH public key -
DeleteKeyPairs— Cleanup -
DescribeKeyPairs— List for destroy cleanup
VPC (Security Groups):
-
CreateSecurityGroup— Firewall rules -
CreateSecurityGroupPolicies— SSH (22) + HTTP (80) + HTTPS (443) ingress -
DeleteSecurityGroup— Cleanup on destroy
Resource Mapping
Here is how DigitalOcean and Tencent Cloud concepts map:
| Concept | DigitalOcean | Tencent Cloud |
|---|---|---|
| Compute | Droplet | CVM Instance |
| Auth | Bearer token | TC3-HMAC-SHA256 |
| Region | sgp1 | ap-singapore |
| Size | s-2vcpu-4gb | S5.MEDIUM4 |
| SSH Keys | Account SSH Keys | KeyPair API |
| Firewall | Cloud Firewall | Security Groups (VPC) |
| Tags | Droplet tags | Instance tags |
| User data | cloud-init | cloud-init (base64) |
| Image | ubuntu-24-04-x64 | img-487zeit5 |
One gotcha: Tencent Cloud requires user data to be base64-encoded, while DigitalOcean accepts it as plain text. Small difference, easy to miss.
Web UI: Dynamic Provider Switching
The web UI (built with Axum + SSE) now has a provider dropdown that dynamically swaps:
- Credential fields — DO Token vs Tencent SecretId/SecretKey
- Region options — sgp1/nyc1/lon1 vs ap-singapore/ap-hongkong/ap-tokyo
- Instance sizes — s-2vcpu-4gb vs S5.MEDIUM4
All powered by a single toggleProvider() JavaScript function that rewrites the form fields on change. The SSE progress streaming works identically for both providers.
CLI Changes
Every command now accepts --provider:
# Deploy
clawmacdo deploy --provider=tencent \\
--tencent-secret-id=AKIDxxx \\
--tencent-secret-key=xxx \\
--anthropic-key=sk-ant-xxx
# Status
clawmacdo status --provider=tencent \\
--tencent-secret-id=AKIDxxx \\
--tencent-secret-key=xxx
# Destroy
clawmacdo destroy --provider=tencent \\
--tencent-secret-id=AKIDxxx \\
--tencent-secret-key=xxx \\
--name=openclaw-abc12345
# Migrate (cross-cloud supported!)
clawmacdo migrate --provider=tencent \\
--source-ip=1.2.3.4 \\
--source-key=~/.ssh/id_ed25519 \\
--tencent-secret-id=AKIDxxx \\
--tencent-secret-key=xxx
Backward compatible — --provider defaults to digitalocean.
What I Learned
Trait-based abstraction pays off. Adding a new provider was mostly about implementing the trait — the deploy pipeline barely changed.
TC3 signing is fiddly. The canonical request must have headers in exact alphabetical order, with exact newlines. Debug by comparing your canonical request string byte-by-byte with Tencent is documentation examples.
Security groups are mandatory on Tencent. Unlike DO where droplets are accessible by default, Tencent blocks all inbound traffic unless you create a security group. Forgetting this means SSH will time out and you will have no idea why.
Base64 user data. Tencent requires base64-encoded cloud-init. DO takes raw text. Easy to miss, hard to debug (cloud-init silently fails).
The provisioning pipeline is the real value. 80% of the deploy work (install Node, configure OpenClaw, set up systemd, start gateway) happens over SSH and is completely provider-agnostic. The cloud API is just the bootstrap.
What is Next
- AWS support — EC2 + SigV4 signing (same trait pattern)
- Hetzner Cloud — Popular in Europe, simple API
- Live testing on Tencent — Waiting for account approval
- Docker-based deployment option — Research complete, staying host-based for now
Try It
The tencent branch is live:
git clone https://github.com/kenken64/clawmacdo.git
cd clawmacdo
git checkout tencent
cargo build --release
Or grab a binary from Releases (main branch, tencent coming soon).
Star the repo if this is useful: github.com/kenken64/clawmacdo
The best multi-cloud tool is the one where adding a new provider is just implementing a trait. 🦞🦀
Top comments (0)