Deploying Compute Instances with Remote Startup Scripts

By Mesue Collins Asibong • Cloud Engineering Compute Instance Startup Scripts

Automating Compute Engine deployments is critical for scalable infrastructure. While passing a local startup script directly via metadata is common for quick tests, externalizing your configuration scripts to Cloud Storage is the enterprise standard. It enables version control, dynamic updates without recreating instances, and strict IAM boundaries.

Remote Script Execution Architecture

flowchart TD classDef gcp fill:#4285F4,stroke:#fff,stroke-width:2px,color:#fff classDef vm fill:#f4a261,stroke:#fff,stroke-width:2px,color:#000 classDef iam fill:#ea4335,stroke:#fff,stroke-width:2px,color:#fff Dev([Developer]) -->|gsutil cp| GCS[(Cloud Storage Bucket)]:::gcp subgraph "Compute Engine (GCE)" VM[VM Instance]:::vm end SA{Service Account}:::iam -.->|IAM Authorization| GCS VM -->|Impersonates| SA VM -->|Boot up| API[Google Metadata Server]:::gcp API -.->|startup-script-url| VM VM -->|HTTP GET| GCS GCS -.->|install-web.sh| VM VM -->|Executes Script| Web[Apache Web Server]:::gcp

Step 1: Setting up the Execution Environment

We begin by defining environment variables for our project, region, and the unique Cloud Storage bucket name we will create. Cloud Storage buckets require globally unique names, so appending the project ID is a best practice.

export PROJECT_ID=$(gcloud config get-value project)
export REGION="us-central1"
export ZONE="us-central1-a"
export BUCKET_NAME="gs://${PROJECT_ID}-startup-scripts"

gcloud config set compute/region $REGION
gcloud config set compute/zone $ZONE

Step 2: Creating the Cloud Storage Bucket

We use the gsutil or the modern gcloud storage command to provision our regional bucket. Keeping the bucket in the same region as the compute instances reduces egress latency and avoids cross-region network charges during the boot sequence.

gcloud storage buckets create $BUCKET_NAME \
    --location=$REGION \
    --uniform-bucket-level-access

Step 3: Writing and Uploading the Bash Script

Our goal is to deploy an Apache web server that automatically serves a custom HTML page upon boot. We'll write the script locally and then upload it to our bucket.

cat << 'EOF' > install-web.sh
#!/bin/bash
apt-get update
apt-get install -y apache2
cat << 'INDEX' > /var/www/html/index.html

Hello World from a Remote Startup Script!

Deployed automatically on boot.

INDEX systemctl restart apache2 EOF gcloud storage cp install-web.sh $BUCKET_NAME/install-web.sh

Step 4: Provisioning the Compute Instance

Now, we create the Compute Engine VM. The critical flag here is --metadata startup-script-url=.... By passing the Cloud Storage URI, the Compute Engine guest environment automatically authenticates using the default compute service account, downloads the script, and executes it as the root user during boot.

gcloud compute instances create web-server-vm \
    --machine-type=e2-micro \
    --image-family=debian-11 \
    --image-project=debian-cloud \
    --scopes=default,storage-ro \
    --tags=http-server \
    --metadata=startup-script-url=${BUCKET_NAME}/install-web.sh

Step 5: Enabling Firewall Rules & Verifying

Because we tagged the instance with http-server, we must ensure the VPC firewall allows inbound traffic on port 80 to that tag.

gcloud compute firewall-rules create default-allow-http \
    --direction=INGRESS \
    --priority=1000 \
    --network=default \
    --action=ALLOW \
    --rules=tcp:80 \
    --source-ranges=0.0.0.0/0 \
    --target-tags=http-server

Once the VM completes its boot cycle (which takes about 30 seconds), grab the external IP address:

EXTERNAL_IP=$(gcloud compute instances describe web-server-vm \
  --format='get(networkInterfaces[0].accessConfigs[0].natIP)')

curl http://$EXTERNAL_IP

Step 6: Optimizing for Idempotency

Startup scripts run on every boot. To prevent unnecessary package re-installations or service disruptions during reboots, scripts must be idempotent. We can optimize our script by checking if the web server is already configured before executing.

Idempotent Execution Flow

flowchart LR classDef state fill:#326ce5,stroke:#fff,stroke-width:2px,color:#fff classDef action fill:#ea4335,stroke:#fff,stroke-width:2px,color:#fff Start([System Boot]) --> Check{Is Apache Installed?} Check -->|Yes| Skip[Skip Installation]:::state Check -->|No| Install[Run apt-get install]:::action Install --> StartService[Enable & Start Service]:::action Skip --> End([Boot Complete]) StartService --> End
cat << 'EOF' > install-web-optimized.sh
#!/bin/bash
if ! command -v apache2 &> /dev/null
then
    echo "Apache not found. Installing..."
    apt-get update
    apt-get install -y apache2
    cat << 'INDEX' > /var/www/html/index.html
    
    

Optimized Remote Startup Script

Idempotent execution verified.

INDEX systemctl enable apache2 systemctl restart apache2 else echo "Apache is already installed. Skipping initialization." fi EOF gcloud storage cp install-web-optimized.sh $BUCKET_NAME/install-web-optimized.sh

Summary

Externalizing startup scripts to Cloud Storage is a foundational practice in Google Cloud. By enforcing idempotency, infrastructure teams can modify the behavior of instances dynamically without ever touching the Instance Template or modifying metadata directly, ensuring clean, scalable configuration management.