Reference / Create everything
Create every service, then delete it
A step-by-step Terraform walkthrough that creates each Pantech service, shows how they connect, and then deletes them all.
This guide creates every service the Terraform provider offers, one group at a time, and then deletes them. It is written for someone who has not used Terraform before.
What you will create
The services fall into three groups. Each group goes in its own folder with its own state, so you can create and delete them separately.
| Folder | Services |
|---|---|
standard | SSH key, security group, standard server, volume, snapshot, snapshot schedule |
vpc | Network, subnet, firewall rules, server in the network, public IP, port forwarding rule |
database | Managed database and a snapshot of it |
You can stop after any group. They do not depend on each other.
Before you start
- Install Terraform. Use version 1.11 or later, because the database needs it.
- Make a write API key named
terraform-test, with an expiry. A read key can plan but not create. - Give Terraform the key and the address of the API in the terminal you will use:
$ export PANTECHDYNAMICS_API_KEY=PAN_…$ export PANTECHDYNAMICS_BASE_URL=https://api.pantechdynamics.com/public/v1- Make a working folder and three folders inside it:
$ mkdir terraform-tour && cd terraform-tour$ mkdir standard vpc databaseKeep the key out of every file. The provider reads it from the environment.
Step 1: look up what is available
You choose plans, images and disks by their short names, called slugs. Read the valid ones from your account instead of guessing. In standard/, create catalogue.tf:
terraform {
required_providers {
pantechdynamics = {
source = "Pantech-Dynamics/pantechdynamics"
version = "~> 0.1"
}
}
}
provider "pantechdynamics" {}
data "pantechdynamics_plans" "all" {}
data "pantechdynamics_images" "all" {}
data "pantechdynamics_disk_offerings" "all" {}
data "pantechdynamics_database_engines" "all" {}
output "plans" {
value = [for p in data.pantechdynamics_plans.all.plans : p.slug]
}
output "images" {
value = [for i in data.pantechdynamics_images.all.images : i.slug]
}
output "disk_offerings" {
value = [for d in data.pantechdynamics_disk_offerings.all.disk_offerings : "${d.slug} (${d.storage_type})"]
}Run it. init downloads the provider, and apply only reads, so it costs nothing:
$ cd standard$ terraform init$ terraform applyWrite down a plan slug (the smallest is cheapest), an image slug such as ubuntu-24-04, and a disk offering. The examples below use individual, ubuntu-24-04 and small-local-20gb. Replace them with values from your own output.
Step 2: standard services
Still in standard/, add main.tf. Each block is one resource, and a reference such as pantechdynamics_ssh_key.admin.id tells Terraform to create the SSH key first.
# 1. An SSH key to log in with.
resource "pantechdynamics_ssh_key" "admin" {
name = "tour-admin"
public_key = file("~/.ssh/id_ed25519.pub")
}
# 2. A firewall. Inbound traffic that no rule allows is dropped.
resource "pantechdynamics_security_group" "web" {
name = "tour-web"
rules = [
{ direction = "ingress", protocol = "tcp", port_range = "22", cidr = "203.0.113.0/24" },
{ direction = "ingress", protocol = "tcp", port_range = "443", cidr = "0.0.0.0/0" },
{ direction = "egress", protocol = "all", cidr = "0.0.0.0/0" },
]
}
# 3. A server. This places a paid order.
resource "pantechdynamics_instance" "web" {
name = "tour-web-1"
plan_slug = "individual"
image_slug = "ubuntu-24-04"
ssh_key_id = pantechdynamics_ssh_key.admin.id
security_group_id = pantechdynamics_security_group.web.id
}
# 4. A data disk, attached to the server.
resource "pantechdynamics_volume" "data" {
name = "tour-data"
disk_offering_slug = "small-local-20gb"
instance_id = pantechdynamics_instance.web.id
mount_point = "/data" # a label; nothing is mounted for you
}
# 5. A snapshot of that disk. Snapshot a disk that is attached to a running
# server, as here. A disk that is not attached can fail to snapshot.
resource "pantechdynamics_snapshot" "data" {
name = "tour-data-1"
volume_id = pantechdynamics_volume.data.id
}
# 6. A daily automatic snapshot of the same disk.
resource "pantechdynamics_snapshot_schedule" "data" {
volume_id = pantechdynamics_volume.data.id
frequency = "daily"
retention_count = 7
}
output "web_address" {
value = pantechdynamics_instance.web.private_ipv4
}Change the SSH key path to your own, and the 203.0.113.0/24 network to the one you connect from.
Now plan and apply:
$ terraform plan$ terraform applyThe plan should say 6 to add. Type yes to confirm. Creating the server takes a minute or more, and the volume, the snapshot and the first apply together can take several minutes. If something fails midway, run terraform apply again: Terraform remembers what it already created and carries on.
When it finishes, web_address is the server's address:
$ ssh your_user@$(terraform output -raw web_address)Try a change
Edit main.tf, change name = "tour-web-1" to "tour-web-2", and run terraform plan. It shows ~ for an in-place change: the server is renamed without being replaced. Apply it, or put the name back. A line that says must be replaced means the object would be deleted and created again, so read those carefully.
An encrypted data disk
To protect a data disk at rest, add encrypted = true to the volume. Changing it later replaces the volume, because an existing volume cannot be converted.
resource "pantechdynamics_volume" "secure" {
name = "tour-secure"
disk_offering_slug = "small-local-20gb"
encrypted = true
instance_id = pantechdynamics_instance.web.id
}
output "secure_volume_encryption" {
value = pantechdynamics_volume.secure.encryption_status
}Terraform creates and attaches the volume, but the disk is not encrypted yet. encryption_status is requested until the Pantech volume agent inside the server formats the disk with a key from Pantech, and it becomes encrypted only after the platform confirms that. The agent does the format by itself the moment the volume is attached. A server built from an encrypted image already has the agent and enrolls itself at first boot; on any other server, install the agent and enroll it once. Run terraform refresh afterwards to see the new status. The steps, and what is and is not protected, are in Encrypted data volumes.
encrypted cannot be combined with source_snapshot_id: a volume restored from a snapshot takes its encryption from the snapshot.
Step 3: a private network (VPC)
A VPC is a private network. A server in it has only a private address, and a public IP is how the outside reaches it. In vpc/, create main.tf:
terraform {
required_providers {
pantechdynamics = {
source = "Pantech-Dynamics/pantechdynamics"
version = "~> 0.1"
}
}
}
provider "pantechdynamics" {}
resource "pantechdynamics_ssh_key" "admin" {
name = "tour-vpc-admin"
public_key = file("~/.ssh/id_ed25519.pub")
}
# 1. The network, billed monthly.
resource "pantechdynamics_network" "main" {
name = "tour-main"
cidr = "10.0.0.0/16"
}
# 2. A slice of the network. The range must sit inside the network's.
resource "pantechdynamics_subnet" "web" {
network_id = pantechdynamics_network.main.id
name = "tour-web"
cidr = "10.0.1.0/24"
}
# 3. Firewall rules. They are stateless, so outbound traffic needs its own rule.
resource "pantechdynamics_firewall_rule" "ssh" {
subnet_id = pantechdynamics_subnet.web.id
number = 100
protocol = "tcp"
port_start = 22
cidr = "203.0.113.0/24"
}
resource "pantechdynamics_firewall_rule" "egress" {
subnet_id = pantechdynamics_subnet.web.id
number = 200
direction = "egress"
protocol = "all"
cidr = "0.0.0.0/0"
}
# 4. A server inside the subnet. A VPC server has no security group.
resource "pantechdynamics_instance" "private" {
name = "tour-private-1"
plan_slug = "individual"
image_slug = "ubuntu-24-04"
ssh_key_id = pantechdynamics_ssh_key.admin.id
subnet_id = pantechdynamics_subnet.web.id
}
# 5. A public address that maps to that server.
resource "pantechdynamics_public_ip" "web" {
network_id = pantechdynamics_network.main.id
instance_id = pantechdynamics_instance.private.id
}
output "vpc_address" {
value = pantechdynamics_public_ip.web.address
}Plan and apply as before. A network takes about a minute to create, and a subnet about the same.
Port forwarding
A public IP can instead carry port forwarding rules, which send one public port to one server. Add this to vpc/main.tf if you want to try it:
resource "pantechdynamics_public_ip" "gateway" {
network_id = pantechdynamics_network.main.id
purpose = "port_forwarding"
}
resource "pantechdynamics_port_forwarding_rule" "ssh" {
public_ip_id = pantechdynamics_public_ip.gateway.id
instance_id = pantechdynamics_instance.private.id
public_port_start = 2222
private_port_start = 22
}This sends port 2222 on the gateway address to SSH on the server. The subnet firewall must allow port 22 as well, which the ssh rule above does.
Step 4: a managed database
In database/, add main.tf. The password is a write-only value: Terraform sends it to the API and never saves it in the plan or state. You choose it, and it must be 16 to 128 printable characters with no space, quote or backslash.
terraform {
required_providers {
pantechdynamics = {
source = "Pantech-Dynamics/pantechdynamics"
version = "~> 0.1"
}
}
}
provider "pantechdynamics" {}
variable "db_password" {
type = string
sensitive = true
}
data "pantechdynamics_database_engines" "all" {}
resource "pantechdynamics_database" "orders" {
name = "tour-orders-db"
engine = "postgresql"
version = "18"
plan_slug = "starter"
zone_id = "af-abj-1"
admin_username = "app_admin"
access_rules = ["203.0.113.0/24"] # who may connect
password_wo = var.db_password
password_wo_version = 1
}
resource "pantechdynamics_database_snapshot" "before" {
database_id = pantechdynamics_database.orders.id
name = "tour-before-change"
}
output "db_host" {
value = pantechdynamics_database.orders.hostname
}Check that engine, version and plan_slug are offered to your account: add output "engines" { value = data.pantechdynamics_database_engines.all.engines } and run terraform apply first, or read the list on the databases page.
Pass the password from the environment so it never sits in a file:
$ export TF_VAR_db_password='a-long-password-you-chose'$ terraform init$ terraform plan$ terraform applyA database is paid for when it is ordered, and provisioning takes several minutes. To set a new password later, change password_wo and raise password_wo_version, because Terraform cannot see a write-only value change.
Delete everything
Deleting works the same in every folder. Terraform deletes in the reverse order of creation, so the database snapshot goes before the database, the public IP before the server, and the server before the network.
- Go to a folder and look at what would be deleted:
$ cd standard$ terraform plan -destroy- Delete it:
$ terraform destroyType yes. Deleting a server takes a few minutes, and a database or network can take longer. Wait for Destroy complete!.
-
Repeat in
vpc/anddatabase/. If you only created some groups, only destroy those. -
Check nothing is left.
terraform state listshould print nothing in each folder, and the console should show no servers, volumes, snapshots, networks, public IPs or databases from this tour.
If a delete fails
- A snapshot that failed. A snapshot that never completed can fail to delete (
PROVISIONING_RETRIES_EXHAUSTED), and while it sits in your state Terraform will not delete the volume it belongs to. The platform has already marked it for deletion. Take it out of Terraform's state and carry on, then check the console:
$ terraform state rm pantechdynamics_snapshot.data$ terraform destroy- "In use" or
409. A volume cannot be deleted while attached, and a security group cannot be deleted while a server uses it. Runterraform destroyagain after the server is gone. - A name cannot be used again straight after deleting. The names of deleted security groups and snapshots stay reserved for a while. Wait a few minutes or choose a new name when you run the tour again.
- A resource shows as failed. Run
terraform destroyonce more. A failed create is still recorded, and the next destroy removes it.
Where to go next
- Terraform provider lists every setting, resource and data source.
- The provider's registry page documents every argument.
- API keys explains scopes and expiry.