Skip to content

Commit 492357a

Browse files
DEVOPS-281 terraforminator automation removes rgs with TEMPORAR tag as TRUE
1 parent e8d72d0 commit 492357a

File tree

6 files changed

+215
-35
lines changed

6 files changed

+215
-35
lines changed

.github/dependabot.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
version: 2
3+
updates:
4+
- package-ecosystem: "pip"
5+
directory: /
6+
schedule:
7+
interval: monthly
8+
# Assignees to set on pull requests
9+
assignees:
10+
- "githubofkrishnadhas"
11+
# prefix specifies a prefix for all commit messages. When you specify a prefix for commit messages,
12+
# GitHub will automatically add a colon between the defined prefix and the commit message provided the
13+
# defined prefix ends with a letter, number, closing parenthesis, or closing bracket.
14+
commit-message:
15+
prefix: "dependabot python package"
16+
# Use reviewers to specify individual reviewers or teams of reviewers for all pull requests raised for a package manager.
17+
reviewers:
18+
- "devwithkrishna/admin"
19+
# Raise pull requests for version updates to pip against the `main` branch
20+
target-branch: "main"
21+
# Labels on pull requests for version updates only
22+
# labels:
23+
# - "pip dependencies"
24+
# Increase the version requirements for Composer only when required
25+
versioning-strategy: increase-if-necessary
26+
# Dependabot opens a maximum of five pull requests for version updates. Once there are five open pull requests from Dependabot,
27+
# Dependabot will not open any new requests until some of those open requests are merged or closed.
28+
# Use open-pull-requests-limit to change this limit.
29+
open-pull-requests-limit: 10
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: azure-decommision-unsed-resource-groups-with-temporary-tag-as-true
2+
on:
3+
schedule:
4+
- cron: '0 0 * * *' # This means every day at 00:00 UTC
5+
workflow_dispatch:
6+
inputs:
7+
subscription_name:
8+
required: true
9+
type: string
10+
default: "TECH-ARCHITECTS-NONPROD"
11+
description: "The azure subscription name."
12+
13+
run-name: azure-decommision-unsed-resource-groups-with-temporary-tag-as-true
14+
jobs:
15+
azure-decommision-unsed-resource-groups-with-temporary-tag-as-true:
16+
runs-on: ubuntu-latest
17+
env:
18+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
19+
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
20+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
21+
subscription_name: "TECH-ARCHITECTS-NONPROD"
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@v4
25+
- name: set up python
26+
uses: actions/setup-python@v2
27+
with:
28+
python-version: '3.11'
29+
- name: package installations
30+
run: |
31+
pip install poetry
32+
poetry install
33+
- name: run python program
34+
run: |
35+
poetry run python3 terraforminator.py --subscription_name ${{ env.subscription_name }}
36+
- name: program execution completed
37+
run: echo "program execution completed"

