Skip to content

Commit 821c9ff

Browse files
authored
Merge pull request #142 from AET-DevOps26/feat/azure-vm-lifecycle
feat: on-demand Azure VM lifecycle with start/stop workflows
2 parents e519d82 + 30b1a7d commit 821c9ff

8 files changed

Lines changed: 197 additions & 20 deletions

File tree

.github/workflows/deploy-ansible.yml

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,44 @@ on:
99
- 'services/**'
1010
- 'infra/**'
1111
- 'ansible/**'
12-
- '.github/workflows/ansible-deploy.yml'
12+
- '.github/workflows/deploy-ansible.yml'
1313

1414
workflow_dispatch:
1515

16+
permissions:
17+
id-token: write
18+
contents: read
19+
20+
env:
21+
RESOURCE_GROUP: my-app-rg
22+
VM_NAME: my-vm
23+
1624
jobs:
1725
deploy:
1826
runs-on: ubuntu-latest
1927
steps:
2028
- name: Checkout code
2129
uses: actions/checkout@v4
2230

31+
- name: Azure login (OIDC)
32+
uses: azure/login@v2
33+
with:
34+
client-id: ${{ vars.AZURE_CLIENT_ID }}
35+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
36+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
37+
38+
- name: Resolve VM public IP
39+
run: |
40+
IP=$(az vm show -d \
41+
--resource-group "$RESOURCE_GROUP" \
42+
--name "$VM_NAME" \
43+
--query publicIps -o tsv)
44+
if [ -z "$IP" ]; then
45+
echo "::error::VM has no public IP. Run 'Start environment' first."
46+
exit 1
47+
fi
48+
echo "VM_IP=$IP" >> "$GITHUB_ENV"
49+
2350
- name: Set up Python
2451
uses: actions/setup-python@v5
2552
with:
@@ -37,8 +64,20 @@ jobs:
3764
known_hosts: 'placeholder' # Will be overwritten by ssh-keyscan next
3865

3966
- name: Scan VM Host Key
67+
run: ssh-keyscan -H "$VM_IP" >> ~/.ssh/known_hosts
68+
69+
- name: Write inventory with the resolved IP
4070
run: |
41-
ssh-keyscan -H 4.223.70.80 >> ~/.ssh/known_hosts
71+
cat > infra/inventory.ini <<EOF
72+
[web]
73+
$VM_IP
74+
75+
[all:vars]
76+
ansible_user=azureuser
77+
ansible_ssh_private_key_file=~/.ssh/id_rsa
78+
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
79+
ansible_python_interpreter=/usr/bin/python3.10
80+
EOF
4281
4382
- name: Run Playbook
4483
run: |

.github/workflows/vm-start.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Start environment
2+
3+
# Brings the Azure VM online for a demo. Creates a public IP, attaches it,
4+
# starts the VM (containers auto-restart via `restart: unless-stopped`), and
5+
# arms an auto-shutdown timer so the VM deallocates itself after `minutes`.
6+
# The public URL is printed in the run summary.
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
minutes:
12+
description: "Minutes to keep the VM running before it auto-shuts-down (e.g. 5, 60, 120)"
13+
required: true
14+
default: "120"
15+
16+
permissions:
17+
id-token: write
18+
contents: read
19+
20+
env:
21+
RESOURCE_GROUP: my-app-rg
22+
VM_NAME: my-vm
23+
NIC_NAME: my-nic
24+
IP_CONFIG: internal
25+
PIP_NAME: my-vm-pip
26+
LOCATION: spaincentral
27+
28+
jobs:
29+
start:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Azure login (OIDC)
33+
uses: azure/login@v2
34+
with:
35+
client-id: ${{ vars.AZURE_CLIENT_ID }}
36+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
37+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
38+
39+
- name: Create public IP (if missing)
40+
run: |
41+
az network public-ip create \
42+
--resource-group "$RESOURCE_GROUP" \
43+
--name "$PIP_NAME" \
44+
--location "$LOCATION" \
45+
--sku Standard \
46+
--allocation-method Static \
47+
--only-show-errors
48+
49+
- name: Attach public IP to the VM's NIC
50+
run: |
51+
az network nic ip-config update \
52+
--resource-group "$RESOURCE_GROUP" \
53+
--nic-name "$NIC_NAME" \
54+
--name "$IP_CONFIG" \
55+
--public-ip-address "$PIP_NAME" \
56+
--only-show-errors
57+
58+
- name: Start the VM
59+
run: az vm start --resource-group "$RESOURCE_GROUP" --name "$VM_NAME"
60+
61+
- name: Arm auto-shutdown timer (now + minutes, UTC)
62+
env:
63+
MINUTES: ${{ github.event.inputs.minutes }}
64+
run: |
65+
SHUTDOWN=$(date -u -d "+${MINUTES} minutes" +%H%M)
66+
az vm auto-shutdown \
67+
--resource-group "$RESOURCE_GROUP" \
68+
--name "$VM_NAME" \
69+
--time "$SHUTDOWN"
70+
echo "SHUTDOWN_UTC=$SHUTDOWN" >> "$GITHUB_ENV"
71+
72+
- name: Publish the URL
73+
env:
74+
MINUTES: ${{ github.event.inputs.minutes }}
75+
run: |
76+
IP=$(az network public-ip show \
77+
--resource-group "$RESOURCE_GROUP" \
78+
--name "$PIP_NAME" \
79+
--query ipAddress -o tsv)
80+
{
81+
echo "### 🚀 Environment starting"
82+
echo ""
83+
echo "**URL:** http://$IP (allow ~1–2 min for containers to come up)"
84+
echo ""
85+
echo "Auto-shutdown armed for **${SHUTDOWN_UTC} UTC** (in ${MINUTES} min)."
86+
echo "Run **Stop environment** to end early and release the IP."
87+
} >> "$GITHUB_STEP_SUMMARY"

