Applying a Terraform Plan and Connecting to Minecraft

Reading the first Terraform plan, approving the cost, booting the VM, and connecting from Java Edition

terraform plan computes the changes and stores them in a plan file. Applying the saved plan creates the server resources, and charges can begin from that point.

Prerequisites

scripts/preflight.sh, terraform test, and terraform validate must all succeed. Check the billing link and the budget alert in the Console again. Unless a command says otherwise, run it in minecraft-one-root in Cloud Shell.

Verification scope

The Terraform configuration passed mock provider tests and static checks. In manual verification during July 2026, Minecraft 1.21.8 with Java 21 and Minecraft 26.2 with Java 25 each reached Done (...)! in an Ubuntu 24.04 container. The automated container test kept in the repository pins the 1.21.8 and Java 21 combination. In the 1.21.8 login protocol test, an empty whitelist refused the connection and allowed login after a player was added. A real Google Cloud terraform apply, VM creation, and a Java Edition client connection were not performed. The plan below creates nothing, and server costs can begin the moment you apply. The current state is local and container verification complete, real Google Cloud apply unverified.

The First Plan File

Run: this does not create resources yet

terraform plan -out=minecraft.tfplan
scripts/review-plan.sh minecraft.tfplan

When the plan finishes, Plan: ... to add, ... to change, ... to destroy. appears. The numbers vary with the provider version and the existing project state. Read the objects being created, changed, and destroyed along with the totals. review-plan.sh summarizes the create, update, replace, and destroy counts of a first deployment plan. It fails when an update, a replacement, or a deletion appears, or when a required resource type is missing. After the check finishes, read the terraform show output yourself.

Check

terraform show minecraft.tfplan

In an empty project with fresh state, the following path should connect.

flowchart LR
  player["Java Edition player"] --> ip["Static external IPv4"]
  admin["Administrator public IPv4 /32"] --> ssh["SSH firewall"]
  ip --> game["Game firewall TCP 25565"]
  ssh --> vm["Spot VM"]
  game --> vm
  vm --> disk["Boot disk and world"]
  vm --> sa["VM service account"]
  sa --> backup["Backup bucket"]

Find each of the following in the plan, one at a time.

  • Is game TCP 25565 the only port open to 0.0.0.0/0?
  • Is the SSH source_ranges a single public IPv4 of yours with /32?
  • Is the VM’s provisioning_model set to SPOT with a preemption action of STOP?
  • Is a dedicated minecraft-server service account attached to the VM?
  • Does the boot disk go away together with the VM?
  • Is the backup bucket’s force_destroy set to false?
  • Is destroy or replace attached to a resource you did not expect?

If an error says a network or service account with the same name already exists, do not delete that resource. Decide first whether to use a new name or to check ownership and then run terraform import.

Approval Just Before Costs Begin

Before terraform apply, you can stop without paying to run a server. The plan file fixes what gets created; actual charges depend on usage and the current price list. A Compute Engine VM, disks, a static external IPv4, Cloud Storage, and network usage can all carry charges. A Spot VM is not free either.

Do not apply if you cannot answer any one of the following.

  • Are the project and the billing account the ones I intended?
  • Are the budget alert amount and its recipients correct?
  • Are the allowed SSH address and the public game port correct?
  • Can I run the restore test in part 06 first, without uploading a world that matters?
  • Can I stop the VM with part 08 when it is not in use, and clean everything up with part 10 when I am done running it?

If you decide to continue, apply the saved plan as it is.

Costs money: apply the saved plan

terraform apply minecraft.tfplan

A normal result is Apply complete! and the outputs. If it fails partway on a 403, a quota shortfall, or missing Spot capacity, only some of the resources may exist. Fix the error, create a new plan, and read it again. Do not reapply the old minecraft.tfplan.

Outputs Used to Connect

Run: in minecraft-one-root

export PROJECT_ID="$(
  terraform output -raw minecraft_project_id
)"
export MINECRAFT_INSTANCE="$(
  terraform output -raw minecraft_instance_name
)"
export MINECRAFT_ZONE="$(
  terraform output -raw minecraft_zone
)"
export MINECRAFT_IP="$(
  terraform output -raw minecraft_public_ip
)"
export MINECRAFT_PORT="$(
  terraform output -raw minecraft_game_port
)"
export VM_SERVICE_ACCOUNT="$(
  terraform output -raw minecraft_service_account
)"

If a command prints no value, check that you are in the directory where apply finished and that the backend matches.

OS Login Permissions

The VM uses enable-oslogin = "TRUE" in metadata. Instead of attaching SSH keys to the VM configuration, Google accounts and IAM decide who connects.

Run: in minecraft-one-root

export ADMIN_ACCOUNT="$(
  gcloud auth list --filter=status:ACTIVE \
    --format="value(account)"
)"

gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member="user:${ADMIN_ACCOUNT}" \
  --role="roles/compute.osAdminLogin"

gcloud iam service-accounts add-iam-policy-binding \
  "${VM_SERVICE_ACCOUNT}" \
  --member="user:${ADMIN_ACCOUNT}" \
  --role="roles/iam.serviceAccountUser"

The bindings in each result should show the current account with the role you granted. For an account outside the organization, an organization administrator may need to add roles/compute.osLoginExternalUser.

VM, systemd, Minecraft

systemd on Ubuntu starts background programs such as Minecraft and records their state. systemctl reads service state, and journalctl reads the logs systemd collected.

1. Is the VM running?

Check: every 10 seconds for up to 5 minutes

