DEV Community

Kenneth Phang
Kenneth Phang

Posted on

How I Added Multi-Cloud Support to My Rust CLI — DigitalOcean + Tencent Cloud in One Tool

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
Enter fullscreen mode Exit fullscreen mode

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>>;
}
Enter fullscreen mode Exit fullscreen mode

Both DoClient and TencentClient implement this trait. The deploy command just dispatches:

pub async fn run(params: DeployParams) -> Result<DeployRecord> {
    match resolve_provider(&params.provider)? {
        CloudProviderType::DigitalOcean => run_do(params).await,
        CloudProviderType::Tencent => run_tencent(params).await,
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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())
);
Enter fullscreen mode Exit fullscreen mode

The Authorization header then looks like:

TC3-HMAC-SHA256 Credential=AKIDxxx/2026-03-05/cvm/tc3_request,
SignedHeaders=content-type;host, Signature=abc123...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Backward compatible — --provider defaults to digitalocean.

What I Learned

  1. Trait-based abstraction pays off. Adding a new provider was mostly about implementing the trait — the deploy pipeline barely changed.

  2. 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.

  3. 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.

  4. Base64 user data. Tencent requires base64-encoded cloud-init. DO takes raw text. Easy to miss, hard to debug (cloud-init silently fails).

  5. 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 optionResearch 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
Enter fullscreen mode Exit fullscreen mode

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)