.github/workflows/vm-stop.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Stop environment
2+
3+
# Deallocates the VM (stops compute billing) and deletes the public IP, so the
4+
# only ongoing cost while idle is the OS disk. Safe to run anytime; the VM's
5+
# data and containers survive on the disk and return on the next start.
6+
7+
on:
8+
workflow_dispatch:
9+
10+
permissions:
11+
id-token: write
12+
contents: read
13+
14+
env:
15+
RESOURCE_GROUP: my-app-rg
16+
VM_NAME: my-vm
17+
NIC_NAME: my-nic
18+
IP_CONFIG: internal
19+
PIP_NAME: my-vm-pip
20+
21+
jobs:
22+
stop:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Azure login (OIDC)
26+
uses: azure/login@v2
27+
with:
28+
client-id: ${{ vars.AZURE_CLIENT_ID }}
29+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
30+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
31+
32+
- name: Deallocate the VM
33+
run: az vm deallocate --resource-group "$RESOURCE_GROUP" --name "$VM_NAME"
34+
35+
- name: Detach and delete the public IP
36+
run: |
37+
az network nic ip-config update \
38+
--resource-group "$RESOURCE_GROUP" \
39+
--nic-name "$NIC_NAME" \
40+
--name "$IP_CONFIG" \
41+
--remove publicIpAddress \
42+
--only-show-errors || true
43+
az network public-ip delete \
44+
--resource-group "$RESOURCE_GROUP" \
45+
--name "$PIP_NAME" \
46+
--only-show-errors || true
47+
48+
- name: Summary
49+
run: |
50+
{
51+
echo "### 🛑 Environment stopped"
52+
echo ""
53+
echo "VM deallocated and public IP released."
54+
} >> "$GITHUB_STEP_SUMMARY"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Rancher deployment (Kubernetes): https://devsecops.stud.k8s.aet.cit.tum.de
66

