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, andterraform validatemust all succeed. Check the billing link and the budget alert in the Console again. Unless a command says otherwise, run it inminecraft-one-rootin 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 Cloudterraform apply, VM creation, and a Java Edition client connection were not performed. Theplanbelow creates nothing, and server costs can begin the moment youapply. The current state islocal 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_rangesa single public IPv4 of yours with/32? - Is the VM’s
provisioning_modelset toSPOTwith a preemption action ofSTOP? - Is a dedicated
minecraft-serverservice account attached to the VM? - Does the boot disk go away together with the VM?
- Is the backup bucket’s
force_destroyset tofalse? - Is
destroyorreplaceattached 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
| Symptom | Check first | Stop condition |
|---|---|---|
403 on VM creation | Deploy account IAM and the service account User role | Do not add permissions by guessing |
| Spot VM creation fails | Spot capacity in the zone, machine type, quota | Do not create it over and over |
| SSH refused | Current public IP, admin_cidr, OS Login | Do not open the firewall to everyone |
| Startup script fails | JAR URL and hash, package log | Do not try to join the game before Done |
| Service restarts repeatedly | journalctl -u minecraft | Read the error before raising memory at random |
| Service is healthy, only the game fails | Listen port and game firewall | Do 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.serviceisactive (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
No comments yet. Be the first to leave one.
Pending review