pulumi-django-azure 1.0.21__py3-none-any.whl → 1.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pulumi-django-azure might be problematic. Click here for more details.
- pulumi_django_azure/__init__.py +1 -0
- pulumi_django_azure/azure_helper.py +67 -0
- pulumi_django_azure/django_deployment.py +20 -21
- pulumi_django_azure/management/commands/__init__.py +0 -0
- pulumi_django_azure/management/commands/purge_cdn.py +52 -0
- pulumi_django_azure/middleware.py +43 -0
- pulumi_django_azure/settings.py +154 -0
- {pulumi_django_azure-1.0.21.dist-info → pulumi_django_azure-1.0.23.dist-info}/METADATA +61 -97
- pulumi_django_azure-1.0.23.dist-info/RECORD +12 -0
- {pulumi_django_azure-1.0.21.dist-info → pulumi_django_azure-1.0.23.dist-info}/WHEEL +1 -1
- pulumi_django_azure-1.0.21.dist-info/RECORD +0 -7
- {pulumi_django_azure-1.0.21.dist-info → pulumi_django_azure-1.0.23.dist-info/licenses}/LICENSE +0 -0
- {pulumi_django_azure-1.0.21.dist-info → pulumi_django_azure-1.0.23.dist-info}/top_level.txt +0 -0
pulumi_django_azure/__init__.py
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from subprocess import check_output
|
|
6
|
+
|
|
7
|
+
from azure.identity import DefaultAzureCredential
|
|
8
|
+
from azure.mgmt.resource import SubscriptionClient
|
|
9
|
+
from azure.mgmt.resource.subscriptions.models import Subscription
|
|
10
|
+
|
|
11
|
+
# Azure credentials
|
|
12
|
+
AZURE_CREDENTIAL = DefaultAzureCredential()
|
|
13
|
+
|
|
14
|
+
# Get the local IP addresses of the machine (only when runnig on Azure)
|
|
15
|
+
if os.environ.get("ORYX_ENV_NAME"):
|
|
16
|
+
LOCAL_IP_ADDRESSES = check_output(["hostname", "--all-ip-addresses"]).decode("utf-8").strip().split(" ")
|
|
17
|
+
else:
|
|
18
|
+
LOCAL_IP_ADDRESSES = []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_db_password() -> str:
|
|
22
|
+
"""
|
|
23
|
+
Get a valid password for the database.
|
|
24
|
+
"""
|
|
25
|
+
return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RedisCredentials:
|
|
30
|
+
username: str
|
|
31
|
+
password: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_redis_credentials() -> RedisCredentials:
|
|
35
|
+
"""
|
|
36
|
+
Get valid credentials for the Redis cache.
|
|
37
|
+
"""
|
|
38
|
+
token = AZURE_CREDENTIAL.get_token("https://redis.azure.com/.default").token
|
|
39
|
+
return RedisCredentials(_extract_username_from_token(token), token)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_subscription() -> Subscription:
|
|
43
|
+
"""
|
|
44
|
+
Get the subscription for the current user.
|
|
45
|
+
"""
|
|
46
|
+
subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
|
|
47
|
+
subscriptions = list(subscription_client.subscriptions.list())
|
|
48
|
+
return subscriptions[0]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _extract_username_from_token(token: str) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Extract the username from the JSON Web Token (JWT) token.
|
|
54
|
+
"""
|
|
55
|
+
parts = token.split(".")
|
|
56
|
+
base64_str = parts[1]
|
|
57
|
+
|
|
58
|
+
if len(base64_str) % 4 == 2:
|
|
59
|
+
base64_str += "=="
|
|
60
|
+
elif len(base64_str) % 4 == 3:
|
|
61
|
+
base64_str += "="
|
|
62
|
+
|
|
63
|
+
json_bytes = base64.b64decode(base64_str)
|
|
64
|
+
json_str = json_bytes.decode("utf-8")
|
|
65
|
+
jwt = json.loads(json_str)
|
|
66
|
+
|
|
67
|
+
return jwt["oid"]
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
-
from typing import Optional, Union
|
|
3
2
|
|
|
4
3
|
import pulumi
|
|
5
4
|
import pulumi_azure_native as azure
|
|
@@ -15,7 +14,7 @@ class HostDefinition:
|
|
|
15
14
|
:param identifier: An identifier for this host definition (optional).
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
|
-
def __init__(self, host: str, zone:
|
|
17
|
+
def __init__(self, host: str, zone: azure.network.Zone | None = None, identifier: str | None = None):
|
|
19
18
|
self.host = host
|
|
20
19
|
self.zone = zone
|
|
21
20
|
self._identifier = identifier
|
|
@@ -68,12 +67,12 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
68
67
|
app_service_ip_prefix: str,
|
|
69
68
|
app_service_sku: azure.web.SkuDescriptionArgs,
|
|
70
69
|
storage_account_name: str,
|
|
71
|
-
storage_allowed_origins:
|
|
72
|
-
pgadmin_access_ip:
|
|
73
|
-
pgadmin_dns_zone:
|
|
74
|
-
cache_ip_prefix:
|
|
75
|
-
cache_sku:
|
|
76
|
-
cdn_host:
|
|
70
|
+
storage_allowed_origins: Sequence[str] | None = None,
|
|
71
|
+
pgadmin_access_ip: Sequence[str] | None = None,
|
|
72
|
+
pgadmin_dns_zone: azure.network.Zone | None = None,
|
|
73
|
+
cache_ip_prefix: str | None = None,
|
|
74
|
+
cache_sku: azure.cache.SkuArgs | None = None,
|
|
75
|
+
cdn_host: HostDefinition | None = None,
|
|
77
76
|
opts=None,
|
|
78
77
|
):
|
|
79
78
|
"""
|
|
@@ -134,7 +133,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
134
133
|
# Create a pgAdmin app
|
|
135
134
|
self._create_pgadmin_app(access_ip=pgadmin_access_ip, dns_zone=pgadmin_dns_zone)
|
|
136
135
|
|
|
137
|
-
def _create_storage(self, account_name: str, allowed_origins:
|
|
136
|
+
def _create_storage(self, account_name: str, allowed_origins: Sequence[str] | None = None):
|
|
138
137
|
# Create blob storage
|
|
139
138
|
self._storage_account = azure.storage.StorageAccount(
|
|
140
139
|
f"sa-{self._name}",
|
|
@@ -169,7 +168,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
169
168
|
),
|
|
170
169
|
)
|
|
171
170
|
|
|
172
|
-
def _create_cdn(self, custom_host:
|
|
171
|
+
def _create_cdn(self, custom_host: HostDefinition | None) -> pulumi.Output[str]:
|
|
173
172
|
"""
|
|
174
173
|
Create a CDN endpoint. If a host name is given, it will be used as the custom domain.
|
|
175
174
|
Otherwise, the default CDN host name will be returned.
|
|
@@ -392,7 +391,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
392
391
|
self,
|
|
393
392
|
name,
|
|
394
393
|
prefix,
|
|
395
|
-
delegation_service:
|
|
394
|
+
delegation_service: str | None = None,
|
|
396
395
|
service_endpoints: Sequence[str] = [],
|
|
397
396
|
) -> azure.network.Subnet:
|
|
398
397
|
"""
|
|
@@ -433,7 +432,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
433
432
|
sku=sku,
|
|
434
433
|
)
|
|
435
434
|
|
|
436
|
-
def _create_pgadmin_app(self, access_ip:
|
|
435
|
+
def _create_pgadmin_app(self, access_ip: Sequence[str] | None = None, dns_zone: azure.network.Zone | None = None):
|
|
437
436
|
# Determine the IP restrictions
|
|
438
437
|
ip_restrictions = []
|
|
439
438
|
default_restriction = azure.web.DefaultAction.ALLOW
|
|
@@ -562,10 +561,10 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
562
561
|
def _add_webapp_host(
|
|
563
562
|
self,
|
|
564
563
|
app: azure.web.WebApp,
|
|
565
|
-
host:
|
|
564
|
+
host: str | pulumi.Input[str],
|
|
566
565
|
suffix: str,
|
|
567
566
|
identifier: str,
|
|
568
|
-
depends_on:
|
|
567
|
+
depends_on: Sequence[pulumi.Resource] | None = None,
|
|
569
568
|
):
|
|
570
569
|
"""
|
|
571
570
|
Because of a circular dependency, we need to create the certificate and the binding in two steps.
|
|
@@ -838,13 +837,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
838
837
|
website_hosts: list[HostDefinition],
|
|
839
838
|
django_settings_module: str,
|
|
840
839
|
python_version: str = "3.13",
|
|
841
|
-
environment_variables:
|
|
842
|
-
secrets:
|
|
843
|
-
comms_data_location:
|
|
844
|
-
comms_domains:
|
|
845
|
-
dedicated_app_service_sku:
|
|
846
|
-
vault_administrators:
|
|
847
|
-
cache_db:
|
|
840
|
+
environment_variables: dict[str, str] | None = None,
|
|
841
|
+
secrets: dict[str, str] | None = None,
|
|
842
|
+
comms_data_location: str | None = None,
|
|
843
|
+
comms_domains: list[HostDefinition] | None = None,
|
|
844
|
+
dedicated_app_service_sku: azure.web.SkuDescriptionArgs | None = None,
|
|
845
|
+
vault_administrators: list[str] | None = None,
|
|
846
|
+
cache_db: int | None = None,
|
|
848
847
|
) -> azure.web.WebApp:
|
|
849
848
|
"""
|
|
850
849
|
Create a Django website with it's own database and storage containers.
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from azure.mgmt.cdn import CdnManagementClient
|
|
4
|
+
from azure.mgmt.cdn.models import PurgeParameters
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.core.management.base import BaseCommand
|
|
7
|
+
from pulumi_django_azure.azure_helper import AZURE_CREDENTIAL
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Command(BaseCommand):
|
|
11
|
+
help = "Purges the CDN endpoint"
|
|
12
|
+
|
|
13
|
+
def add_arguments(self, parser):
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--wait",
|
|
16
|
+
action="store_true",
|
|
17
|
+
help="Wait for the purge operation to complete",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def handle(self, *args, **options):
|
|
21
|
+
# Read environment variables
|
|
22
|
+
resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
|
|
23
|
+
profile_name = os.getenv("CDN_PROFILE")
|
|
24
|
+
endpoint_name = os.getenv("CDN_ENDPOINT")
|
|
25
|
+
content_paths = ["/*"]
|
|
26
|
+
|
|
27
|
+
# Ensure all required environment variables are set
|
|
28
|
+
if not all([resource_group, profile_name, endpoint_name]):
|
|
29
|
+
self.stderr.write(self.style.ERROR("Missing required environment variables."))
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
# Authenticate with Azure
|
|
33
|
+
cdn_client = CdnManagementClient(AZURE_CREDENTIAL, settings.AZURE_SUBSCRIPTION_ID)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Purge the CDN endpoint
|
|
37
|
+
purge_operation = cdn_client.endpoints.begin_purge_content(
|
|
38
|
+
resource_group_name=resource_group,
|
|
39
|
+
profile_name=profile_name,
|
|
40
|
+
endpoint_name=endpoint_name,
|
|
41
|
+
content_file_paths=PurgeParameters(content_paths=content_paths),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Check if the --wait argument was provided
|
|
45
|
+
if options["wait"]:
|
|
46
|
+
purge_operation.result() # Wait for the operation to complete
|
|
47
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
|
|
48
|
+
else:
|
|
49
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.core.cache import cache
|
|
3
|
+
from django.db import connection
|
|
4
|
+
from django.db.utils import OperationalError
|
|
5
|
+
from django.http import HttpResponse
|
|
6
|
+
from django_redis import get_redis_connection
|
|
7
|
+
|
|
8
|
+
from .azure_helper import get_db_password, get_redis_credentials
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HealthCheckMiddleware:
|
|
12
|
+
def __init__(self, get_response):
|
|
13
|
+
self.get_response = get_response
|
|
14
|
+
|
|
15
|
+
def __call__(self, request):
|
|
16
|
+
if request.path == settings.HEALTH_CHECK_PATH:
|
|
17
|
+
# Update the database credentials if needed
|
|
18
|
+
if settings.AZURE_DB_PASSWORD:
|
|
19
|
+
settings.DATABASES["default"]["PASSWORD"] = get_db_password()
|
|
20
|
+
|
|
21
|
+
# Update the Redis credentials if needed
|
|
22
|
+
if settings.AZURE_REDIS_CREDENTIALS:
|
|
23
|
+
redis_credentials = get_redis_credentials()
|
|
24
|
+
|
|
25
|
+
# Re-authenticate the Redis connection
|
|
26
|
+
redis_connection = get_redis_connection("default")
|
|
27
|
+
redis_connection.execute_command("AUTH", redis_credentials.username, redis_credentials.password)
|
|
28
|
+
|
|
29
|
+
settings.CACHES["default"]["OPTIONS"]["PASSWORD"] = redis_credentials.password
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Test the database connection
|
|
33
|
+
connection.ensure_connection()
|
|
34
|
+
|
|
35
|
+
# Test the Redis connection
|
|
36
|
+
cache.set("health_check", "test")
|
|
37
|
+
|
|
38
|
+
return HttpResponse("OK")
|
|
39
|
+
|
|
40
|
+
except OperationalError:
|
|
41
|
+
return HttpResponse(status=503)
|
|
42
|
+
|
|
43
|
+
return self.get_response(request)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import environ
|
|
2
|
+
from azure.keyvault.secrets import SecretClient
|
|
3
|
+
|
|
4
|
+
from .azure_helper import AZURE_CREDENTIAL, LOCAL_IP_ADDRESSES, get_db_password, get_redis_credentials, get_subscription
|
|
5
|
+
|
|
6
|
+
env = environ.Env()
|
|
7
|
+
|
|
8
|
+
SECRET_KEY = env("DJANGO_SECRET_KEY")
|
|
9
|
+
|
|
10
|
+
IS_AZURE_ENVIRONMENT = env("ORYX_ENV_NAME", default=None) is not None
|
|
11
|
+
|
|
12
|
+
# Some generic stuff we only need if we're running in Azure.
|
|
13
|
+
# Most of the other stuff will check for the explicit variable we need.
|
|
14
|
+
if IS_AZURE_ENVIRONMENT:
|
|
15
|
+
# Detect HTTPS behind AppService
|
|
16
|
+
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
17
|
+
|
|
18
|
+
# Azure context
|
|
19
|
+
AZURE_SUBSCRIPTION = get_subscription()
|
|
20
|
+
AZURE_TENANT_ID = AZURE_SUBSCRIPTION.tenant_id
|
|
21
|
+
AZURE_SUBSCRIPTION_ID = AZURE_SUBSCRIPTION.subscription_id
|
|
22
|
+
|
|
23
|
+
# Health check path
|
|
24
|
+
HEALTH_CHECK_PATH = env("HEALTH_CHECK_PATH", default="/health")
|
|
25
|
+
|
|
26
|
+
ALLOWED_HOSTS: list = env.list("DJANGO_ALLOWED_HOSTS", default=[])
|
|
27
|
+
|
|
28
|
+
# WEBSITE_HOSTNAME contains the Azure domain name
|
|
29
|
+
if website_hostname := env("WEBSITE_HOSTNAME", default=None):
|
|
30
|
+
ALLOWED_HOSTS.append(website_hostname)
|
|
31
|
+
|
|
32
|
+
# Add the local IP addresses of the machine for health checks
|
|
33
|
+
ALLOWED_HOSTS.extend(LOCAL_IP_ADDRESSES)
|
|
34
|
+
|
|
35
|
+
# Azure Key Vault
|
|
36
|
+
if azure_key_vault := env("AZURE_KEY_VAULT", default=None):
|
|
37
|
+
AZURE_KEY_VAULT_URI = f"https://{azure_key_vault}.vault.azure.net"
|
|
38
|
+
AZURE_KEY_VAULT_CLIENT = SecretClient(vault_url=AZURE_KEY_VAULT_URI, credential=AZURE_CREDENTIAL)
|
|
39
|
+
|
|
40
|
+
# Allow CSRF cookies to be sent from our domains
|
|
41
|
+
# CSRF_TRUSTED_ORIGINS = ["https://" + host for host in ALLOWED_HOSTS]
|
|
42
|
+
if azure_storage_account_name := env("AZURE_STORAGE_ACCOUNT_NAME", default=None):
|
|
43
|
+
AZURE_ACCOUNT_NAME = azure_storage_account_name
|
|
44
|
+
AZURE_TOKEN_CREDENTIAL = AZURE_CREDENTIAL
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# CDN domain - shared for all storages
|
|
48
|
+
if cdn_host := env("CDN_HOST", default=None):
|
|
49
|
+
AZURE_CUSTOM_DOMAIN = cdn_host
|
|
50
|
+
|
|
51
|
+
STATIC_URL = f"https://{AZURE_CUSTOM_DOMAIN}/static/"
|
|
52
|
+
MEDIA_URL = f"https://{AZURE_CUSTOM_DOMAIN}/media/"
|
|
53
|
+
|
|
54
|
+
# Storage configuration
|
|
55
|
+
STORAGES = {}
|
|
56
|
+
if container_media := env("AZURE_STORAGE_CONTAINER_MEDIA", default=None):
|
|
57
|
+
STORAGES["default"] = {
|
|
58
|
+
"BACKEND": "storages.backends.azure_storage.AzureStorage",
|
|
59
|
+
"OPTIONS": {
|
|
60
|
+
"azure_container": container_media,
|
|
61
|
+
"overwrite_files": False,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if container_staticfiles := env("AZURE_STORAGE_CONTAINER_STATICFILES", default=None):
|
|
66
|
+
STORAGES["staticfiles"] = {
|
|
67
|
+
"BACKEND": "storages.backends.azure_storage.AzureStorage",
|
|
68
|
+
"OPTIONS": {
|
|
69
|
+
"azure_container": container_staticfiles,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# This setting enables password rotation in the health check middleware
|
|
74
|
+
if IS_AZURE_ENVIRONMENT:
|
|
75
|
+
AZURE_DB_PASSWORD = True
|
|
76
|
+
DATABASES = {
|
|
77
|
+
"default": {
|
|
78
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
79
|
+
"NAME": env("DB_NAME"),
|
|
80
|
+
"USER": env("DB_USER"),
|
|
81
|
+
"HOST": env("DB_HOST"),
|
|
82
|
+
"PASSWORD": get_db_password(),
|
|
83
|
+
"PORT": "5432",
|
|
84
|
+
"OPTIONS": {
|
|
85
|
+
"sslmode": "require",
|
|
86
|
+
},
|
|
87
|
+
# Make connections persistent
|
|
88
|
+
"CONN_MAX_AGE": None,
|
|
89
|
+
# To enable health checks, add the following:
|
|
90
|
+
# "CONN_HEALTH_CHECKS": True,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Email
|
|
95
|
+
EMAIL_BACKEND = "django_azure_communication_email.EmailBackend"
|
|
96
|
+
AZURE_COMMUNICATION_ENDPOINT = env("AZURE_COMMUNICATION_SERVICE_ENDPOINT", default=None)
|
|
97
|
+
DEFAULT_FROM_EMAIL = env("DJANGO_DEFAULT_FROM_EMAIL", default=None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Logging
|
|
101
|
+
if IS_AZURE_ENVIRONMENT:
|
|
102
|
+
LOGGING = {
|
|
103
|
+
"version": 1,
|
|
104
|
+
"disable_existing_loggers": False,
|
|
105
|
+
"handlers": {
|
|
106
|
+
"file": {
|
|
107
|
+
"level": "INFO",
|
|
108
|
+
"class": "logging.handlers.TimedRotatingFileHandler",
|
|
109
|
+
"filename": "/home/LogFiles/django.log",
|
|
110
|
+
"when": "h",
|
|
111
|
+
"interval": 1,
|
|
112
|
+
"backupCount": 24,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"loggers": {
|
|
116
|
+
"django": {
|
|
117
|
+
"handlers": ["file"],
|
|
118
|
+
"level": "INFO",
|
|
119
|
+
"propagate": True,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Redis, if enabled
|
|
125
|
+
redis_cache_host = env("REDIS_CACHE_HOST", default=None)
|
|
126
|
+
redis_cache_port = env("REDIS_CACHE_PORT", default=None)
|
|
127
|
+
redis_cache_db = env("REDIS_CACHE_DB", default=None)
|
|
128
|
+
|
|
129
|
+
if redis_cache_host and redis_cache_port and redis_cache_db:
|
|
130
|
+
# This will enable the health check to update the Redis credentials
|
|
131
|
+
AZURE_REDIS_CREDENTIALS = True
|
|
132
|
+
|
|
133
|
+
# This will prevent the website from failing if Redis is not available
|
|
134
|
+
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
|
|
135
|
+
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
|
|
136
|
+
|
|
137
|
+
REDIS_CACHE_HOST = redis_cache_host
|
|
138
|
+
REDIS_CACHE_PORT = redis_cache_port
|
|
139
|
+
REDIS_CACHE_DB = redis_cache_db
|
|
140
|
+
redis_credentials = get_redis_credentials()
|
|
141
|
+
REDIS_USERNAME = redis_credentials.username
|
|
142
|
+
REDIS_PASSWORD = redis_credentials.password
|
|
143
|
+
|
|
144
|
+
CACHES = {
|
|
145
|
+
"default": {
|
|
146
|
+
"BACKEND": "django_redis.cache.RedisCache",
|
|
147
|
+
"LOCATION": f"rediss://{REDIS_USERNAME}@{REDIS_CACHE_HOST}:{REDIS_CACHE_PORT}/{REDIS_CACHE_DB}",
|
|
148
|
+
"OPTIONS": {
|
|
149
|
+
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
150
|
+
"PARSER_CLASS": "redis.connection._HiredisParser",
|
|
151
|
+
"PASSWORD": REDIS_PASSWORD,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pulumi-django-azure
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.23
|
|
4
4
|
Summary: Simply deployment of Django on Azure with Pulumi
|
|
5
5
|
Author-email: Maarten Ureel <maarten@youreal.eu>
|
|
6
6
|
License: MIT License
|
|
@@ -30,12 +30,23 @@ Keywords: django,pulumi,azure
|
|
|
30
30
|
Classifier: License :: OSI Approved :: MIT License
|
|
31
31
|
Classifier: Programming Language :: Python
|
|
32
32
|
Classifier: Programming Language :: Python :: 3
|
|
33
|
-
Requires-Python:
|
|
33
|
+
Requires-Python: <3.14,>=3.11
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
|
-
Requires-Dist:
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist:
|
|
36
|
+
Requires-Dist: azure-identity<2.0.0,>=1.21.0
|
|
37
|
+
Requires-Dist: azure-keyvault-secrets<5.0.0,>=4.9.0
|
|
38
|
+
Requires-Dist: azure-mgmt-cdn<14.0.0,>=13.1.1
|
|
39
|
+
Requires-Dist: azure-mgmt-resource<24.0.0,>=23.3.0
|
|
40
|
+
Requires-Dist: django<6.0.0,>=5.1.7
|
|
41
|
+
Requires-Dist: django-azure-communication-email<2.0.0,>=1.3.0
|
|
42
|
+
Requires-Dist: django-environ<0.13.0,>=0.12.0
|
|
43
|
+
Requires-Dist: django-redis<6.0.0,>=5.4.0
|
|
44
|
+
Requires-Dist: django-storages[azure]<2.0.0,>=1.14.5
|
|
45
|
+
Requires-Dist: pulumi>=3.156.0
|
|
46
|
+
Requires-Dist: pulumi-azure-native>=2.89.1
|
|
47
|
+
Requires-Dist: pulumi-random>=4.18.0
|
|
48
|
+
Requires-Dist: redis[hiredis]<6.0.0,>=5.2.1
|
|
49
|
+
Dynamic: license-file
|
|
39
50
|
|
|
40
51
|
# Pulumi Django Deployment
|
|
41
52
|
|
|
@@ -57,12 +68,36 @@ Your Django project should contain a folder `cicd` with these files:
|
|
|
57
68
|
Note that this runs in the identity of the build container, so you should not run database or storage manipulations here.
|
|
58
69
|
* startup.sh: commands to run the actual application. I recommend to put at least:
|
|
59
70
|
```bash
|
|
71
|
+
# Compile translations
|
|
72
|
+
apt-get -y install gettext
|
|
73
|
+
python manage.py compilemessages
|
|
74
|
+
|
|
75
|
+
# Run collectstatic in the background
|
|
76
|
+
sh cicd/collectstatic.sh &
|
|
77
|
+
|
|
60
78
|
python manage.py migrate
|
|
61
|
-
python manage.py collectstatic --noinput
|
|
62
|
-
python manage.py purge_cdn
|
|
63
79
|
gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
|
|
64
80
|
```
|
|
65
|
-
|
|
81
|
+
|
|
82
|
+
Be sure to change `yourapplication` in the above.
|
|
83
|
+
|
|
84
|
+
* collecstatic.sh
|
|
85
|
+
```bash
|
|
86
|
+
#!/bin/bash
|
|
87
|
+
|
|
88
|
+
LOGFILE="/var/log/django_collectstatic.log"
|
|
89
|
+
|
|
90
|
+
# Collectstatic
|
|
91
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting collectstatic" >> $LOGFILE
|
|
92
|
+
python manage.py collectstatic --noinput -v 2 >> $LOGFILE
|
|
93
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished collectstatic" >> $LOGFILE
|
|
94
|
+
|
|
95
|
+
# Purge the CDN
|
|
96
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting purge_cdn" >> $LOGFILE
|
|
97
|
+
python manage.py purge_cdn >> $LOGFILE
|
|
98
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished purge_cdn" >> $LOGFILE
|
|
99
|
+
```
|
|
100
|
+
|
|
66
101
|
## Installation
|
|
67
102
|
This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
|
|
68
103
|
|
|
@@ -127,8 +162,23 @@ django.add_database_administrator(
|
|
|
127
162
|
)
|
|
128
163
|
```
|
|
129
164
|
|
|
130
|
-
##
|
|
165
|
+
## Changes to your Django project
|
|
166
|
+
1. Add `pulumi_django_azure` to your `INSTALLED_APPS`
|
|
167
|
+
2. Add to your settings file:
|
|
168
|
+
```python
|
|
169
|
+
from pulumi_django_azure.settings import * # noqa: F403
|
|
170
|
+
|
|
171
|
+
# This will provide the management command to purge the CDN
|
|
172
|
+
INSTALLED_APPS += ["pulumi_django_azure"]
|
|
173
|
+
|
|
174
|
+
# This will provide the health check middleware that will also take care of credential rotation.
|
|
175
|
+
MIDDLEWARE += ["pulumi_django_azure.middleware.HealthCheckMiddleware"]
|
|
176
|
+
```
|
|
177
|
+
This will pre-configure most settings to make your app work on Azure. You can check the source for details,
|
|
178
|
+
and ofcourse override any value after importing them.
|
|
179
|
+
|
|
131
180
|
|
|
181
|
+
## Deployment steps
|
|
132
182
|
1. Deploy without custom hosts (for CDN and websites)
|
|
133
183
|
2. Configure the PostgreSQL server (create and grant permissions to role for your websites)
|
|
134
184
|
3. Retrieve the deployment SSH key and configure your remote GIT repository with it
|
|
@@ -211,19 +261,8 @@ The environment variable will be suffixed with `_SECRET_NAME`.
|
|
|
211
261
|
|
|
212
262
|
Then, in your application, retrieve this data from the vault, e.g.:
|
|
213
263
|
```python
|
|
214
|
-
from azure.keyvault.secrets import SecretClient
|
|
215
|
-
from azure.identity import DefaultAzureCredential
|
|
216
|
-
|
|
217
|
-
# Azure credentials
|
|
218
|
-
azure_credential = DefaultAzureCredential()
|
|
219
|
-
|
|
220
|
-
# Azure Key Vault
|
|
221
|
-
AZURE_KEY_VAULT = env("AZURE_KEY_VAULT")
|
|
222
|
-
AZURE_KEY_VAULT_URI = f"https://{AZURE_KEY_VAULT}.vault.azure.net"
|
|
223
|
-
azure_key_vault_client = SecretClient(vault_url=AZURE_KEY_VAULT_URI, credential=azure_credential)
|
|
224
|
-
|
|
225
264
|
# Social Auth settings
|
|
226
|
-
oauth_secret =
|
|
265
|
+
oauth_secret = AZURE_KEY_VAULT_CLIENT.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
|
|
227
266
|
oauth_secret = json.loads(oauth_secret.value)
|
|
228
267
|
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = oauth_secret["client_id"]
|
|
229
268
|
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = oauth_secret["secret"]
|
|
@@ -257,81 +296,6 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
|
|
|
257
296
|
|
|
258
297
|
This would then trigger a redeploy everytime you make a commit to your live branch.
|
|
259
298
|
|
|
260
|
-
## CDN Purging
|
|
261
|
-
We added a management command to Django to purge the CDN cache, and added that to the startup script. Our version is here:
|
|
262
|
-
```python
|
|
263
|
-
import os
|
|
264
|
-
|
|
265
|
-
from azure.mgmt.cdn import CdnManagementClient
|
|
266
|
-
from azure.mgmt.cdn.models import PurgeParameters
|
|
267
|
-
from django.core.management.base import BaseCommand
|
|
268
|
-
|
|
269
|
-
from core.azure_helper import AZURE_CREDENTIAL, get_subscription_id
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
class Command(BaseCommand):
|
|
273
|
-
help = "Purges the CDN endpoint"
|
|
274
|
-
|
|
275
|
-
def add_arguments(self, parser):
|
|
276
|
-
parser.add_argument(
|
|
277
|
-
"--wait",
|
|
278
|
-
action="store_true",
|
|
279
|
-
help="Wait for the purge operation to complete",
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
def handle(self, *args, **options):
|
|
283
|
-
# Read environment variables
|
|
284
|
-
resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
|
|
285
|
-
profile_name = os.getenv("CDN_PROFILE")
|
|
286
|
-
endpoint_name = os.getenv("CDN_ENDPOINT")
|
|
287
|
-
content_paths = ["/*"]
|
|
288
|
-
|
|
289
|
-
# Ensure all required environment variables are set
|
|
290
|
-
if not all([resource_group, profile_name, endpoint_name]):
|
|
291
|
-
self.stderr.write(self.style.ERROR("Missing required environment variables."))
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
# Authenticate with Azure
|
|
295
|
-
cdn_client = CdnManagementClient(AZURE_CREDENTIAL, get_subscription_id())
|
|
296
|
-
|
|
297
|
-
try:
|
|
298
|
-
# Purge the CDN endpoint
|
|
299
|
-
purge_operation = cdn_client.endpoints.begin_purge_content(
|
|
300
|
-
resource_group_name=resource_group,
|
|
301
|
-
profile_name=profile_name,
|
|
302
|
-
endpoint_name=endpoint_name,
|
|
303
|
-
content_file_paths=PurgeParameters(content_paths=content_paths),
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
# Check if the --wait argument was provided
|
|
307
|
-
if options["wait"]:
|
|
308
|
-
purge_operation.result() # Wait for the operation to complete
|
|
309
|
-
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
|
|
310
|
-
else:
|
|
311
|
-
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
|
|
312
|
-
|
|
313
|
-
except Exception as e:
|
|
314
|
-
self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
And our azure_helper:
|
|
318
|
-
```python
|
|
319
|
-
from azure.identity import DefaultAzureCredential
|
|
320
|
-
from azure.mgmt.resource import SubscriptionClient
|
|
321
|
-
|
|
322
|
-
# Azure credentials
|
|
323
|
-
AZURE_CREDENTIAL = DefaultAzureCredential()
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def get_db_password() -> str:
|
|
327
|
-
return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def get_subscription_id() -> str:
|
|
331
|
-
subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
|
|
332
|
-
subscriptions = list(subscription_client.subscriptions.list())
|
|
333
|
-
return subscriptions[0].subscription_id
|
|
334
|
-
```
|
|
335
299
|
|
|
336
300
|
## Change requests
|
|
337
301
|
I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pulumi_django_azure/__init__.py,sha256=WoTHLNGnqc3dQoJtzrAJY8OVA7ReP6XFkDb9BXZGfJ8,117
|
|
2
|
+
pulumi_django_azure/azure_helper.py,sha256=HU0cb85jTeh70qjv11foWuqgE-SNQ21bhMKsy1lB7MM,1903
|
|
3
|
+
pulumi_django_azure/django_deployment.py,sha256=bSMczOl7nivLGVhfEXroDc_FyD0hXLne1TwhdhLMoNY,49260
|
|
4
|
+
pulumi_django_azure/middleware.py,sha256=kOWtlY3kjmUgpTXjDhGIzNeJtBWjn8FRmGQRZqWsSG0,1589
|
|
5
|
+
pulumi_django_azure/settings.py,sha256=cLSj1aWu6iKSFOeoeuSrC6TDpR4d6PUD-BJjaOx7FtY,5472
|
|
6
|
+
pulumi_django_azure/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
pulumi_django_azure/management/commands/purge_cdn.py,sha256=1lZI5liDIiOGyJRmo9gRn-BDLHaRuipaUmUAm06bRr0,2093
|
|
8
|
+
pulumi_django_azure-1.0.23.dist-info/licenses/LICENSE,sha256=NX2LN3U319Zaac8b7ZgfNOco_nTBbN531X_M_13niSg,1087
|
|
9
|
+
pulumi_django_azure-1.0.23.dist-info/METADATA,sha256=WhsPl3cERHbFqFjMJOiIlWJRoJpr40KU8qSqulqYDBw,12537
|
|
10
|
+
pulumi_django_azure-1.0.23.dist-info/WHEEL,sha256=tTnHoFhvKQHCh4jz3yCn0WPTYIy7wXx3CJtJ7SJGV7c,91
|
|
11
|
+
pulumi_django_azure-1.0.23.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
|
|
12
|
+
pulumi_django_azure-1.0.23.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pulumi_django_azure/__init__.py,sha256=5RY9reSVNw-HULrOXfhcq3cyPne-94ojFmeV1m6kIVg,79
|
|
2
|
-
pulumi_django_azure/django_deployment.py,sha256=e4DHIoB3ibzFHAuK1lwaRCLrLly5DdhikQLuhOMYpBc,49365
|
|
3
|
-
pulumi_django_azure-1.0.21.dist-info/LICENSE,sha256=NX2LN3U319Zaac8b7ZgfNOco_nTBbN531X_M_13niSg,1087
|
|
4
|
-
pulumi_django_azure-1.0.21.dist-info/METADATA,sha256=QiN9s5lzA-moy_wwLxZSScUqj-p1goLfUIHmBrG7eQA,13955
|
|
5
|
-
pulumi_django_azure-1.0.21.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
6
|
-
pulumi_django_azure-1.0.21.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
|
|
7
|
-
pulumi_django_azure-1.0.21.dist-info/RECORD,,
|
{pulumi_django_azure-1.0.21.dist-info → pulumi_django_azure-1.0.23.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|