.gitignore

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,5 @@
1-
# Local .terraform directories
2-
**/.terraform/*
1+
.idea/**
2+
.env
3+
poetry.lock
34

4-
# .tfstate files
5-
*.tfstate
6-
*.tfstate.*
75

8-
# Crash log files
9-
crash.log
10-
crash.*.log
11-
12-
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
13-
# password, private keys, and other secrets. These should not be part of version
14-
# control as they are data points which are potentially sensitive and subject
15-
# to change depending on the environment.
16-
*.tfvars
17-
*.tfvars.json
18-
19-
# Ignore override files as they are usually used to override resources locally and so
20-
# are not checked in
21-
override.tf
22-
override.tf.json
23-
*_override.tf
24-
*_override.tf.json
25-
26-
# Ignore transient lock info files created by terraform apply
27-
.terraform.tfstate.lock.info
28-
29-
# Include override files you do wish to add to version control using negated pattern
30-
# !example_override.tf
31-
32-
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
33-
# example: *tfplan*
34-
35-
# Ignore CLI configuration files
36-
.terraformrc
37-
terraform.rc

pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[tool.poetry]
2+
name = "azure-terraforminator"
3+
version = "0.1.0"
4+
description = ""
5+
authors = ["githubofkrishnadhas <[email protected]>"]
6+
readme = "README.md"
7+
8+
[tool.poetry.dependencies]
9+
python = "^3.11"
10+
requests = "^2.32.3"
11+
python-dotenv = "^1.0.1"
12+
azure-identity = "^1.18.0"
13+
azure-mgmt-resource = "^23.1.1"
14+
azure-mgmt-resourcegraph = "^8.0.0"
15+
azure-core = "^1.31.0"
16+
17+
18+
[build-system]
19+
requires = ["poetry-core"]
20+
build-backend = "poetry.core.masonry.api"

resource_graph_query.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from azure.identity import DefaultAzureCredential
2+
import azure.mgmt.resourcegraph as arg
3+
import logging
4+
import os
5+
from dotenv import load_dotenv
6+
7+
def run_azure_rg_query(subscription_name: str):
8+
"""
9+
Run a resource graph query to get the subscription id of a subscription back
10+
:return: subscription_id str
11+
"""
12+
credential = DefaultAzureCredential()
13+
# Create Azure Resource Graph client and set options
14+
arg_client = arg.ResourceGraphClient(credential)
15+
16+
query = f"""
17+
resourcecontainers
18+
| where type == 'microsoft.resources/subscriptions' and name == '{subscription_name}'
19+
| project subscriptionId
20+
"""
21+
22+
print(f"query is {query}")
23+
24+
# Create query
25+
arg_query = arg.models.QueryRequest( query=query)
26+
27+
# Run query
28+
arg_result = arg_client.resources(arg_query)
29+
30+
# Show Python object
31+
# print(arg_result)
32+
subscription_id = arg_result.data[0]['subscriptionId']
33+
print(f"Subscription ID is : {subscription_id}")
34+
return subscription_id
35+
36+
def main():
37+
"""
38+
To test the script
39+
:return:
40+
"""
41+
load_dotenv()
42+
logging.info("ARG query being prepared......")
43+
run_azure_rg_query(subscription_name="TECH-ARCHITECTS-NONPROD")
44+
logging.info("ARG query Completed......")
45+
46+
47+
if __name__ == "__main__":
48+
main()

terraforminator.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
from datetime import datetime
3+
import argparse
4+
import asyncio
5+
from dotenv import load_dotenv
6+
from azure.identity import DefaultAzureCredential
7+
from azure.mgmt.resource.resources.v2022_09_01 import ResourceManagementClient
8+
from azure.core.exceptions import ResourceNotFoundError
9+
from resource_graph_query import run_azure_rg_query
10+
11+
async def list_resource_groups_with_temporary_tag(subscription_id: str):
12+
"""
13+
List the resource groups in authenticated subscriptions with Temporary tag value as TRUE
14+
:return:
15+
"""
16+
# load_dotenv()
17+
credential = DefaultAzureCredential()
18+
resource_management_client = ResourceManagementClient(subscription_id=subscription_id, credential=credential)
19+
tag_filter= f"tagName eq 'Temporary' and tagValue eq 'TRUE'"
20+
all_rgs_filtered = resource_management_client.resource_groups.list(filter=tag_filter)
21+
rgs_to_deleted = []
22+
for rg in all_rgs_filtered:
23+
rg_dict = {
24+
'name': rg.name,
25+
'location': rg.location
26+
}
27+
rgs_to_deleted.append(rg_dict) # final dictionary of rgs to be deleted with Temporary tag value as TRUE
28+
29+
print(rgs_to_deleted)
30+
31+
return rgs_to_deleted
32+
async def delete_resource_groups(subscription_id: str, rgs_to_be_deleted: list[dict]):
33+
"""
34+
Delete the resource groups with Temporary tag value as TRUE
35+
:return:
36+
"""
37+
credential = DefaultAzureCredential()
38+
resource_management_client = ResourceManagementClient(subscription_id=subscription_id, credential=credential)
39+
40+
for rg in rgs_to_be_deleted:
41+
try:
42+
print(f"Deleting {rg['name']} from {subscription_id}")
43+
resource_management_client.resource_groups.begin_delete(resource_group_name=rg['name']).result()
44+
print(f"Successfully deleted {rg['name']}")
45+
46+
except ResourceNotFoundError:
47+
48+
print(f"Resource group '{rg['name']}' not found.")
49+
50+
except Exception as e:
51+
52+
print(f"Failed to delete resource group '{rg['name']}': {e}")
53+
# Optional: Add a short delay between deletions to prevent overwhelming the service
54+
await asyncio.sleep(1)
55+
56+
57+
async def main():
58+
"""To test the code"""
59+
start_time = datetime.utcnow() # Get start time in UTC
60+
print(f"Process started at (UTC): {start_time}")
61+
load_dotenv()
62+
parser = argparse.ArgumentParser("Decommission nolonger used resource groups in Azure.")
63+
parser.add_argument("--subscription_name", type=str, required=True,help="Azure subscription name")
64+
65+
args = parser.parse_args()
66+
67+
subscription_name = args.subscription_name
68+
subscription_id = run_azure_rg_query(subscription_name=subscription_name)
69+
rgs_to_deleted = await list_resource_groups_with_temporary_tag(subscription_id=subscription_id)
70+
await delete_resource_groups(subscription_id=subscription_id, rgs_to_be_deleted=rgs_to_deleted)
71+
end_time = datetime.utcnow() # Get end time in UTC
72+
print(f"Process completed at (UTC): {end_time}")
73+
# Calculate and print elapsed time
74+
elapsed_time = end_time - start_time
75+
print(f"Total elapsed time: {elapsed_time} (hh:mm:ss)")
76+
77+
if __name__ == "__main__":
78+
asyncio.run(main())

0 commit comments

Comments
 (0)