7-
Azure deployment (Docker Compose): http://4.223.70.80/
7+
Azure deployment (Docker Compose): Unfortunately, all of us almost ran out of credits on Azure. Therefore, we can only serve it on-demand: [Run the `vm-start` workflow](https://github.com/AET-DevOps26/team-devsecops/actions/workflows/vm-start.yml) to activate an Azure instance for 2 hours (IP address will be printed in the logs).
88

99
Coverage reports: https://aet-devops26.github.io/team-devsecops/
1010

infra/docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ networks:
55
services:
66
nginx:
77
image: nginx:alpine
8+
restart: unless-stopped
89
ports:
910
- "80:80"
1011
volumes:
@@ -18,6 +19,7 @@ services:
1819

1920
py-recipe-service:
2021
build: ../services/py-recipe-service
22+
restart: unless-stopped
2123
env_file: .env
2224
expose:
2325
- "8080"
@@ -26,6 +28,7 @@ services:
2628

2729
py-help-service:
2830
build: ../services/py-help-service
31+
restart: unless-stopped
2932
env_file: .env
3033
expose:
3134
- "8080"
@@ -54,6 +57,7 @@ services:
5457

5558
spring-api:
5659
build: ../services/spring-api
60+
restart: unless-stopped
5761
env_file: .env
5862
expose:
5963
- "8080"
@@ -77,6 +81,7 @@ services:
7781

7882
web-client:
7983
build: ../web-client
84+
restart: unless-stopped
8085
expose:
8186
- "8080"
8287
networks:

infra/id_rsa.pub

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCEoD/FM5r6/dGFx5/5wu29zPXq7MyYe85wgCz7yXukKMBGjqcRhyzuiBDNxUON3SL3xL1T6H9/DA0wmSBoAvvY6QeKdBz5LTLl29Ot84ZI3WivwJdOo/uqokOqU+HDGHQ46yUWe6NcBndwha3QkBU7++HaSzt9WuLozUjvWWkplYWlGtB3khqngJGCDo8Oh63ekXuJYdbfwi7g/rAO9/ipVyCGsTg8oJAItPyQYd8MgBYQMmOoO3k073XDFaBI9TO+WZ3m8eSmzyTyV8pQlFn/Yl7d63SWOLcfGwgIHlFNWhUyoB+FlBKBF3LUpi+negOWbHzL5sKzaJGj9UMvT1MVPcAmd6Fgv7bHbtdhN8x4tCrYLlVKDHTEp1m8aIaa6Y7lIsyvL8R1hH9jAQU4xgN6do7d/fiIMxQrdWIeMSbbvDRGdYI/e51zF7m7Iq5l6gvu8IWU7tX40zyXIaLdPIAYH+DyhQOCCk2CY7QUlU9+Br3SrRGbcpcXV3Gj7IiIf6IDSrr4ZXlB8VbOMpQu9yywHhsdLTiu50HNEqtKMDriAPUzZqRlHj+ZZfQY+Xmbs0lPhPMy5hY3kxcOsnG26jwbEf2iY2z5I8YNM0Jxb6KP0CSHcKHiVlopBaofre3md9Yn0Wq7FWga0h3NfM8wpbooTTq4o5NGD6Y6LrEl4n9glw== dave@wolfsburg
1+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQChTNMZffIK4v5uX2CCJJHHGjmSBgVfv070mPy1FdmYkpgPAHBgr8gjko5lmpAzcw4eai/xz5d/L4JNFn/H9JE1jJqVt2DCnRbtLJmHHFEMyWEpmIOJy2cLepdBHCsv8aLmo15emJCw0bCOV/YEATJKreCLZgfRGZK/XKI2Ymy1tGGKJN7TsGp84rLc5c9jRlxOiV6s8zt2d9GT3v6ksjcrAvXR1tT2GRxelTvnska4G1M7dTBlBjZa2UEyccA4ZyQGrdRjHU6OmH0Chp5nz2btSxw+J1vO7DkdYDKW8zUcpOwwCySc3Ip7dWllwalaWimRT+2H7x10xoy/g7lCjYWdL7/c9eSr2bdg4iym4ZsJ3JYspKmTgBmVcDurMdQgCWk4j0Dz4GDil+sqecpp6ZjZkydrr2lF9jiQpYChkCb8bVC7eRpnBNYLMo7LaZdj7PLZdUbME6Iiw+Ryv4djj1uWTiJsypShwjQ/VSCOHiBTynlMCZS/Gl0pK8vAQf3rckBektPPagje4FrNiWnOZ9AX5Kj4ttsE/rYJuK7QIgB6J0sD62rNwzA1SF/811wOu4EEE0pd1Vbut8iAFkxvQXkT7Llw5lLH56D+ZdzBYkfQ62UrC1ZA4ZPOZM3F1uE2bdYRlQwPRDxZI4zBjJZv1mTEcbRpP0Fz5HUzv9B+L0Q1Jw== jakob@jakob-desktop

infra/inventory.ini

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
# .github/workflows/deploy-ansible.yml) regenerates the IP at runtime. For a local Ansible
2+
# run, replace __VM_IP__ with the IP printed by the "Start environment" workflow.
13
[web]
2-
4.223.70.80
4+
__VM_IP__
35

46
[all:vars]
57
ansible_user=azureuser

infra/main.tf

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
# allowed regions: ["swedencentral","polandcentral","austriaeast","spaincentral","germanywestcentral"]
21
resource "azurerm_resource_group" "rg" {
32
name = "my-app-rg"
4-
location = "swedencentral"
3+
location = "spaincentral"
54
}
65

76
resource "azurerm_virtual_network" "vnet" {
@@ -18,14 +17,6 @@ resource "azurerm_subnet" "subnet" {
1817
address_prefixes = ["10.0.2.0/24"]
1918
}
2019

21-
resource "azurerm_public_ip" "pip" {
22-
name = "my-vm-pip"
23-
location = azurerm_resource_group.rg.location
24-
resource_group_name = azurerm_resource_group.rg.name
25-
allocation_method = "Static"
26-
sku = "Standard"
27-
}
28-
2920
resource "azurerm_network_security_group" "nsg" {
3021
name = "my-nsg"
3122
location = azurerm_resource_group.rg.location
@@ -92,15 +83,14 @@ resource "azurerm_network_interface" "nic" {
9283
name = "internal"
9384
subnet_id = azurerm_subnet.subnet.id
9485
private_ip_address_allocation = "Dynamic"
95-
public_ip_address_id = azurerm_public_ip.pip.id
86+
}
87+
88+
lifecycle {
89+
ignore_changes = [ip_configuration[0].public_ip_address_id]
9690
}
9791
}
9892

9993
resource "azurerm_network_interface_security_group_association" "nsg_assoc" {
10094
network_interface_id = azurerm_network_interface.nic.id
10195
network_security_group_id = azurerm_network_security_group.nsg.id
10296
}
103-
104-
output "public_ip" {
105-
value = azurerm_public_ip.pip.ip_address
106-
}

0 commit comments

Comments
 (0)