Skip to content

Commit a029d7c

Browse files
authored
Sample: WebApp with a custom Image (#77)
1 parent 317be46 commit a029d7c

13 files changed

Lines changed: 449 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This repository contains comprehensive sample projects demonstrating how to deve
3434
| [Web App and CosmosDB for NoSQL API ](./samples/web-app-cosmosdb-nosql-api/python/README.md) | Azure Web App using CosmosDB for NoSQL API |
3535
| [Web App and Managed Identities](./samples/web-app-managed-identity/python/README.md) | Azure Web App using Managed Identities |
3636
| [Web App and SQL Database ](./samples/web-app-sql-database/python/README.md) | Azure Web App using SQL Database |
37+
| [Web App with Custom Docker Image](./samples/web-app-custom-image/python/README.md) | Azure Web App running a custom Docker image |
3738
| [ACI and Blob Storage](./samples/aci-blob-storage/python/README.md) | Azure Container Instances with ACR, Key Vault, and Blob Storage |
3839
| [Azure Service Bus with Spring Boot](./samples/servicebus/java/README.md) | Azure Service Bus used by a Spring Boot application |
3940

run-samples.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ SAMPLES=(
3737
"samples/web-app-cosmosdb-mongodb-api/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
3838
"samples/web-app-managed-identity/python|bash scripts/user-assigned.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
3939
"samples/web-app-sql-database/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/get-web-app-url.sh"
40+
"samples/web-app-custom-image/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
4041
"samples/aci-blob-storage/python|bash scripts/deploy.sh|bash scripts/validate.sh"
4142
)
4243

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Azure Web App With Custom Docker Image
2+
3+
This sample demonstrates a Python Flask web application hosted on an Azure Web App using a custom Docker image. The deployment builds the image from the local `src/Dockerfile`, creates an Azure Container Registry resource, and configures a Linux Web App to run the custom image in the LocalStack Azure emulator.
4+
5+
## Architecture
6+
7+
The sample creates the following Azure resources:
8+
9+
1. **Azure Resource Group**: Logical container for all resources in the sample.
10+
2. **Azure Container Registry**: Stores the custom Docker image metadata and credentials.
11+
3. **Azure App Service Plan**: Linux plan used by the Web App.
12+
4. **Azure Web App**: Runs the Flask application from the custom image.
13+
14+
## Prerequisites
15+
16+
- Docker
17+
- Azure CLI
18+
- azlocal CLI
19+
- jq
20+
- LocalStack for Azure
21+
22+
## Deploy
23+
24+
Start LocalStack for Azure and configure Azure CLI interception as described in the repository root README. Then run:
25+
26+
```bash
27+
cd samples/web-app-custom-image/python
28+
bash scripts/deploy.sh
29+
```
30+
31+
The script builds the Docker image from `src/`, creates the App Service resources, and configures the Web App to use the custom image.
32+
33+
## Validate
34+
35+
```bash
36+
bash scripts/validate.sh
37+
```
38+
39+
## Invoke The App
40+
41+
```bash
42+
bash scripts/call-web-app.sh
43+
```
44+
45+
The app exposes:
46+
47+
- `/` for the HTML page
48+
- `/api/status` for a JSON health response
49+
50+
## Local Docker Run
51+
52+
You can run the same image directly with Docker:
53+
54+
```bash
55+
cd src
56+
docker build -t vacation-planner-webapp:v1 .
57+
docker run --rm -p 8080:80 vacation-planner-webapp:v1
58+
curl http://127.0.0.1:8080/api/status
59+
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Web App Custom Image Scripts
2+
3+
These scripts deploy and validate a Python Flask application running on Azure Web App for Containers.
4+
5+
## Deploy
6+
7+
```bash
8+
bash scripts/deploy.sh
9+
```
10+
11+
The deployment script creates:
12+
13+
- Resource group
14+
- Azure Container Registry with admin credentials enabled
15+
- Custom Docker image built from `src/Dockerfile`
16+
- Linux App Service Plan
17+
- Web App configured to use the custom image
18+
19+
If pushing to the emulated registry is unavailable in the current LocalStack environment, the script falls back to the local Docker image tag.
20+
21+
## Validate
22+
23+
```bash
24+
bash scripts/validate.sh
25+
```
26+
27+
## Call The Web App
28+
29+
```bash
30+
bash scripts/call-web-app.sh
31+
```
32+
33+
The call script first uses the LocalStack proxy endpoint and then, when available, calls the Docker host port mapped to the emulated Web App container.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
PREFIX='local'
5+
SUFFIX='test'
6+
RESOURCE_GROUP_NAME="${PREFIX}-custom-image-rg"
7+
WEB_APP_NAME="${PREFIX}-custom-image-webapp-${SUFFIX}"
8+
9+
get_docker_container_name_by_prefix() {
10+
local app_prefix="$1"
11+
docker ps --format "{{.Names}}" | grep "^${app_prefix}" | head -1
12+
}
13+
14+
get_docker_container_port_mapping() {
15+
local container_name="$1"
16+
local container_port="$2"
17+
docker inspect -f "{{(index (index .NetworkSettings.Ports \"${container_port}/tcp\") 0).HostPort}}" "$container_name"
18+
}
19+
20+
APP_HOST_NAME=$(az webapp show \
21+
--name "$WEB_APP_NAME" \
22+
--resource-group "$RESOURCE_GROUP_NAME" \
23+
--query "defaultHostName" \
24+
--output tsv \
25+
--only-show-errors)
26+
27+
if [ -z "$APP_HOST_NAME" ]; then
28+
echo "Failed to retrieve Web App hostname."
29+
exit 1
30+
fi
31+
32+
echo "Web App hostname: $APP_HOST_NAME"
33+
34+
echo "Calling Web App using $APP_HOST_NAME..."
35+
curl -fsS "http://$APP_HOST_NAME/api/status"
36+
echo ""
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Variables
5+
PREFIX='local'
6+
SUFFIX='test'
7+
LOCATION='westeurope'
8+
RESOURCE_GROUP_NAME="${PREFIX}-custom-image-rg"
9+
ACR_NAME="${PREFIX}customimageacr"
10+
APP_SERVICE_PLAN_NAME="${PREFIX}-custom-image-plan-${SUFFIX}"
11+
APP_SERVICE_PLAN_SKU="B1"
12+
WEB_APP_NAME="${PREFIX}-custom-image-webapp-${SUFFIX}"
13+
IMAGE_NAME="custom-image-webapp"
14+
IMAGE_TAG="v1"
15+
CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)"
16+
17+
cd "$CURRENT_DIR" || exit
18+
19+
echo "Creating resource group [$RESOURCE_GROUP_NAME]..."
20+
az group create \
21+
--name "$RESOURCE_GROUP_NAME" \
22+
--location "$LOCATION"
23+
24+
echo "Creating Azure Container Registry [$ACR_NAME]..."
25+
az acr create \
26+
--name "$ACR_NAME" \
27+
--resource-group "$RESOURCE_GROUP_NAME" \
28+
--location "$LOCATION" \
29+
--sku Basic \
30+
--admin-enabled true
31+
32+
az acr login --name $ACR_NAME
33+
34+
LOGIN_SERVER=$(az acr show \
35+
--name "$ACR_NAME" \
36+
--resource-group "$RESOURCE_GROUP_NAME" \
37+
--query "loginServer" \
38+
--output tsv \
39+
--only-show-errors)
40+
41+
FULL_IMAGE="${LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}"
42+
LOCAL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
43+
44+
echo "Building custom Docker image [$LOCAL_IMAGE]..."
45+
docker build -t "$LOCAL_IMAGE" ../src/
46+
docker tag "$LOCAL_IMAGE" "$FULL_IMAGE"
47+
48+
echo "Pushing image [$FULL_IMAGE] to ACR..."
49+
docker push "$FULL_IMAGE"
50+
WEBAPP_IMAGE="$FULL_IMAGE"
51+
52+
53+
echo "Creating Linux App Service Plan [$APP_SERVICE_PLAN_NAME]..."
54+
az appservice plan create \
55+
--resource-group "$RESOURCE_GROUP_NAME" \
56+
--name "$APP_SERVICE_PLAN_NAME" \
57+
--location "$LOCATION" \
58+
--sku "$APP_SERVICE_PLAN_SKU" \
59+
--is-linux
60+
61+
echo "Creating Web App [$WEB_APP_NAME] from custom image [$WEBAPP_IMAGE]..."
62+
63+
az webapp create \
64+
--resource-group "$RESOURCE_GROUP_NAME" \
65+
--plan "$APP_SERVICE_PLAN_NAME" \
66+
--name "$WEB_APP_NAME" \
67+
--container-image-name "$WEBAPP_IMAGE"
68+
69+
echo "Setting Web App container settings..."
70+
az webapp config appsettings set \
71+
--name "$WEB_APP_NAME" \
72+
--resource-group "$RESOURCE_GROUP_NAME" \
73+
--settings \
74+
WEBSITE_PORT="80" \
75+
WEBSITES_PORT="80" \
76+
APP_NAME="Custom Image" \
77+
IMAGE_NAME="$WEBAPP_IMAGE"
78+
79+
echo "Listing resources in resource group [$RESOURCE_GROUP_NAME]..."
80+
az resource list --resource-group "$RESOURCE_GROUP_NAME" --output table
81+
82+
echo ""
83+
echo "Deployment complete."
84+
echo "Resource Group: $RESOURCE_GROUP_NAME"
85+
echo "App Service Plan: $APP_SERVICE_PLAN_NAME"
86+
echo "Web App: $WEB_APP_NAME"
87+
echo "ACR: $ACR_NAME ($LOGIN_SERVER)"
88+
echo "Image: $WEBAPP_IMAGE"
89+
echo ""
90+
echo "Run 'bash scripts/validate.sh' to verify the deployment."
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
PREFIX='local'
5+
SUFFIX='test'
6+
RESOURCE_GROUP_NAME="${PREFIX}-custom-image-rg"
7+
ACR_NAME="${PREFIX}customimageacr"
8+
APP_SERVICE_PLAN_NAME="${PREFIX}-custom-image-plan-${SUFFIX}"
9+
WEB_APP_NAME="${PREFIX}-custom-image-webapp-${SUFFIX}"
10+
11+
echo -e "[$RESOURCE_GROUP_NAME] resource group:\n"
12+
az group show \
13+
--name "$RESOURCE_GROUP_NAME" \
14+
--output table
15+
16+
echo -e "\n[$APP_SERVICE_PLAN_NAME] App Service Plan:\n"
17+
az appservice plan show \
18+
--name "$APP_SERVICE_PLAN_NAME" \
19+
--resource-group "$RESOURCE_GROUP_NAME" \
20+
--output table
21+
22+
echo -e "\n[$ACR_NAME] Azure Container Registry:\n"
23+
az acr show \
24+
--name "$ACR_NAME" \
25+
--resource-group "$RESOURCE_GROUP_NAME" \
26+
--output table
27+
28+
echo -e "\n[$WEB_APP_NAME] Web App:\n"
29+
az webapp show \
30+
--name "$WEB_APP_NAME" \
31+
--resource-group "$RESOURCE_GROUP_NAME" \
32+
--query "{name:name, state:state, defaultHostName:defaultHostName, kind:kind}" \
33+
--output table
34+
35+
echo -e "\n[$WEB_APP_NAME] app settings:\n"
36+
az webapp config appsettings list \
37+
--name "$WEB_APP_NAME" \
38+
--resource-group "$RESOURCE_GROUP_NAME" \
39+
--query "[?name=='IMAGE_NAME' || name=='WEBSITE_PORT' || name=='WEBSITES_PORT'].[name,value]" \
40+
--output table
41+
42+
echo -e "\nResources in [$RESOURCE_GROUP_NAME]:\n"
43+
az resource list \
44+
--resource-group "$RESOURCE_GROUP_NAME" \
45+
--output table
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.git
2+
__pycache__
3+
*.pyc
4+
*.zip
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY . .
9+
10+
ENV PORT=80
11+
EXPOSE 80
12+
13+
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=80"]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
import socket
3+
4+
from flask import Flask, jsonify, render_template
5+
6+
7+
app = Flask(__name__)
8+
9+
10+
@app.route("/")
11+
def index():
12+
return render_template(
13+
"index.html",
14+
app_name=os.environ.get("APP_NAME", "Custom Image Web App"),
15+
image_name=os.environ.get("IMAGE_NAME", "vacation-planner-webapp:v1"),
16+
hostname=socket.gethostname(),
17+
)
18+
19+
20+
@app.route("/api/status")
21+
def status():
22+
return jsonify(
23+
{
24+
"status": "ok",
25+
"app": os.environ.get("APP_NAME", "Custom Image Web App"),
26+
"image": os.environ.get("IMAGE_NAME", "vacation-planner-webapp:v1"),
27+
"hostname": socket.gethostname(),
28+
}
29+
)
30+
31+
32+
if __name__ == "__main__":
33+
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "80")))

0 commit comments

Comments
 (0)