Object Storage on VNG Cloud vStorage (Terraform)

Provisions S3-compatible object storage buckets on VNG Cloud vStorage and prints a monthly cost estimate from the quoted rate card. Mirrors the layout of deployments/postgres: per-env overlays, a workspace-isolated deploy.sh, git-ignored secrets.

Why the AWS provider (not vngcloud/vngcloud)

vStorage is S3-compatible, and the native vngcloud/vngcloud provider does not expose object storage (it only covers vServer, vDB, vLB, vKS). So buckets are managed with the standard hashicorp/aws provider pointed at the vStorage S3 endpoint, with every AWS-only preflight (STS, IMDS, account-id, region allow-list) switched off and path-style addressing on. See provider.tf.

Files

FilePurpose
provider.tfAWS provider aimed at the vStorage S3 endpoint (skip_* flags)
variables.tfAll inputs and their defaults
main.tfThe bucket(s) + optional versioning
cost.tfMonthly cost estimate arithmetic (creates nothing)
outputs.tfBucket names/URLs + cost estimate outputs
overlays/uat.tfvarsUAT env config (non-secret, committed)
overlays/prod.tfvarsPROD env config
terraform.tfvars.exampleSecrets template (copy to terraform.tfvars)
.env.exampleAlternative secrets via TF_VAR_* (copy to .env)
deploy.shEnv-aware <uat|prod> plan/apply/destroy wrapper
undeploy.shTear down an env’s buckets (destroy) with preview + confirm; --force empties non-empty buckets
.gitignoreKeeps secrets and state out of git

Pricing (quoted rate card)

ItemRateDefault estimate
Storage1 TB = 1,000,000 VND / monthestimated_storage_tb = 1
Bandwidth1 GB = 580 VNDestimated_bandwidth_gb = 200 (inbound + outbound total)

Default estimate → 1 * 1,000,000 + 200 * 580 = 1,116,000 VND/month. terraform plan / terraform output estimated_cost_summary prints the breakdown. These inputs only drive the outputs — they provision nothing. The storage quota is set on the vStorage project (see below), not by Terraform; these estimated_* values are just a forecast to review the monthly bill.

Hierarchy — the vStorage project comes first

vStorage PROJECT   ← created in the console: region + quota/package + billing period
   └── S3 key       ← created inside the project; scopes access_key/secret_key to it
        └── bucket(s)  ← what THIS Terraform manages, over the S3 API

Terraform cannot create the project: the vngcloud/vngcloud provider does not expose vStorage, and project creation is a billing action (pick the storage quota, package, period, auto-renew, then check out) that is not part of the S3 API the aws provider speaks. So the project is a manual prerequisite — like the vIAM service account the postgres module needs. The S3 key you configure is what binds this module to a specific project; Terraform then just manages buckets inside it.

Prerequisites

  • Terraform >= 1.3
  • A vStorage project — create it first at https://vstorage.console.vngcloud.vn → select region → Create a Project → choose quota/package + period. This is where the storage capacity (and its cost) is provisioned.
  • vStorage S3 keys (access_key / secret_key), created inside that project: vStorage console → IAM → Service account → vStorage credentials → Create a S3 key (Secret shown once). The key ties Terraform to the project.
  • The exact S3 endpoint for the project’s region — pattern https://<region>.vstorage.vngcloud.vn. This account’s object-storage regions are hcm04 (default here) and han02 — not hcm03 (that’s a vServer/vDB zone only). Copy the exact endpoint from the console if unsure.

Project creation is console-only for now. Creating a vStorage project via the REST API failed at the billing/order step for this account (code 114, “Could not send order request”) for every payload, so the API bootstrap was dropped. Create the project in the console — the full investigation, the API facts (endpoint, auth, body schema, 30 GB quota min), and the reason are in ../issues/2026-08-17-vstorage-create-project-api-blocked.md.

Usage

cd deployments/storage
cp terraform.tfvars.example terraform.tfvars   # fill in the S3 keys (or use .env)
# edit overlays/uat.tfvars / overlays/prod.tfvars (bucket_names, endpoint, sizing)
 
./deploy.sh uat plan
./deploy.sh uat apply
./deploy.sh prod plan
 
# Tear down (destroy) an env's buckets — previews, then asks you to type the env name:
./undeploy.sh uat            # fails if a bucket still has objects
./undeploy.sh uat --force    # empties non-empty buckets first (deletes ALL objects)

On Windows, run deploy.sh from Git Bash. To drive Terraform directly, select the workspace and pass the overlay yourself: terraform workspace select uat && terraform plan -var-file=overlays/uat.tfvars.

Notes

  • Bucket names are globally unique within the vStorage tenant, so UAT and PROD must use different names (the overlays already do).
  • The shared master-profile projection bucket is configured as master_profile_s3_bucket = "c360-master-profiles" in both environment overlays. It is intentionally not included in either overlay’s environment-specific bucket_names list because UAT and PROD use separate Terraform workspaces while the bucket name is globally unique. Product/content import buckets are separate per environment: c360-product-imports-uat / c360-content-imports-uat and c360-product-imports-prod / c360-content-imports-prod. Terraform provisions these through the product_import_s3_bucket and content_import_s3_bucket variables, and the API verifies or creates both configured buckets during startup. The corresponding names must also be passed to the API and Dagster worker by their deployment env files. When s3_auto_create_buckets = true, server/deploy-api.sh, server/deploy-backend.sh, and server/deploy-tracking.sh perform an idempotent head_bucket/create_bucket bootstrap before starting their containers. The backend deploy also backfills existing master-profile projection objects when the shared bucket is empty.
  • Endpoint: s3_endpoint defaults to HCM04 (this account’s object-storage regions are hcm04 / han02, not hcm03). Override it in the overlay if your project lives elsewhere. region is a SigV4 signing label only and must stay us-east-1 so the AWS SDK omits the bucket LocationConstraint (vStorage rejects any other value with InvalidLocationConstraint).
  • Teardown: ./undeploy.sh <env> destroys the buckets (with a preview + typed confirmation) and leaves the vStorage project alone. S3 won’t delete a non-empty bucket, so add --force to empty it first (deletes all objects, irreversible).
  • Versioning is off in UAT, on in PROD (enable_versioning). Only the versioning API is used beyond bucket create; AWS-only sub-resources (public-access-block, ownership controls) are omitted as vStorage lacks them.
  • Secrets & state: terraform.tfvars, .env, and *.tfstate hold the S3 secret key in plaintext and are git-ignored. Use a remote backend for team use.