Create a customized image and deploy it as an Azure VM
This guide shows you how to customize a marketplace Azure Linux image to include a simple HTTP application written in Python and then deploy it to Azure as a VM.
Words of caution
This is intended as an example only.
In general, for web applications, it is better to use a dedicated hosting service like Azure App Service, Azure Container Apps, or Azure Kubernetes Service than managing the VMs directly.
In addition, it is a good idea to stick both a load balancer (e.g. Azure Application Gateway) and a CDN (Content Delivery Network) (e.g. Azure Front Door) in front of any HTTP endpoints.
Steps
-
Create a directory to stage all the build artifacts:
STAGE_DIR="<staging-directory>" mkdir -p "$STAGE_DIR"
-
Download Azure Linux VHD file: Download Azure Linux Marketplace Image
-
Move the downloaded VHD file to the staging directory.
mv ./image.vhd "$STAGE_DIR"
-
Create a file named
$STAGE_DIR/image-config.yaml
with the following contents:os: additionalFiles: # Create a basic HTTP app using Flask. - destination: /home/myapp/myapp.py content: | from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "<p>Hello, World!</p>" # Create a systemd service so that the app runs on OS boot. - destination: /usr/local/lib/systemd/system/myapp.service content: | [Unit] Description=My App [Service] Type=exec ExecStart=/home/myapp/venv/bin/waitress-serve --listen 127.0.0.1:8080 myapp:app User=myapp WorkingDirectory=~ [Install] WantedBy=multi-user.target # Use nginx to proxy port 80 to port 8080. # This allows the app to run without root permissions. - destination: /etc/nginx/nginx.conf content: | worker_processes 1; events { worker_connections 1024; } http { include mime.types; server { listen 80; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } } } # Update the iptables rules to allow port 80. - destination: /etc/systemd/scripts/ip4save content: | # init *filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT DROP [0:0] # Allow local-only connections -A INPUT -i lo -j ACCEPT -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT #keep commented till upgrade issues are sorted #-A INPUT -j LOG --log-prefix "FIREWALL:INPUT " -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT # Allow ICMP Time Exceeded - Used for TTL decrementing -A INPUT -p icmp --icmp-type 11 -j ACCEPT # Allot ICMP Destination Unreachable - Used for MTU negotiation -A INPUT -p icmp --icmp-type 3 -j ACCEPT # Open port 80. -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT -A OUTPUT -j ACCEPT COMMIT packages: install: - nginx - python3 - python3-pip services: enable: - nginx - myapp users: - name: myapp startupCommand: /usr/sbin/nologin scripts: postCustomization: # Install the required Python packages for the app. - content: | set -eux python3 -m venv /home/myapp/venv /home/myapp/venv/bin/pip3 install Flask waitress
-
Run Image Customizer to create the new image:
IMG_CUSTOMIZER_TAG="mcr.microsoft.com/azurelinux/imagecustomizer:0.18.0" docker run \ --rm \ --privileged=true \ -v /dev:/dev \ -v "$STAGE_DIR:/mnt/staging:z" \ "$IMG_CUSTOMIZER_TAG" \ --image-file "/mnt/staging/image.vhd" \ --config-file "/mnt/staging/image-config.yaml" \ --build-dir "/mnt/staging/build" \ --output-image-format "vhd-fixed" \ --output-image-file "/mnt/staging/out/image.vhd" \ --log-level debug
-
Create disk in Azure:
DISK_NAME="<disk-name>" VM_RG="<vm-resource-group-name>" VM_LOC="<azure-location>" LOCAL_DISK_PATH="$STAGE_DIR/out/image.vhd" DISK_SIZE="$(stat -c '%s' "$LOCAL_DISK_PATH")" az group create --location "$VM_LOC" --name "$VM_RG" az disk create -n "$DISK_NAME" -g "$VM_RG" --sku standard_lrs --os-type Linux \ --hyper-v-generation V2 --upload-type Upload --upload-size-bytes "$DISK_SIZE"
-
Upload new VHD to Azure:
SAS_JSON="$(az disk grant-access -n "$DISK_NAME" -g "$VM_RG" --access-level Write --duration-in-seconds 86400)" SAS_URL="$(jq -r '.accessSas // .accessSAS' <<< "$SAS_JSON")" azcopy copy "$LOCAL_DISK_PATH" "$SAS_URL" --blob-type PageBlob az disk revoke-access -n "$DISK_NAME" -g "$VM_RG"
-
Create Azure VM:
VM_NAME="<vm-name>" az vm create \ --resource-group "$VM_RG" \ --name "$VM_NAME" \ --attach-os-disk "$DISK_NAME" \ --os-type linux \ --public-ip-sku Standard
-
Open HTTP port:
az vm open-port -g "$VM_RG" -n "$VM_NAME" --port 80
-
Wait for the VM to boot.
-
Query HTTP endpoint:
IP_ADDRESS_JSON="$(az vm list-ip-addresses -g "$VM_RG" -n "$VM_NAME")"
IP_ADDRESS="$(jq -r '.[0].virtualMachine.network.publicIpAddresses.[0].ipAddress' <<< "$IP_ADDRESS_JSON")"
curl -sSL "$IP_ADDRESS"