pulumi-django-azure 1.0.4__py3-none-any.whl → 1.0.12__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/django_deployment.py +198 -11
- {pulumi_django_azure-1.0.4.dist-info → pulumi_django_azure-1.0.12.dist-info}/METADATA +144 -1
- pulumi_django_azure-1.0.12.dist-info/RECORD +7 -0
- {pulumi_django_azure-1.0.4.dist-info → pulumi_django_azure-1.0.12.dist-info}/WHEEL +1 -1
- pulumi_django_azure-1.0.4.dist-info/RECORD +0 -7
- {pulumi_django_azure-1.0.4.dist-info → pulumi_django_azure-1.0.12.dist-info}/LICENSE +0 -0
- {pulumi_django_azure-1.0.4.dist-info → pulumi_django_azure-1.0.12.dist-info}/top_level.txt +0 -0
|
@@ -14,6 +14,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
14
14
|
tenant_id: str,
|
|
15
15
|
resource_group_name: pulumi.Input[str],
|
|
16
16
|
vnet: azure.network.VirtualNetwork,
|
|
17
|
+
pgsql_sku: azure.dbforpostgresql.SkuArgs,
|
|
17
18
|
pgsql_ip_prefix: str,
|
|
18
19
|
appservice_ip_prefix: str,
|
|
19
20
|
app_service_sku: azure.web.SkuDescriptionArgs,
|
|
@@ -28,6 +29,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
28
29
|
:param tenant_id: The Entra tenant ID for the database authentication.
|
|
29
30
|
:param resource_group_name: The resource group name to create the resources in.
|
|
30
31
|
:param vnet: The virtual network to create the subnets in.
|
|
32
|
+
:param pgsql_sku: The SKU for the PostgreSQL server.
|
|
31
33
|
:param pgsql_ip_prefix: The IP prefix for the PostgreSQL subnet.
|
|
32
34
|
:param appservice_ip_prefix: The IP prefix for the app service subnet.
|
|
33
35
|
:param app_service_sku: The SKU for the app service plan.
|
|
@@ -39,6 +41,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
39
41
|
super().__init__("pkg:index:DjangoDeployment", name, None, opts)
|
|
40
42
|
|
|
41
43
|
# child_opts = pulumi.ResourceOptions(parent=self)
|
|
44
|
+
self._config = pulumi.Config()
|
|
42
45
|
|
|
43
46
|
self._name = name
|
|
44
47
|
self._tenant_id = tenant_id
|
|
@@ -50,7 +53,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
50
53
|
self._cdn_host = self._create_cdn(custom_host=cdn_host)
|
|
51
54
|
|
|
52
55
|
# PostgreSQL resources
|
|
53
|
-
self._create_database(ip_prefix=pgsql_ip_prefix)
|
|
56
|
+
self._create_database(sku=pgsql_sku, ip_prefix=pgsql_ip_prefix)
|
|
54
57
|
|
|
55
58
|
# Subnet for the apps
|
|
56
59
|
self._app_subnet = self._create_subnet(
|
|
@@ -92,7 +95,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
92
95
|
"""
|
|
93
96
|
|
|
94
97
|
# Put CDN in front
|
|
95
|
-
|
|
98
|
+
self._cdn_profile = azure.cdn.Profile(
|
|
96
99
|
f"cdn-{self._name}",
|
|
97
100
|
resource_group_name=self._rg,
|
|
98
101
|
location="global",
|
|
@@ -122,7 +125,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
122
125
|
],
|
|
123
126
|
is_http_allowed=False,
|
|
124
127
|
is_https_allowed=True,
|
|
125
|
-
profile_name=
|
|
128
|
+
profile_name=self._cdn_profile.name,
|
|
126
129
|
origin_host_header=endpoint_origin,
|
|
127
130
|
origins=[azure.cdn.DeepCreatedOriginArgs(name="origin-storage", host_name=endpoint_origin)],
|
|
128
131
|
query_string_caching_behavior=azure.cdn.QueryStringCachingBehavior.IGNORE_QUERY_STRING,
|
|
@@ -135,7 +138,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
135
138
|
azure.cdn.CustomDomain(
|
|
136
139
|
f"cdn-custom-domain-{self._name}",
|
|
137
140
|
resource_group_name=self._rg,
|
|
138
|
-
profile_name=
|
|
141
|
+
profile_name=self._cdn_profile.name,
|
|
139
142
|
endpoint_name=self._cdn_endpoint.name,
|
|
140
143
|
host_name=custom_host,
|
|
141
144
|
)
|
|
@@ -145,7 +148,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
145
148
|
# Return the default CDN host name
|
|
146
149
|
return self._cdn_endpoint.host_name
|
|
147
150
|
|
|
148
|
-
def _create_database(self, ip_prefix: str):
|
|
151
|
+
def _create_database(self, sku: azure.dbforpostgresql.SkuArgs, ip_prefix: str):
|
|
149
152
|
# Create subnet for PostgreSQL
|
|
150
153
|
subnet = self._create_subnet(
|
|
151
154
|
name="pgsql",
|
|
@@ -176,10 +179,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
176
179
|
self._pgsql = azure.dbforpostgresql.Server(
|
|
177
180
|
f"pgsql-{self._name}",
|
|
178
181
|
resource_group_name=self._rg,
|
|
179
|
-
sku=
|
|
180
|
-
name="Standard_B2s",
|
|
181
|
-
tier=azure.dbforpostgresql.SkuTier.BURSTABLE,
|
|
182
|
-
),
|
|
182
|
+
sku=sku,
|
|
183
183
|
version="16",
|
|
184
184
|
auth_config=azure.dbforpostgresql.AuthConfigArgs(
|
|
185
185
|
password_auth=azure.dbforpostgresql.PasswordAuthEnum.DISABLED,
|
|
@@ -260,8 +260,6 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
260
260
|
# azure.web.NameValuePairArgs(name="WEBSITE_HTTPLOGGING_RETENTION_DAYS", value="7"),
|
|
261
261
|
# pgAdmin settings
|
|
262
262
|
azure.web.NameValuePairArgs(name="PGADMIN_DISABLE_POSTFIX", value="true"),
|
|
263
|
-
azure.web.NameValuePairArgs(name="PGADMIN_AUTHENTICATION_SOURCES", value="['oauth2, 'internal']"),
|
|
264
|
-
azure.web.NameValuePairArgs(name="PGADMIN_OAUTH2_NAME", value="azure"),
|
|
265
263
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_EMAIL", value="dbadmin@dbadmin.net"),
|
|
266
264
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_PASSWORD", value="dbadmin"),
|
|
267
265
|
],
|
|
@@ -348,6 +346,112 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
348
346
|
host_name=host,
|
|
349
347
|
)
|
|
350
348
|
|
|
349
|
+
def _add_webapp_comms(self, data_location: str, domains: list[str], suffix: str) -> azure.communication.CommunicationService:
|
|
350
|
+
email_service = azure.communication.EmailService(
|
|
351
|
+
f"comms-email-{suffix}",
|
|
352
|
+
resource_group_name=self._rg,
|
|
353
|
+
location="global",
|
|
354
|
+
data_location=data_location,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
domain_resources = []
|
|
358
|
+
if domains:
|
|
359
|
+
# Add our own custom domains
|
|
360
|
+
for domain in domains:
|
|
361
|
+
safe_host = domain.replace(".", "-")
|
|
362
|
+
d = azure.communication.Domain(
|
|
363
|
+
f"comms-email-domain-{suffix}-{safe_host}",
|
|
364
|
+
resource_group_name=self._rg,
|
|
365
|
+
location="global",
|
|
366
|
+
domain_management=azure.communication.DomainManagement.CUSTOMER_MANAGED,
|
|
367
|
+
domain_name=domain,
|
|
368
|
+
email_service_name=email_service.name,
|
|
369
|
+
)
|
|
370
|
+
domain_resources.append(d.id.apply(lambda n: n))
|
|
371
|
+
else:
|
|
372
|
+
# Add an Azure managed domain
|
|
373
|
+
d = azure.communication.Domain(
|
|
374
|
+
f"comms-email-domain-{suffix}-azure",
|
|
375
|
+
resource_group_name=self._rg,
|
|
376
|
+
location="global",
|
|
377
|
+
domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
|
|
378
|
+
domain_name="AzureManagedDomain",
|
|
379
|
+
email_service_name=email_service.name,
|
|
380
|
+
)
|
|
381
|
+
domain_resources.append(d.id.apply(lambda n: n))
|
|
382
|
+
|
|
383
|
+
# Create Communication Services and link the domains
|
|
384
|
+
comm_service = azure.communication.CommunicationService(
|
|
385
|
+
f"comms-{suffix}",
|
|
386
|
+
resource_group_name=self._rg,
|
|
387
|
+
location="global",
|
|
388
|
+
data_location=data_location,
|
|
389
|
+
linked_domains=domain_resources,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return comm_service
|
|
393
|
+
|
|
394
|
+
def _add_webapp_vault(self, administrators: list[str], suffix: str) -> azure.keyvault.Vault:
|
|
395
|
+
# Create a keyvault with a random suffix to make the name unique
|
|
396
|
+
random_suffix = pulumi_random.RandomString(
|
|
397
|
+
f"vault-suffix-{suffix}",
|
|
398
|
+
# Total length is 24, so deduct the length of the suffix
|
|
399
|
+
length=(24 - 7 - len(suffix)),
|
|
400
|
+
special=False,
|
|
401
|
+
upper=False,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
vault = azure.keyvault.Vault(
|
|
405
|
+
f"vault-{suffix}",
|
|
406
|
+
resource_group_name=self._rg,
|
|
407
|
+
vault_name=random_suffix.result.apply(lambda r: f"vault-{suffix}-{r}"),
|
|
408
|
+
properties=azure.keyvault.VaultPropertiesArgs(
|
|
409
|
+
tenant_id=self._tenant_id,
|
|
410
|
+
sku=azure.keyvault.SkuArgs(
|
|
411
|
+
name=azure.keyvault.SkuName.STANDARD,
|
|
412
|
+
family=azure.keyvault.SkuFamily.A,
|
|
413
|
+
),
|
|
414
|
+
enable_rbac_authorization=True,
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Find the Key Vault Administrator role
|
|
419
|
+
administrator_role = vault.id.apply(
|
|
420
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
421
|
+
role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
|
|
422
|
+
scope=scope,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Add vault administrators
|
|
427
|
+
for a in administrators:
|
|
428
|
+
azure.authorization.RoleAssignment(
|
|
429
|
+
f"ra-{suffix}-vault-admin-{a}",
|
|
430
|
+
principal_id=a,
|
|
431
|
+
principal_type=azure.authorization.PrincipalType.USER,
|
|
432
|
+
role_definition_id=administrator_role.id,
|
|
433
|
+
scope=vault.id,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return vault
|
|
437
|
+
|
|
438
|
+
def _add_webapp_secret(self, vault: azure.keyvault.Vault, secret_name: str, config_secret_name: str, suffix: str):
|
|
439
|
+
secret = self._config.require_secret(config_secret_name)
|
|
440
|
+
|
|
441
|
+
# Normalize the secret name
|
|
442
|
+
secret_name = secret_name.replace("_", "-").lower()
|
|
443
|
+
|
|
444
|
+
# Create a secret in the vault
|
|
445
|
+
return azure.keyvault.Secret(
|
|
446
|
+
f"secret-{suffix}-{secret_name}",
|
|
447
|
+
resource_group_name=self._rg,
|
|
448
|
+
vault_name=vault.name,
|
|
449
|
+
secret_name=secret_name,
|
|
450
|
+
properties=azure.keyvault.SecretPropertiesArgs(
|
|
451
|
+
value=secret,
|
|
452
|
+
),
|
|
453
|
+
)
|
|
454
|
+
|
|
351
455
|
def _get_storage_account_access_keys(
|
|
352
456
|
self, storage_account: azure.storage.StorageAccount
|
|
353
457
|
) -> Sequence[azure.storage.outputs.StorageAccountKeyResponse]:
|
|
@@ -394,6 +498,10 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
394
498
|
website_hosts: list[str],
|
|
395
499
|
django_settings_module: str,
|
|
396
500
|
environment_variables: dict[str, str] = {},
|
|
501
|
+
secrets: dict[str, str] = {},
|
|
502
|
+
comms_data_location: Optional[str] = None,
|
|
503
|
+
comms_domains: Optional[list[str]] = [],
|
|
504
|
+
vault_administrators: Optional[list[str]] = [],
|
|
397
505
|
) -> azure.web.WebApp:
|
|
398
506
|
"""
|
|
399
507
|
Create a Django website with it's own database and storage containers.
|
|
@@ -405,6 +513,12 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
405
513
|
:param website_hosts: The list of custom host names for the website.
|
|
406
514
|
:param django_settings_module: The Django settings module to load.
|
|
407
515
|
:param environment_variables: A dictionary of environment variables to set.
|
|
516
|
+
:param secrets: A dictionary of secrets to store in the Key Vault and assign as environment variables.
|
|
517
|
+
The key is the name of the Pulumi secret, the value is the name of the environment variable
|
|
518
|
+
and the name of the secret in the Key Vault.
|
|
519
|
+
:param comms_data_location: The data location for the Communication Services (optional if you don't need it).
|
|
520
|
+
:param comms_domains: The list of custom domains for the E-mail Communication Services (optional).
|
|
521
|
+
:param vault_administrator: The principal ID of the vault administrator (optional).
|
|
408
522
|
"""
|
|
409
523
|
|
|
410
524
|
# Create a database
|
|
@@ -433,6 +547,22 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
433
547
|
container_name=f"{name}-static",
|
|
434
548
|
)
|
|
435
549
|
|
|
550
|
+
# Communication Services (optional)
|
|
551
|
+
if comms_data_location:
|
|
552
|
+
comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
|
|
553
|
+
# Add the service endpoint as environment variable
|
|
554
|
+
environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
|
|
555
|
+
else:
|
|
556
|
+
comms = None
|
|
557
|
+
|
|
558
|
+
# Key Vault
|
|
559
|
+
vault = self._add_webapp_vault(vault_administrators, f"{name}-{self._name}")
|
|
560
|
+
|
|
561
|
+
# Add secrets
|
|
562
|
+
for config_name, env_name in secrets.items():
|
|
563
|
+
s = self._add_webapp_secret(vault, env_name, config_name, f"{name}-{self._name}")
|
|
564
|
+
environment_variables[f"{env_name}_SECRET_NAME"] = s.name
|
|
565
|
+
|
|
436
566
|
# Create a Django Secret Key (random)
|
|
437
567
|
secret_key = pulumi_random.RandomString(f"django-secret-{name}-{self._name}", length=50)
|
|
438
568
|
|
|
@@ -455,6 +585,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
455
585
|
),
|
|
456
586
|
https_only=True,
|
|
457
587
|
site_config=azure.web.SiteConfigArgs(
|
|
588
|
+
app_command_line="cicd/startup.sh",
|
|
458
589
|
always_on=True,
|
|
459
590
|
health_check_path=self.HEALTH_CHECK_PATH,
|
|
460
591
|
ftps_state=azure.web.FtpsState.DISABLED,
|
|
@@ -473,12 +604,16 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
473
604
|
azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
|
|
474
605
|
azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
|
|
475
606
|
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=",".join(website_hosts)),
|
|
607
|
+
# Vault settings
|
|
608
|
+
azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
|
|
476
609
|
# Storage settings
|
|
477
610
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_ACCOUNT_NAME", value=self._storage_account.name),
|
|
478
611
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_STATICFILES", value=static_container.name),
|
|
479
612
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_MEDIA", value=media_container.name),
|
|
480
613
|
# CDN
|
|
481
614
|
azure.web.NameValuePairArgs(name="CDN_HOST", value=self._cdn_host),
|
|
615
|
+
azure.web.NameValuePairArgs(name="CDN_PROFILE", value=self._cdn_profile.name),
|
|
616
|
+
azure.web.NameValuePairArgs(name="CDN_ENDPOINT", value=self._cdn_endpoint.name),
|
|
482
617
|
# Database settings
|
|
483
618
|
azure.web.NameValuePairArgs(name="DB_HOST", value=self._pgsql.fully_qualified_domain_name),
|
|
484
619
|
azure.web.NameValuePairArgs(name="DB_NAME", value=db.name),
|
|
@@ -517,6 +652,24 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
517
652
|
f"{name}_deploy_ssh_key_url", app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1")
|
|
518
653
|
)
|
|
519
654
|
|
|
655
|
+
# Find the role for Key Vault Secrets User
|
|
656
|
+
vault_access_role = vault.id.apply(
|
|
657
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
658
|
+
role_definition_id="4633458b-17de-408a-b874-0445c86b69e6",
|
|
659
|
+
scope=scope,
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# Grant the app access to the vault
|
|
664
|
+
azure.authorization.RoleAssignment(
|
|
665
|
+
f"ra-{name}-vault-user",
|
|
666
|
+
principal_id=principal_id,
|
|
667
|
+
principal_type=azure.authorization.PrincipalType.SERVICE_PRINCIPAL,
|
|
668
|
+
# Key Vault Secrets User
|
|
669
|
+
role_definition_id=vault_access_role.id,
|
|
670
|
+
scope=vault.id,
|
|
671
|
+
)
|
|
672
|
+
|
|
520
673
|
# Find the role for Storage Blob Data Contributor
|
|
521
674
|
storage_role = self._storage_account.id.apply(
|
|
522
675
|
lambda scope: azure.authorization.get_role_definition(
|
|
@@ -534,6 +687,40 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
534
687
|
scope=self._storage_account.id,
|
|
535
688
|
)
|
|
536
689
|
|
|
690
|
+
# Grant the app to send e-mails
|
|
691
|
+
if comms:
|
|
692
|
+
comms_role = comms.id.apply(
|
|
693
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
694
|
+
# Contributor
|
|
695
|
+
role_definition_id="b24988ac-6180-42a0-ab88-20f7382dd24c",
|
|
696
|
+
scope=scope,
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
azure.authorization.RoleAssignment(
|
|
701
|
+
f"ra-{name}-comms",
|
|
702
|
+
principal_id=principal_id,
|
|
703
|
+
principal_type=azure.authorization.PrincipalType.SERVICE_PRINCIPAL,
|
|
704
|
+
role_definition_id=comms_role.id,
|
|
705
|
+
scope=comms.id,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
# Grant the app to purge the CDN endpoint
|
|
709
|
+
cdn_role = self._cdn_endpoint.id.apply(
|
|
710
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
711
|
+
role_definition_id="/426e0c7f-0c7e-4658-b36f-ff54d6c29b45",
|
|
712
|
+
scope=scope,
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
azure.authorization.RoleAssignment(
|
|
717
|
+
f"ra-{name}-cdn",
|
|
718
|
+
principal_id=principal_id,
|
|
719
|
+
principal_type=azure.authorization.PrincipalType.SERVICE_PRINCIPAL,
|
|
720
|
+
role_definition_id=cdn_role.id,
|
|
721
|
+
scope=self._cdn_endpoint.id,
|
|
722
|
+
)
|
|
723
|
+
|
|
537
724
|
# Create a CORS rules for this website
|
|
538
725
|
if website_hosts:
|
|
539
726
|
origins = [f"https://{host}" for host in website_hosts]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pulumi-django-azure
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.12
|
|
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
|
|
@@ -45,9 +45,24 @@ To have a proper and secure environment, we need these components:
|
|
|
45
45
|
* Storage account for media and static files
|
|
46
46
|
* CDN endpoint in front with a domain name of our choosing
|
|
47
47
|
* PostgreSQL server
|
|
48
|
+
* Azure Communication Services to send e-mails
|
|
48
49
|
* Webapp with multiple custom host names and managed SSL for the website itself
|
|
50
|
+
* Azure Key Vault per application
|
|
49
51
|
* Webapp running pgAdmin
|
|
50
52
|
|
|
53
|
+
## Project requirements
|
|
54
|
+
Your Django project should contain a folder `cicd` with these files:
|
|
55
|
+
* pre_build.sh: commands to be executed before building the application, for example NPM install, CSS build commands,...
|
|
56
|
+
* post_build.sh: commands to be executed after building the application, e.g. cleaning up.
|
|
57
|
+
Note that this runs in the identity of the build container, so you should not run database or storage manipulations here.
|
|
58
|
+
* startup.sh: commands to run the actual application. I recommend to put at least:
|
|
59
|
+
```bash
|
|
60
|
+
python manage.py migrate
|
|
61
|
+
python manage.py collectstatic --noinput
|
|
62
|
+
python manage.py purge_cdn
|
|
63
|
+
gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
|
|
64
|
+
```
|
|
65
|
+
Be sure to change `yourapplication` in the above. To see the `purge_cdn` management command we use here, see below.
|
|
51
66
|
## Installation
|
|
52
67
|
This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
|
|
53
68
|
|
|
@@ -101,6 +116,8 @@ django.add_django_website(
|
|
|
101
116
|
repository_branch="main",
|
|
102
117
|
website_hosts=["example.com", "www.example.com"],
|
|
103
118
|
django_settings_module="mywebsite.settings.production",
|
|
119
|
+
comms_data_location="europe",
|
|
120
|
+
comms_domains=["mydomain.com"],
|
|
104
121
|
)
|
|
105
122
|
|
|
106
123
|
django.add_database_administrator(
|
|
@@ -120,6 +137,7 @@ django.add_database_administrator(
|
|
|
120
137
|
6. Re-deploy with custom hosts
|
|
121
138
|
7. Re-deploy once more to enable HTTPS on website domains
|
|
122
139
|
8. Manually activate HTTPS on the CDN host
|
|
140
|
+
9. Go to the e-mail communications service on Azure and configure DKIM, SPF,... for your custom domains.
|
|
123
141
|
|
|
124
142
|
## Custom domain name for CDN
|
|
125
143
|
When deploying the first time, you will get a `cdn_cname` output. You need to create a CNAME to this domain before the deployment of the custom domain will succeed.
|
|
@@ -172,6 +190,55 @@ pgAdmin will be created with a default login:
|
|
|
172
190
|
|
|
173
191
|
Best practice is to log in right away, create a user for yourself and delete this default user.
|
|
174
192
|
|
|
193
|
+
## Azure OAuth2 / Django Social Auth
|
|
194
|
+
If you want to set up login with Azure, which would make sense since you are in the ecosystem, you need to create an App Registration in Entra ID, create a secret and then register these settings in your stack:
|
|
195
|
+
```
|
|
196
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.key' secret_ID
|
|
197
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.secret' secret_value
|
|
198
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.tenant_id' directory_tenant_id
|
|
199
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.client_id' application_id
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Then in your Django deployment, pass to the `add_django_website` command:
|
|
203
|
+
```
|
|
204
|
+
secrets={
|
|
205
|
+
"mywebsite_social_auth_azure": "AZURE_OAUTH",
|
|
206
|
+
},
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The value will be automatically stored in the vault where the application has access to.
|
|
210
|
+
The environment variable will be suffixed with `_SECRET_NAME`.
|
|
211
|
+
|
|
212
|
+
Then, in your application, retrieve this data from the vault, e.g.:
|
|
213
|
+
```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
|
+
# Social Auth settings
|
|
226
|
+
oauth_secret = azure_key_vault_client.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
|
|
227
|
+
oauth_secret = json.loads(oauth_secret.value)
|
|
228
|
+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = oauth_secret["client_id"]
|
|
229
|
+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = oauth_secret["secret"]
|
|
230
|
+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = oauth_secret["tenant_id"]
|
|
231
|
+
SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ["username", "first_name", "last_name", "email"]
|
|
232
|
+
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
|
|
233
|
+
|
|
234
|
+
AUTHENTICATION_BACKENDS = (
|
|
235
|
+
"social_core.backends.azuread_tenant.AzureADTenantOAuth2",
|
|
236
|
+
"django.contrib.auth.backends.ModelBackend",
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
And of course add the login button somewhere, following Django Social Auth instructions.
|
|
241
|
+
|
|
175
242
|
## Automate deployments
|
|
176
243
|
When using a service like GitLab, you can configure a Webhook to fire upon a push to your branch.
|
|
177
244
|
|
|
@@ -190,6 +257,82 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
|
|
|
190
257
|
|
|
191
258
|
This would then trigger a redeploy everytime you make a commit to your live branch.
|
|
192
259
|
|
|
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
|
+
|
|
193
336
|
## Change requests
|
|
194
337
|
I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
|
|
195
338
|
Therefore this project is not super generic, but tailored to my needs. I am however open to pull or change requests to improve this project or to make it more usable for others.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pulumi_django_azure/__init__.py,sha256=tXTvPfr8-Nll5cjMyY9yj_0z_PQ0XAcxihzHRCES-hU,63
|
|
2
|
+
pulumi_django_azure/django_deployment.py,sha256=MBdNH2iEOoNyWY5mxiLy8iUkW514LsG-wNjo4MVZlqY,31479
|
|
3
|
+
pulumi_django_azure-1.0.12.dist-info/LICENSE,sha256=NX2LN3U319Zaac8b7ZgfNOco_nTBbN531X_M_13niSg,1087
|
|
4
|
+
pulumi_django_azure-1.0.12.dist-info/METADATA,sha256=hPKNy_NeqAhvQGJFn-4Zw8AUiWU6pM0jpS9ypjzX2as,13957
|
|
5
|
+
pulumi_django_azure-1.0.12.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
6
|
+
pulumi_django_azure-1.0.12.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
|
|
7
|
+
pulumi_django_azure-1.0.12.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pulumi_django_azure/__init__.py,sha256=tXTvPfr8-Nll5cjMyY9yj_0z_PQ0XAcxihzHRCES-hU,63
|
|
2
|
-
pulumi_django_azure/django_deployment.py,sha256=yvpOUkcHs37Mb2xTD6BqtB21opxTvFLQzRxMHXTK2aY,23410
|
|
3
|
-
pulumi_django_azure-1.0.4.dist-info/LICENSE,sha256=NX2LN3U319Zaac8b7ZgfNOco_nTBbN531X_M_13niSg,1087
|
|
4
|
-
pulumi_django_azure-1.0.4.dist-info/METADATA,sha256=25RH8tZL2LrhBjuYRngUcRArER9TiAorLEaCJulFhLA,7887
|
|
5
|
-
pulumi_django_azure-1.0.4.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
6
|
-
pulumi_django_azure-1.0.4.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
|
|
7
|
-
pulumi_django_azure-1.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|