Documentation

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

    FolderServices
    standardSSH key, security group, standard server, volume, snapshot, snapshot schedule
    vpcNetwork, subnet, firewall rules, server in the network, public IP, port forwarding rule
    databaseManaged database and a snapshot of it

    You can stop after any group. They do not depend on each other.

    Before you start

    1. Install Terraform. Use version 1.11 or later, because the database needs it.
    2. Make a write API key named terraform-test, with an expiry. A read key can plan but not create.
    3. Give Terraform the key and the address of the API in the terminal you will use:
    Terminal
    $ export PANTECHDYNAMICS_API_KEY=PAN_…$ export PANTECHDYNAMICS_BASE_URL=https://api.pantechdynamics.com/public/v1
    1. Make a working folder and three folders inside it:
    Terminal
    $ mkdir terraform-tour && cd terraform-tour$ mkdir standard vpc database

    Keep 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:

    standard/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:

    Terminal
    $ cd standard$ terraform init$ terraform apply

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

    standard/main.tf
    # 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:

    Terminal
    $ terraform plan$ terraform apply

    The 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:

    Terminal
    $ 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.

    HCL
    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:

    vpc/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:

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

    database/main.tf
    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:

    Terminal
    $ export TF_VAR_db_password='a-long-password-you-chose'$ terraform init$ terraform plan$ terraform apply

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

    1. Go to a folder and look at what would be deleted:
    Terminal
    $ cd standard$ terraform plan -destroy
    1. Delete it:
    Terminal
    $ terraform destroy

    Type yes. Deleting a server takes a few minutes, and a database or network can take longer. Wait for Destroy complete!.

    1. Repeat in vpc/ and database/. If you only created some groups, only destroy those.

    2. Check nothing is left. terraform state list should 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:
    Terminal
    $ 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. Run terraform destroy again 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 destroy once more. A failed create is still recorded, and the next destroy removes it.

    Where to go next

    Was this page helpful?

    Updated