(
  for attempt in $(seq 1 30); do
    status="$(
      gcloud compute instances describe "${MINECRAFT_INSTANCE}" \
        --zone="${MINECRAFT_ZONE}" \
        --format="value(status)"
    )"
    printf 'VM status: %s\n' "${status}"
    [[ "${status}" == "RUNNING" ]] && break
    [[ "${status}" == "TERMINATED" ]] && {
      echo "VM stopped before startup completed." >&2
      exit 1
    }
    sleep 10
  done
  [[ "${status}" == "RUNNING" ]]
)

The whole block is wrapped in parentheses, so the exit inside it does not close the current Cloud Shell tab. It spans several lines, but copy it whole and paste it at once.

RUNNING means the operating system booted. The Java installation and the server download may still be in progress. On TERMINATED, check the apply errors or the Spot preemption record first.

2. Is the systemd service running?

The first boot installs packages, so it can take a few minutes. Read the startup script log.

The first time you run gcloud compute ssh with this account, it announces that it will create an SSH key and asks Enter passphrase (empty for no passphrase):. For this walkthrough, type nothing and press Enter twice to continue with an empty passphrase. The key is stored under $HOME/.ssh in Cloud Shell, and the same session does not ask again.

Check: every 15 seconds until SSH is ready, for up to 5 minutes

(
  for attempt in $(seq 1 20); do
    if gcloud compute ssh "${MINECRAFT_INSTANCE}" \
      --zone="${MINECRAFT_ZONE}" \
      --command="sudo journalctl \
        -b \
        -u google-startup-scripts.service \
        -n 150 \
        --no-pager"; then
      break
    fi
    [[ "${attempt}" -eq 20 ]] && exit 1
    sleep 15
  done
)

If you see SHA-256 mismatch, a download failure, or a package installation error, fix that input before checking the Minecraft service.

Check: in Cloud Shell

gcloud compute ssh "${MINECRAFT_INSTANCE}" \
  --zone="${MINECRAFT_ZONE}" \
  --command="sudo systemctl status minecraft --no-pager"

active (running) means the Java process is alive. On failed, start from the first error in the log below.

3. Has Minecraft finished loading the world?

Check: in Cloud Shell

gcloud compute ssh "${MINECRAFT_INSTANCE}" \
  --zone="${MINECRAFT_ZONE}" \
  --command="sudo journalctl \
    -b \
    -u minecraft \
    -n 120 \
    --no-pager"

A line in the following form is the ready signal.

Done (12.345s)! For help, type "help"

Connecting on active alone can fail while the world is still being generated. If Done does not appear, resolve the error in the log first.

First Java Edition Connection

The first boot already carries white-list=true and enforce-whitelist=true. The server runs, but the empty whitelist keeps every player out. Open the Minecraft console once over SSH and register the first operator and your friends.

Run: interactive SSH

gcloud compute ssh "${MINECRAFT_INSTANCE}" \
  --zone="${MINECRAFT_ZONE}"

Once the VM prompt opens, stop the service and run the same JAR in the foreground. sudo -u minecraft runs it as the same minecraft user the service uses, and nogui opens only the console, without a graphical window.

sudo systemctl stop minecraft
cd /srv/minecraft
sudo -u minecraft /usr/bin/java \
  -Xms2G -Xmx4G \
  -jar server.jar nogui

This console is tied to the SSH connection. If SSH drops before you finish registering, the server shuts down with it. In that case, connect over SSH again, run sudo systemctl start minecraft first, and repeat this procedure from the beginning.

After Done (...)!, type the following commands one line at a time. Drop the angle brackets and put in your Java Edition profile name. The profile name is that name exactly, capitalization included, as shown in the account menu at the top right of the Minecraft launcher or on the in-game pause screen.

whitelist add <your-player-name>
op <your-player-name>
whitelist add <friend-player-name>
whitelist list
stop

Skip the friend line if there is no friend to add. Check for Added ... to the whitelist and the operator message. When stop finishes and the VM shell prompt returns, start the systemd service again.

sudo systemctl start minecraft
sudo systemctl is-active minecraft
exit

Do not move on to joining the game if the service is not active or the console commands could not find the name. Turning the whitelist off to run a fully public server is out of scope for this beginner track.

Check

printf 'Minecraft address: %s:%s\n' \
  "${MINECRAFT_IP}" \
  "${MINECRAFT_PORT}"

In Minecraft Java Edition, open Multiplayer → Add Server and put the printed address into Server Address. With the default port 25565, either IP:25565 or the IP alone works. A client and a server on different versions produce a version error.

Place one test block in the new world and remember where it is. Do not move a world that matters yet.

When the Connection Fails

SymptomCheck firstStop condition
403 on VM creationDeploy account IAM and the service account User roleDo not add permissions by guessing
Spot VM creation failsSpot capacity in the zone, machine type, quotaDo not create it over and over
SSH refusedCurrent public IP, admin_cidr, OS LoginDo not open the firewall to everyone
Startup script failsJAR URL and hash, package logDo not try to join the game before Done
Service restarts repeatedlyjournalctl -u minecraftRead the error before raising memory at random
Service is healthy, only the game failsListen port and game firewallDo not widen the SSH port range

Most of the “Check first” column is reachable with commands already shown in this post. When SSH is refused, compare the current public IP against admin_cidr with curl https://api4.ipify.org (if it changed, follow the update procedure in part 08), and for startup script and service problems run the journalctl -u google-startup-scripts.service and journalctl -u minecraft checks above again, reading from the first error line.

External Connectivity After a Server Migration lays out the check path from the firewall to the Minecraft port.

Completion Check

  • The VM status is RUNNING.
  • minecraft.service is active (running).
  • The log contains Done (...)!.
  • The first operator and the players you allow are on the whitelist.
  • A Java Edition client connects over the static IPv4.

The test block you placed in the connected world becomes the restore criterion in Minecraft World Backup and Restore Test.

References

Comments

Comments

    Image preview