Kirim Email dari Google Cloud Run dengan MailAnvil API — Tutorial Lengkap
Kirim Email dari Google Cloud Run dengan MailAnvil API — Tutorial Lengkap
Cloud Run di GCP punya reputasi lebih baik dari Lambda untuk job yang butuh koneksi keluar: container biasa, port apa saja, tanpa batasan egress seperti email. Tapi masalah kirim email tetap sama — SMTP lambat untuk job pendek dan library menambah berat container.
Solusi yang saya pakai: satu fetch() ke MailAnvil API. Tutorial ini Node.js 20, tapi polanya sama untuk Go, Python, atau PHP di Cloud Run.
Kenapa Cloud Run + HTTP API?
- Job pendek. Service yang hanya kirim email selesai dalam <1 detik. Koneksi SMTP (handshake + TLS + EHLO) bisa memakan separuh waktu itu.
- Min-dependency. Image Node tanpa
node_modulessama sekali → build cepat, attack surface kecil. - Retry rapi. Response MailAnvil punya status code + email ID. Error SMTP sering ambigu ("connection dropped" — terkirim atau tidak?).
Step 1: API Key
Simpan sebagai Secret Manager secret, mount sebagai environment variable saat deploy:
echo -n "ma_live_xxx" | gcloud secrets create mailanvil-api-key --data-file=-
gcloud run deploy email-sender \
--source . \
--region asia-southeast2 \
--set-secrets MAILANVIL_API_KEY=mailanvil-api-key:latest \
--allow-unauthenticated=false
Region asia-southeast2 = Jakarta. Latensi ke MailAnvil (jalan di Cloudflare edge) rendah dari sini.
Step 2: Service
// index.mjs
const API = "https://api.mailanvil.com/v1/emails";
import http from "node:http";
const PORT = process.env.PORT || 8080;
const sendEmail = async ({ to, subject, html, from = "[email protected]" }) => {
if (!to || !subject || !html) throw new Error("to, subject, html wajib");
const res = await fetch(API, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.MAILANVIL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ from, to, subject, html }),
});
const data = await res.json();
if (!res.ok) {
const err = new Error(`MailAnvil ${res.status}`);
err.status = res.status;
throw err;
}
return data;
};
http.createServer(async (req, res) => {
if (req.method !== "POST") { res.writeHead(405).end(); return; }
let body = "";
for await (const chunk of req) body += chunk;
try {
const result = await sendEmail(JSON.parse(body));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ id: result.id }));
} catch (e) {
// 4xx = payload/token salah, jangan retry. 5xx = boleh retry.
res.writeHead(e.status && e.status < 500 ? 400 : 502);
res.end(JSON.stringify({ error: e.message }));
}
}).listen(PORT);
Dockerfile:
FROM node:20-slim
COPY index.mjs .
CMD ["node", "index.mjs"]
Tanpa package.json, tanpa npm install. Image ~200MB, build hitungan detik.
Step 3: Panggil dari Cloud Tasks (Queue Pattern)
Untuk email yang tidak boleh hilang (invoice, OTP), jangan panggil service langsung dari request path aplikasi. Pakai Cloud Tasks sebagai queue:
gcloud tasks queue create email-outbound --location=asia-southeast2 \
--max-attempts=5 --min-backoff=10s
Aplikasi utama cukup enqueue task berisi payload email. Cloud Tasks memanggil URL service email-sender, dan kalau service balas 5xx, task di-retry dengan backoff otomatis. Ini gratisan versi sederhana dari antrean email — tanpa Redis, tanpa BullMQ.
Pola penting di handler: 4xx jangan di-retry. Payload salah akan di-coba 5 kali sia-sia lalu masuk DLW. Balas 400 dan Cloud Tasks berhenti.
Step 4: Verifikasi
curl -X POST https://email-sender-xxx-uc.a.run.app \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"to":"[email protected]","subject":"Test Cloud Run","html":"<p>Halo dari Cloud Run</p>"}'
Respons {"id":"..."} = email terkirim. ID dipakai cek status via GET /v1/emails/{id} atau webhook.
Catatan Produksi
- Min instances: kalau latensi kritis (OTP), set
--min-instances=1. Cold start Cloud Run ~500ms–2s tergantung image. - Idempotensi: Cloud Tasks retry bisa kirim ganda. Sertakan
idempotency_keyper pesan di payload MailAnvil supaya retry aman. - Domain: verifikasi SPF/DKIM/DMARC domain pengirim di dashboard MailAnvil sebelum production traffic.
- Monitoring: log-based metric di error log + alert Cloud Monitoring ke Telegram/Slack. Email gagal diam-diam itu bug produksi paling mahal.
Biaya
Cloud Run request 200ms × 256MiB ≈ Rp0,05 per email (free tier 2jt request/bulan menutup sebagian besar startup tahap awal). MailAnvil mulai Rp249rb/bulan — bayar QRIS, tanpa kartu kredit. Total tetap di bawah Rp10 per email transaksional, dan tidak ada bandwidth SMTP yang harus dijaga.