Compare commits
8 Commits
055f51aa55
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| de9ba78089 | |||
| 3c4e10eda7 | |||
| 287dfc0b8b | |||
| 5561f10958 | |||
| 85dd574991 | |||
| 4f97dc3362 | |||
| 9100f71ab5 | |||
| 0d12f24dec |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Sławomir Koszewski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
84
README.md
84
README.md
@@ -1,46 +1,78 @@
|
||||
# Azure Image Chooser
|
||||
|
||||
Azure Image Chooser is a [Streamlit](https://streamlit.io) application that allows users to select Azure VM images from the Azure Marketplace.
|
||||
Azure Image Chooser is a Node.js application that allows users to select Azure VM images from the Azure Marketplace.
|
||||
|
||||
Azure Image Chooser is written in Python and requires Python interpreter. At the time of writing this, Python 3.13 is the latest.
|
||||
Azure Image Chooser has a TypeScript backend (Express) and a React frontend (Vite). At the time of writing this, Node.js 24 is used by the container image and is the recommended version for local runs.
|
||||
|
||||
You can run it on your local machine or deploy to any platform that runs containers. A Docker file and Terraform code to deploy to the Azure are provided.
|
||||
You can run it on your local machine or deploy to any platform that runs containers.
|
||||
|
||||
## Environment variables
|
||||
|
||||
Required for both local and container run:
|
||||
|
||||
- `AZURE_SUBSCRIPTION_ID`: Azure subscription used for Marketplace queries.
|
||||
|
||||
Authentication variables (only needed when identity is not provided by the environment):
|
||||
|
||||
- `AZURE_TENANT_ID`: Microsoft Entra tenant ID for service principal auth.
|
||||
- `AZURE_CLIENT_ID`: Service principal (app registration) client ID.
|
||||
- `AZURE_CLIENT_SECRET`: Service principal client secret.
|
||||
|
||||
Optional:
|
||||
|
||||
- `PORT`: Backend listening port. Default is `3000`.
|
||||
|
||||
Local run notes:
|
||||
|
||||
- `AZURE_SUBSCRIPTION_ID` must be set.
|
||||
- Use either Azure CLI login (`az login`) or the service principal variables above.
|
||||
|
||||
Container run notes:
|
||||
|
||||
- `AZURE_SUBSCRIPTION_ID` must be passed to the container.
|
||||
- Pass `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET` unless your container runtime provides identity (for example Managed Identity / Workload Identity).
|
||||
|
||||
## Running on a local machine
|
||||
|
||||
Create a Python development environment file `.env`:
|
||||
Load environment variables from the repository environment file:
|
||||
|
||||
```shell
|
||||
AZURE_SUBSCRIPTION_ID="subscription_id"
|
||||
AZURE_CLIENT_ID="client_id"
|
||||
AZURE_CLIENT_SECRET="client_secret"
|
||||
AZURE_TENANT_ID="tenant_id"
|
||||
AZURE_LOCATION="westeurope"
|
||||
set -a; source azure.env; set +a
|
||||
```
|
||||
|
||||
> NOTE: Replace the placeholder values with your actual values. Omit `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, and `AZURE_TENANT_ID` if you are using Azure CLI authentication.
|
||||
|
||||
Execute the following commands to run the app:
|
||||
Execute the following commands to install dependencies, build, and run the app:
|
||||
|
||||
```shell
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install pip --upgrade
|
||||
pip install -r requirements.txt
|
||||
cd app
|
||||
streamlit run image-chooser.py
|
||||
cd app/backend
|
||||
npm ci
|
||||
|
||||
cd ../frontend
|
||||
npm ci
|
||||
|
||||
cd backend
|
||||
npm run build
|
||||
|
||||
cd ../frontend
|
||||
npm run build
|
||||
|
||||
cd ../backend
|
||||
npm run start
|
||||
```
|
||||
|
||||
The app will block terminal and start a web server. Follow the instructions in the terminal to access the app.
|
||||
The app will block the terminal and start a web server on port 3000. Open http://localhost:3000 in your browser.
|
||||
|
||||
## Add Certificate Binding for Azure Deployment
|
||||
## Running with Docker
|
||||
|
||||
Unfortunately, as of now, the Terraform Azure Resource Manager Provider does not support binding certificates to container apps. You can still bind the certificate using the Azure CLI.
|
||||
Build and run the container:
|
||||
|
||||
```shell
|
||||
# Find the certificate name
|
||||
CERTIFICATE_LOWERCASE_NAME=$(az containerapp env certificate list -g $RESOURCE_GROUP -n $ENVIRONMENT --query '[].name' -o tsv)
|
||||
|
||||
# Bind the certificate to the container app
|
||||
az containerapp hostname bind --hostname $DOMAIN_NAME -g $RESOURCE_GROUP -n $CONTAINER_APP --environment $ENVIRONMENT --certificate $CERTIFICATE_LOWERCASE_NAME --validation-method CNAME
|
||||
docker build -t azure-image-chooser-node ./app
|
||||
docker run --rm -p 3000:3000 \
|
||||
-e AZURE_SUBSCRIPTION_ID="subscription_id" \
|
||||
-e AZURE_CLIENT_ID="client_id" \
|
||||
-e AZURE_CLIENT_SECRET="client_secret" \
|
||||
-e AZURE_TENANT_ID="tenant_id" \
|
||||
azure-image-chooser-node
|
||||
```
|
||||
|
||||
> NOTE: As with local runs, you can omit AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, and AZURE_TENANT_ID when the runtime environment already provides Azure credentials.
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
FROM node:24-trixie-slim AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY app-new/backend/package*.json backend/
|
||||
COPY app-new/frontend/package*.json frontend/
|
||||
RUN cd backend && npm ci
|
||||
RUN cd frontend && npm ci
|
||||
|
||||
COPY app-new .
|
||||
|
||||
RUN cd backend && npm run build
|
||||
RUN cd frontend && npm run build
|
||||
RUN cd backend && npm prune --omit=dev
|
||||
|
||||
FROM node:24-trixie-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
COPY --from=build /app/dist dist
|
||||
COPY --from=build /app/templates templates
|
||||
COPY --from=build /app/templates.json templates.json
|
||||
COPY --from=build /app/backend/node_modules dist/backend/node_modules
|
||||
|
||||
WORKDIR /app
|
||||
COPY entrypoint.sh entrypoint.sh
|
||||
COPY healthcheck.js healthcheck.js
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 CMD ["node", "/app/healthcheck.js"]
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
exec node /app/dist/backend/server.js
|
||||
@@ -1,22 +0,0 @@
|
||||
[
|
||||
{
|
||||
"label": "Terraform VM image reference",
|
||||
"language": "hcl",
|
||||
"file": "azurerm_hcl.tpl"
|
||||
},
|
||||
{
|
||||
"label": "Azure CLI",
|
||||
"language": "shell",
|
||||
"file": "shell.tpl"
|
||||
},
|
||||
{
|
||||
"label": "Azure Resource Manager Template",
|
||||
"language": "json",
|
||||
"file": "arm_vm.jsonc"
|
||||
},
|
||||
{
|
||||
"label": "Bicep VM image reference",
|
||||
"language": "bicep",
|
||||
"file": "bicep_vm.tpl"
|
||||
}
|
||||
]
|
||||
15
app-streamlit/Dockerfile
Normal file
15
app-streamlit/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --root-user-action=ignore --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY image-chooser.py .
|
||||
COPY templates/ templates/
|
||||
COPY templates.json .
|
||||
COPY ./entrypoint.sh /
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
CMD [ "run", "image-chooser.py" ]
|
||||
46
app-streamlit/README.md
Normal file
46
app-streamlit/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Azure Image Chooser
|
||||
|
||||
Azure Image Chooser is a [Streamlit](https://streamlit.io) application that allows users to select Azure VM images from the Azure Marketplace.
|
||||
|
||||
Azure Image Chooser is written in Python and requires Python interpreter. At the time of writing this, Python 3.13 is the latest.
|
||||
|
||||
You can run it on your local machine or deploy to any platform that runs containers. A Docker file and Terraform code to deploy to the Azure are provided.
|
||||
|
||||
## Running on a local machine
|
||||
|
||||
Create a Python development environment file `.env`:
|
||||
|
||||
```shell
|
||||
AZURE_SUBSCRIPTION_ID="subscription_id"
|
||||
AZURE_CLIENT_ID="client_id"
|
||||
AZURE_CLIENT_SECRET="client_secret"
|
||||
AZURE_TENANT_ID="tenant_id"
|
||||
AZURE_LOCATION="westeurope"
|
||||
```
|
||||
|
||||
> NOTE: Replace the placeholder values with your actual values. Omit `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, and `AZURE_TENANT_ID` if you are using Azure CLI authentication.
|
||||
|
||||
Execute the following commands to run the app:
|
||||
|
||||
```shell
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install pip --upgrade
|
||||
pip install -r requirements.txt
|
||||
cd app
|
||||
streamlit run image-chooser.py
|
||||
```
|
||||
|
||||
The app will block terminal and start a web server. Follow the instructions in the terminal to access the app.
|
||||
|
||||
## Add Certificate Binding for Azure Deployment
|
||||
|
||||
Unfortunately, as of now, the Terraform Azure Resource Manager Provider does not support binding certificates to container apps. You can still bind the certificate using the Azure CLI.
|
||||
|
||||
```shell
|
||||
# Find the certificate name
|
||||
CERTIFICATE_LOWERCASE_NAME=$(az containerapp env certificate list -g $RESOURCE_GROUP -n $ENVIRONMENT --query '[].name' -o tsv)
|
||||
|
||||
# Bind the certificate to the container app
|
||||
az containerapp hostname bind --hostname $DOMAIN_NAME -g $RESOURCE_GROUP -n $CONTAINER_APP --environment $ENVIRONMENT --certificate $CERTIFICATE_LOWERCASE_NAME --validation-method CNAME
|
||||
```
|
||||
8
app-streamlit/entrypoint.sh
Executable file
8
app-streamlit/entrypoint.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$1" = "-s" ] || [ "$1" = "--shell" ]; then
|
||||
shift
|
||||
exec bash $@
|
||||
fi
|
||||
|
||||
exec streamlit $@
|
||||
17
app-streamlit/templates.json
Normal file
17
app-streamlit/templates.json
Normal file
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"label": "Terraform VM image reference",
|
||||
"language": "hcl",
|
||||
"file": "azurerm_hcl.tpl"
|
||||
},
|
||||
{
|
||||
"label": "Azure CLI",
|
||||
"language": "shell",
|
||||
"file": "shell.tpl"
|
||||
},
|
||||
{
|
||||
"label": "Azure Resource Manager Template",
|
||||
"language": "json",
|
||||
"file": "arm_vm.jsonc"
|
||||
}
|
||||
]
|
||||
@@ -1,15 +1,50 @@
|
||||
FROM python:3.13-slim
|
||||
# Build stage
|
||||
FROM node:24-trixie-slim AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
# Copy sources required for build
|
||||
COPY backend/*.json backend/
|
||||
COPY backend/src backend/src
|
||||
|
||||
RUN pip install --root-user-action=ignore --no-cache-dir -r requirements.txt
|
||||
COPY frontend/*.json frontend/
|
||||
COPY frontend/vite.config.ts frontend/vite.config.ts
|
||||
COPY frontend/index.html frontend/index.html
|
||||
COPY frontend/src frontend/src
|
||||
COPY frontend/test frontend/test
|
||||
|
||||
COPY image-chooser.py .
|
||||
COPY templates/ templates/
|
||||
COPY templates.json .
|
||||
COPY ./entrypoint.sh /
|
||||
COPY templates templates
|
||||
COPY templates.json templates.json
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
CMD [ "run", "image-chooser.py" ]
|
||||
# Build backend and frontend
|
||||
RUN cd backend && npm ci && npm run build && npm prune --omit=dev
|
||||
RUN cd frontend && npm ci && npm run build
|
||||
|
||||
# Build the container
|
||||
FROM node:24-trixie-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built artifacts
|
||||
COPY --from=build /app/dist dist
|
||||
COPY --from=build /app/templates templates
|
||||
COPY --from=build /app/templates.json templates.json
|
||||
COPY --from=build /app/backend/node_modules dist/backend/node_modules
|
||||
|
||||
# Copy entrypoint and healthcheck scripts
|
||||
COPY entrypoint.sh entrypoint.sh
|
||||
COPY healthcheck.js healthcheck.js
|
||||
|
||||
# Ensure entrypoint script is executable
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
# Set environment variables and expose port
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
|
||||
# Configure health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 CMD ["node", "/app/healthcheck.js"]
|
||||
|
||||
# Configure entrypoint
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "azure-image-chooser-backend",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"author": "Sławomir Koszewski",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
import { AzureImageService } from "./azure-service";
|
||||
import { TemplateService } from "./template-service";
|
||||
|
||||
const findAppNewRoot = (): string => {
|
||||
const findAppRoot = (): string => {
|
||||
const candidates = [join(__dirname, "../../.."), join(__dirname, "../..")];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
@@ -15,7 +15,7 @@ const findAppNewRoot = (): string => {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unable to resolve app-new root");
|
||||
throw new Error("Unable to resolve app root");
|
||||
};
|
||||
|
||||
const queryLocation = z.object({ location: z.string().min(1) });
|
||||
@@ -143,7 +143,7 @@ const makeApp = () => {
|
||||
res.status(500).json({ message });
|
||||
});
|
||||
|
||||
const frontendRoot = join(findAppNewRoot(), "dist/frontend");
|
||||
const frontendRoot = join(findAppRoot(), "dist/frontend");
|
||||
if (existsSync(frontendRoot)) {
|
||||
app.use(express.static(frontendRoot));
|
||||
app.get(/^(?!\/api).*/, (_req, res) => {
|
||||
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
||||
import nunjucks from "nunjucks";
|
||||
import type { ImageSelection, UsageTemplate } from "./types";
|
||||
|
||||
const findAppNewRoot = (): string => {
|
||||
const findAppRoot = (): string => {
|
||||
const candidates = [join(__dirname, "../../.."), join(__dirname, "../..")];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
@@ -12,19 +12,19 @@ const findAppNewRoot = (): string => {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unable to resolve app-new template root");
|
||||
throw new Error("Unable to resolve app template root");
|
||||
};
|
||||
|
||||
export class TemplateService {
|
||||
private readonly appNewRoot = findAppNewRoot();
|
||||
private readonly appRoot = findAppRoot();
|
||||
|
||||
private readonly env = nunjucks.configure(join(this.appNewRoot, "templates"), {
|
||||
private readonly env = nunjucks.configure(join(this.appRoot, "templates"), {
|
||||
autoescape: false,
|
||||
noCache: true
|
||||
});
|
||||
|
||||
private readonly templates: UsageTemplate[] = JSON.parse(
|
||||
readFileSync(join(this.appNewRoot, "templates.json"), "utf8")
|
||||
readFileSync(join(this.appRoot, "templates.json"), "utf8")
|
||||
) as UsageTemplate[];
|
||||
|
||||
public getTemplates(): UsageTemplate[] {
|
||||
@@ -1,8 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
if [ "$1" = "-s" ] || [ "$1" = "--shell" ]; then
|
||||
shift
|
||||
exec bash $@
|
||||
fi
|
||||
|
||||
exec streamlit $@
|
||||
exec node /app/dist/backend/server.js
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "azure-image-chooser-frontend",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"author": "Sławomir Koszewski",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -13,5 +13,10 @@
|
||||
"label": "Azure Resource Manager Template",
|
||||
"language": "json",
|
||||
"file": "arm_vm.jsonc"
|
||||
},
|
||||
{
|
||||
"label": "Bicep VM image reference",
|
||||
"language": "bicep",
|
||||
"file": "bicep_vm.tpl"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user