ZIP Deployments and the Terraform Import Record
How deployment archives were built and existing Google Cloud resources were attached to Terraform state
All three working directories in the 2025 repository had an import script. Each script records an existing Google Cloud resource being attached to an HCL resource address and state.
terraform import google_compute_address.minecraft_static_ip ^
projects/<PROJECT_ID>/global/addresses/<STATIC_IP_NAME>
terraform import google_storage_bucket.backup_bucket <BACKUP_BUCKET>
terraform import google_compute_instance.minecraft_server ^
projects/<PROJECT_ID>/zones/<ZONE>/instances/<INSTANCE_NAME>
import is the command that attaches an already existing resource to state. Read together, the
import targets and preserved state show that the static IP, bucket, and VM existed first and
Terraform began managing them later.
I reconstructed the deploy procedure of the time from the deployment archives, the batch files, and the import scripts. Every quote below is code left in the repository, with the real identifiers replaced by placeholders. Which command succeeded and when cannot be confirmed, because there are no execution logs.
The Line That Builds the ZIP and the Line That Applies It
As seen in
part 21, the
server and preemption recovery directories each had one batch file, shaped to bundle the source into
a ZIP and then run terraform apply -auto-approve. No apply batch file of the same shape survives in
the backup cleanup directory.
filebase64 reads the ZIP that the batch file just built. A changed ZIP therefore changed the
metadata value and the VM attribute. As described in
part 22, the command that
builds the application and the command that applies the
infrastructure run one after the other in a single file, and the results mix into the same plan.
Add -auto-approve to that, and the approval input step between the plan output and the actual
changes disappears.
Three Function ZIPs Left Behind
The preemption recovery directory has three function deployment archives left in it.
function.zip 2,301 bytes June 27
function_<TIMESTAMP>.zip 2,119 bytes June 20
function_<TIMESTAMP>.zip 2,119 bytes June 20
The two with a timestamp in the name were created 25 seconds apart and are the same size. Then a week
later, function.zip with no timestamp was left at a larger size. Terraform references only the last
of those names.
resource "google_storage_bucket_object" "function_zip" {
name = "function-${filesha256("function.zip")}.zip"
...
source = "function.zip"
}
No command reads the two timestamped files. They look like copies made during a deploy, but which one actually went up cannot be told from the files alone. The source directory and the deployment archive are separate files, and there was nothing checking that the two held the same content.
With filesha256 in the object name, changing the ZIP content changes the object name and makes the
function reference the new object. The hash covers the ZIP file, not the source directory. Apply
without rebuilding the ZIP and the old archive goes up as
it was.
The Order the Import Scripts Left Behind
The three import scripts are a record that each resource existed before the code. The file in the server directory was seven lines: one static IP, one bucket, four firewalls, and one instance.
One resource is missing from the import list. The main.tf at the time managed eight
targets, five of which were firewalls. One firewall, the one for the map plugin, is missing from the
import list.
A resource missing from the import list shows up in the next plan as something to create. The seven
that were imported have their state connected to the real resources, but Terraform sees the missing
one as a resource that does not exist yet. The next apply tries to create it, and if something with
the same name is already there it fails. Run it with -auto-approve and you learn about that
difference only after apply has stopped.
The state that was kept has this firewall in it as a managed target too. How and when it was attached is not in the repository. The script may have been fixed and run again, or a single line may have been run by hand.
The import script in the preemption recovery directory has numbers on it from [1/12] through
[11/12]. There is no twelfth step, and the actual terraform import lines number fifteen. They
attach, in order, the bucket, the Pub/Sub topic, the function, three IAM bindings, the log metric, the
notification channel, the alert policy, and five APIs.
main.tf manages fifteen resources, while what this list points at is fourteen once one duplicate
line is removed. The one missing is the function source object, which Terraform creates fresh from
the ZIP.
Two lines are left as placeholders. Below, the angle brackets are values masked in this post, and the square brackets are unfilled blanks that were in the original script as they are.
terraform import google_monitoring_notification_channel.pubsub_channel ^
<PROJECT_ID>/notificationChannels/[CHANNEL_ID]
terraform import google_monitoring_alert_policy.alert_policy ^
<PROJECT_ID>/alertPolicies/[ALERT_POLICY_ID]
[CHANNEL_ID] and [ALERT_POLICY_ID] have to be replaced with real values before running. The
numbering breaks off partway and the placeholders remain, so this file cannot establish that it was
run from start to finish.
The last line imports the same log metric twice, in two different ID formats.
terraform import google_logging_metric.vm_stopped_metric ^
projects/<PROJECT_ID>/metrics/<LOG_METRIC_NAME>
...
terraform import google_logging_metric.vm_stopped_metric <LOG_METRIC_NAME>
It has the shape of writing down both without being sure which format was right. If one of them succeeds first, the other raises an “already managed” error.
The shell script in the backup cleanup directory handled the same uncertainty a different way.
terraform import \
google_storage_bucket.function_source_bucket \
"${FUNC_SOURCE_BUCKET}" || true
Every import has || true on it. The intent appears to be keeping the script from stopping when an
already imported resource is called again. In exchange it swallows real failures too. Whether it
failed for lack of permission or because the resource name was wrong, the script runs to the end and
finishes as a success. How far it actually got has to be read from a separate terraform plan
afterward.
The Same Value Written Separately in the Batch File and the Variables File
The server import batch file declares constants at the top.
set PROJECT_ID=<PROJECT_ID>
set ZONE=<ZONE>
set BACKUP_BUCKET=<BACKUP_BUCKET>
set MINECRAFT_PORT=<GAME_PORT>
set RCON_PORT=<RCON_PORT>
The same values were also in the defaults in variables.tf. The recovery-side batch file declared the
project, region, zone, instance, and function names on its own, and the backup cleanup side was a
shell script, so the declaration form differed again. Changing the project ID meant editing the
variables file and the script together, and fixing only one side makes the import target and the apply
target diverge.
Deployment Conditions Left Outside the Code
Laid out in order, the deploy procedure of the time went like this.
flowchart LR A["Create resources with Console·gcloud"] --> B["Write HCL"] B --> C["Attach to state with import scripts"] C --> D["Build the ZIP"] D --> E["apply -auto-approve"] E --> F["Check the result in the output"]
There were real circumstances behind this order. The server was already running and players were connected. Wiping everything and rebuilding from code was not the path chosen then. Keeping the existing resources while putting them under Terraform management required import.
The problem was what came after. There was no step in the procedure to check how far import had
succeeded. Had I saved and read a terraform plan, the one missing firewall would have shown as “to
be created.” -auto-approve skipped the approval input after the plan output.
The deploy result was not determined by the HCL in the repository alone. It varied with which ZIP happened to be in the directory at that moment, how far import had been run, and whether the constants in the batch file matched the variable defaults. To build the same result again from the same code, these conditions outside the code have to be matched too.
What the Build Track Does Differently Now
The build track saves the plan to a file and applies only the saved plan.
terraform plan -out=minecraft.tfplan
scripts/review-plan.sh minecraft.tfplan
terraform apply minecraft.tfplan
The review script in the middle fails if the first deployment plan contains updates, replacements,
or deletions, or omits a required resource type. A plan that should contain only creations stops if
other actions appear. A partial import can still pass, however, when every remaining resource is
shown as a creation. A remote resource with the same name might also remain invisible until apply.
That is why a person still has to compare the import list with terraform show.
When an error says a resource with the same name already exists, the build track says to decide whether to use a new name or confirm ownership and import it. The review script does not make that ownership decision.
The deployment archive was cut back outright. The server JAR is not put in as a ZIP; it is passed as a
URL and a SHA-256. The example root in
part 04 includes
.terraform.lock.hcl, so the provider version is pinned.
Import was a path for moving running resources under Terraform management. The procedure had no step to check what had been attached and what was left, and the three preserved states show resources that were subsequently managed in more than one place.
References
Comments
No comments yet. Be the first to leave one.
Pending review