pulumi-django-azure 1.0.0__py3-none-any.whl → 1.0.10__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 → pulumi_django_azure}/django_deployment.py +255 -18
- {pulumi_django_azure-1.0.0.dist-info → pulumi_django_azure-1.0.10.dist-info}/LICENSE +1 -1
- {pulumi_django_azure-1.0.0.dist-info → pulumi_django_azure-1.0.10.dist-info}/METADATA +130 -1
- pulumi_django_azure-1.0.10.dist-info/RECORD +7 -0
- {pulumi_django_azure-1.0.0.dist-info → pulumi_django_azure-1.0.10.dist-info}/WHEEL +1 -1
- pulumi_django_azure-1.0.10.dist-info/top_level.txt +1 -0
- pulumi-django-azure/__init__.py +0 -0
- pulumi_django_azure-1.0.0.dist-info/RECORD +0 -7
- pulumi_django_azure-1.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .django_deployment import DjangoDeployment # noqa: F401
|
|
@@ -11,8 +11,10 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
11
11
|
def __init__(
|
|
12
12
|
self,
|
|
13
13
|
name,
|
|
14
|
+
tenant_id: str,
|
|
14
15
|
resource_group_name: pulumi.Input[str],
|
|
15
16
|
vnet: azure.network.VirtualNetwork,
|
|
17
|
+
pgsql_sku: azure.dbforpostgresql.SkuArgs,
|
|
16
18
|
pgsql_ip_prefix: str,
|
|
17
19
|
appservice_ip_prefix: str,
|
|
18
20
|
app_service_sku: azure.web.SkuDescriptionArgs,
|
|
@@ -20,11 +22,29 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
20
22
|
cdn_host: Optional[str],
|
|
21
23
|
opts=None,
|
|
22
24
|
):
|
|
25
|
+
"""
|
|
26
|
+
Create a Django deployment.
|
|
27
|
+
|
|
28
|
+
:param name: The name of the deployment, will be used to name subresources.
|
|
29
|
+
:param tenant_id: The Entra tenant ID for the database authentication.
|
|
30
|
+
:param resource_group_name: The resource group name to create the resources in.
|
|
31
|
+
:param vnet: The virtual network to create the subnets in.
|
|
32
|
+
:param pgsql_sku: The SKU for the PostgreSQL server.
|
|
33
|
+
:param pgsql_ip_prefix: The IP prefix for the PostgreSQL subnet.
|
|
34
|
+
:param appservice_ip_prefix: The IP prefix for the app service subnet.
|
|
35
|
+
:param app_service_sku: The SKU for the app service plan.
|
|
36
|
+
:param storage_account_name: The name of the storage account. Should be unique across Azure.
|
|
37
|
+
:param cdn_host: A custom CDN host name (optional)
|
|
38
|
+
:param opts: The resource options
|
|
39
|
+
"""
|
|
40
|
+
|
|
23
41
|
super().__init__("pkg:index:DjangoDeployment", name, None, opts)
|
|
24
42
|
|
|
25
43
|
# child_opts = pulumi.ResourceOptions(parent=self)
|
|
44
|
+
self._config = pulumi.Config()
|
|
26
45
|
|
|
27
46
|
self._name = name
|
|
47
|
+
self._tenant_id = tenant_id
|
|
28
48
|
self._rg = resource_group_name
|
|
29
49
|
self._vnet = vnet
|
|
30
50
|
|
|
@@ -33,7 +53,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
33
53
|
self._cdn_host = self._create_cdn(custom_host=cdn_host)
|
|
34
54
|
|
|
35
55
|
# PostgreSQL resources
|
|
36
|
-
self._create_database(ip_prefix=pgsql_ip_prefix)
|
|
56
|
+
self._create_database(sku=pgsql_sku, ip_prefix=pgsql_ip_prefix)
|
|
37
57
|
|
|
38
58
|
# Subnet for the apps
|
|
39
59
|
self._app_subnet = self._create_subnet(
|
|
@@ -128,7 +148,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
128
148
|
# Return the default CDN host name
|
|
129
149
|
return self._cdn_endpoint.host_name
|
|
130
150
|
|
|
131
|
-
def _create_database(self, ip_prefix: str):
|
|
151
|
+
def _create_database(self, sku: azure.dbforpostgresql.SkuArgs, ip_prefix: str):
|
|
132
152
|
# Create subnet for PostgreSQL
|
|
133
153
|
subnet = self._create_subnet(
|
|
134
154
|
name="pgsql",
|
|
@@ -159,15 +179,12 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
159
179
|
self._pgsql = azure.dbforpostgresql.Server(
|
|
160
180
|
f"pgsql-{self._name}",
|
|
161
181
|
resource_group_name=self._rg,
|
|
162
|
-
sku=
|
|
163
|
-
name="Standard_B2s",
|
|
164
|
-
tier=azure.dbforpostgresql.SkuTier.BURSTABLE,
|
|
165
|
-
),
|
|
182
|
+
sku=sku,
|
|
166
183
|
version="16",
|
|
167
184
|
auth_config=azure.dbforpostgresql.AuthConfigArgs(
|
|
168
185
|
password_auth=azure.dbforpostgresql.PasswordAuthEnum.DISABLED,
|
|
169
186
|
active_directory_auth=azure.dbforpostgresql.ActiveDirectoryAuthEnum.ENABLED,
|
|
170
|
-
tenant_id=
|
|
187
|
+
tenant_id=self._tenant_id,
|
|
171
188
|
),
|
|
172
189
|
storage=azure.dbforpostgresql.StorageArgs(storage_size_gb=32),
|
|
173
190
|
network=azure.dbforpostgresql.NetworkArgs(
|
|
@@ -243,8 +260,6 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
243
260
|
# azure.web.NameValuePairArgs(name="WEBSITE_HTTPLOGGING_RETENTION_DAYS", value="7"),
|
|
244
261
|
# pgAdmin settings
|
|
245
262
|
azure.web.NameValuePairArgs(name="PGADMIN_DISABLE_POSTFIX", value="true"),
|
|
246
|
-
azure.web.NameValuePairArgs(name="PGADMIN_AUTHENTICATION_SOURCES", value="['oauth2, 'internal']"),
|
|
247
|
-
azure.web.NameValuePairArgs(name="PGADMIN_OAUTH2_NAME", value="azure"),
|
|
248
263
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_EMAIL", value="dbadmin@dbadmin.net"),
|
|
249
264
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_PASSWORD", value="dbadmin"),
|
|
250
265
|
],
|
|
@@ -331,6 +346,104 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
331
346
|
host_name=host,
|
|
332
347
|
)
|
|
333
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
|
|
396
|
+
vault = azure.keyvault.Vault(
|
|
397
|
+
f"vault-{suffix}",
|
|
398
|
+
resource_group_name=self._rg,
|
|
399
|
+
vault_name=f"vault-{suffix}",
|
|
400
|
+
properties=azure.keyvault.VaultPropertiesArgs(
|
|
401
|
+
tenant_id=self._tenant_id,
|
|
402
|
+
sku=azure.keyvault.SkuArgs(
|
|
403
|
+
name=azure.keyvault.SkuName.STANDARD,
|
|
404
|
+
family=azure.keyvault.SkuFamily.A,
|
|
405
|
+
),
|
|
406
|
+
enable_rbac_authorization=True,
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Find the Key Vault Administrator role
|
|
411
|
+
administrator_role = vault.id.apply(
|
|
412
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
413
|
+
role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
|
|
414
|
+
scope=scope,
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Add vault administrators
|
|
419
|
+
for a in administrators:
|
|
420
|
+
azure.authorization.RoleAssignment(
|
|
421
|
+
f"ra-{suffix}-vault-admin-{a}",
|
|
422
|
+
principal_id=a,
|
|
423
|
+
principal_type=azure.authorization.PrincipalType.USER,
|
|
424
|
+
role_definition_id=administrator_role.id,
|
|
425
|
+
scope=vault.id,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return vault
|
|
429
|
+
|
|
430
|
+
def _add_webapp_secret(self, vault: azure.keyvault.Vault, secret_name: str, config_secret_name: str, suffix: str):
|
|
431
|
+
secret = self._config.require_secret(config_secret_name)
|
|
432
|
+
|
|
433
|
+
# Normalize the secret name
|
|
434
|
+
secret_name = secret_name.replace("_", "-").lower()
|
|
435
|
+
|
|
436
|
+
# Create a secret in the vault
|
|
437
|
+
return azure.keyvault.Secret(
|
|
438
|
+
f"secret-{suffix}-{secret_name}",
|
|
439
|
+
resource_group_name=self._rg,
|
|
440
|
+
vault_name=vault.name,
|
|
441
|
+
secret_name=secret_name,
|
|
442
|
+
properties=azure.keyvault.SecretPropertiesArgs(
|
|
443
|
+
value=secret,
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
|
|
334
447
|
def _get_storage_account_access_keys(
|
|
335
448
|
self, storage_account: azure.storage.StorageAccount
|
|
336
449
|
) -> Sequence[azure.storage.outputs.StorageAccountKeyResponse]:
|
|
@@ -340,15 +453,23 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
340
453
|
:param storage_account: The storage account
|
|
341
454
|
:return: The access keys
|
|
342
455
|
"""
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
456
|
+
keys = pulumi.Output.all(self._rg, storage_account.name).apply(
|
|
457
|
+
lambda args: azure.storage.list_storage_account_keys(
|
|
458
|
+
resource_group_name=args[0],
|
|
459
|
+
account_name=args[1],
|
|
460
|
+
)
|
|
347
461
|
)
|
|
348
462
|
|
|
349
463
|
return keys.keys
|
|
350
464
|
|
|
351
|
-
def add_database_administrator(self, object_id: str, user_name: str
|
|
465
|
+
def add_database_administrator(self, object_id: str, user_name: str):
|
|
466
|
+
"""
|
|
467
|
+
Add an Entra ID as database administrator.
|
|
468
|
+
|
|
469
|
+
:param object_id: The object ID of the user
|
|
470
|
+
:param user_name: The user name (user@example.com)
|
|
471
|
+
"""
|
|
472
|
+
|
|
352
473
|
azure.dbforpostgresql.Administrator(
|
|
353
474
|
# Must be random but a GUID
|
|
354
475
|
f"pgsql-admin-{user_name.replace('@', '_')}-{self._name}",
|
|
@@ -357,7 +478,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
357
478
|
object_id=object_id,
|
|
358
479
|
principal_name=user_name,
|
|
359
480
|
principal_type=azure.dbforpostgresql.PrincipalType.USER,
|
|
360
|
-
tenant_id=
|
|
481
|
+
tenant_id=self._tenant_id,
|
|
361
482
|
)
|
|
362
483
|
|
|
363
484
|
def add_django_website(
|
|
@@ -368,7 +489,30 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
368
489
|
repository_branch: str,
|
|
369
490
|
website_hosts: list[str],
|
|
370
491
|
django_settings_module: str,
|
|
371
|
-
|
|
492
|
+
environment_variables: dict[str, str] = {},
|
|
493
|
+
secrets: dict[str, str] = {},
|
|
494
|
+
comms_data_location: Optional[str] = None,
|
|
495
|
+
comms_domains: Optional[list[str]] = [],
|
|
496
|
+
vault_administrators: Optional[list[str]] = [],
|
|
497
|
+
) -> azure.web.WebApp:
|
|
498
|
+
"""
|
|
499
|
+
Create a Django website with it's own database and storage containers.
|
|
500
|
+
|
|
501
|
+
:param name: The reference for the website, will be used to name subresources.
|
|
502
|
+
:param db_name: The name of the database to create.
|
|
503
|
+
:param repository_url: The URL of the Git repository.
|
|
504
|
+
:param repository_branch: The Git branch to deploy.
|
|
505
|
+
:param website_hosts: The list of custom host names for the website.
|
|
506
|
+
:param django_settings_module: The Django settings module to load.
|
|
507
|
+
:param environment_variables: A dictionary of environment variables to set.
|
|
508
|
+
:param secrets: A dictionary of secrets to store in the Key Vault and assign as environment variables.
|
|
509
|
+
The key is the name of the Pulumi secret, the value is the name of the environment variable
|
|
510
|
+
and the name of the secret in the Key Vault.
|
|
511
|
+
:param comms_data_location: The data location for the Communication Services (optional if you don't need it).
|
|
512
|
+
:param comms_domains: The list of custom domains for the E-mail Communication Services (optional).
|
|
513
|
+
:param vault_administrator: The principal ID of the vault administrator (optional).
|
|
514
|
+
"""
|
|
515
|
+
|
|
372
516
|
# Create a database
|
|
373
517
|
db = azure.dbforpostgresql.Database(
|
|
374
518
|
f"db-{name}",
|
|
@@ -395,9 +539,34 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
395
539
|
container_name=f"{name}-static",
|
|
396
540
|
)
|
|
397
541
|
|
|
542
|
+
# Communication Services (optional)
|
|
543
|
+
if comms_data_location:
|
|
544
|
+
comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
|
|
545
|
+
# Add the service endpoint as environment variable
|
|
546
|
+
environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
|
|
547
|
+
else:
|
|
548
|
+
comms = None
|
|
549
|
+
|
|
550
|
+
# Key Vault
|
|
551
|
+
vault = self._add_webapp_vault(vault_administrators, f"{name}-{self._name}")
|
|
552
|
+
|
|
553
|
+
# Add secrets
|
|
554
|
+
for config_name, env_name in secrets.items():
|
|
555
|
+
s = self._add_webapp_secret(vault, env_name, config_name, f"{name}-{self._name}")
|
|
556
|
+
environment_variables[f"{env_name}_SECRET_NAME"] = s.name
|
|
557
|
+
|
|
398
558
|
# Create a Django Secret Key (random)
|
|
399
559
|
secret_key = pulumi_random.RandomString(f"django-secret-{name}-{self._name}", length=50)
|
|
400
560
|
|
|
561
|
+
# Convert environment variables to NameValuePairArgs
|
|
562
|
+
environment_variables = [
|
|
563
|
+
azure.web.NameValuePairArgs(
|
|
564
|
+
name=key,
|
|
565
|
+
value=value,
|
|
566
|
+
)
|
|
567
|
+
for key, value in environment_variables.items()
|
|
568
|
+
]
|
|
569
|
+
|
|
401
570
|
app = azure.web.WebApp(
|
|
402
571
|
f"app-{name}-{self._name}",
|
|
403
572
|
resource_group_name=self._rg,
|
|
@@ -408,6 +577,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
408
577
|
),
|
|
409
578
|
https_only=True,
|
|
410
579
|
site_config=azure.web.SiteConfigArgs(
|
|
580
|
+
app_command_line="cicd/startup.sh",
|
|
411
581
|
always_on=True,
|
|
412
582
|
health_check_path=self.HEALTH_CHECK_PATH,
|
|
413
583
|
ftps_state=azure.web.FtpsState.DISABLED,
|
|
@@ -419,12 +589,15 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
419
589
|
azure.web.NameValuePairArgs(name="SCM_DO_BUILD_DURING_DEPLOYMENT", value="true"),
|
|
420
590
|
azure.web.NameValuePairArgs(name="PRE_BUILD_COMMAND", value="cicd/pre_build.sh"),
|
|
421
591
|
azure.web.NameValuePairArgs(name="POST_BUILD_COMMAND", value="cicd/post_build.sh"),
|
|
592
|
+
azure.web.NameValuePairArgs(name="DISABLE_COLLECTSTATIC", value="true"),
|
|
422
593
|
azure.web.NameValuePairArgs(name="HEALTH_CHECK_PATH", value=self.HEALTH_CHECK_PATH),
|
|
423
594
|
# Django settings
|
|
424
|
-
azure.web.NameValuePairArgs(name="DEBUG", value="true"),
|
|
595
|
+
# azure.web.NameValuePairArgs(name="DEBUG", value="true"),
|
|
425
596
|
azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
|
|
426
597
|
azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
|
|
427
598
|
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=",".join(website_hosts)),
|
|
599
|
+
# Vault settings
|
|
600
|
+
azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
|
|
428
601
|
# Storage settings
|
|
429
602
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_ACCOUNT_NAME", value=self._storage_account.name),
|
|
430
603
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_STATICFILES", value=static_container.name),
|
|
@@ -435,6 +608,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
435
608
|
azure.web.NameValuePairArgs(name="DB_HOST", value=self._pgsql.fully_qualified_domain_name),
|
|
436
609
|
azure.web.NameValuePairArgs(name="DB_NAME", value=db.name),
|
|
437
610
|
azure.web.NameValuePairArgs(name="DB_USER", value=f"{name}_managed_identity"),
|
|
611
|
+
*environment_variables,
|
|
438
612
|
],
|
|
439
613
|
),
|
|
440
614
|
)
|
|
@@ -468,6 +642,24 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
468
642
|
f"{name}_deploy_ssh_key_url", app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1")
|
|
469
643
|
)
|
|
470
644
|
|
|
645
|
+
# Find the role for Key Vault Secrets User
|
|
646
|
+
vault_access_role = vault.id.apply(
|
|
647
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
648
|
+
role_definition_id="4633458b-17de-408a-b874-0445c86b69e6",
|
|
649
|
+
scope=scope,
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# Grant the app access to the vault
|
|
654
|
+
azure.authorization.RoleAssignment(
|
|
655
|
+
f"ra-{name}-vault-user",
|
|
656
|
+
principal_id=principal_id,
|
|
657
|
+
principal_type=azure.authorization.PrincipalType.SERVICE_PRINCIPAL,
|
|
658
|
+
# Key Vault Secrets User
|
|
659
|
+
role_definition_id=vault_access_role.id,
|
|
660
|
+
scope=vault.id,
|
|
661
|
+
)
|
|
662
|
+
|
|
471
663
|
# Find the role for Storage Blob Data Contributor
|
|
472
664
|
storage_role = self._storage_account.id.apply(
|
|
473
665
|
lambda scope: azure.authorization.get_role_definition(
|
|
@@ -476,10 +668,55 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
476
668
|
)
|
|
477
669
|
)
|
|
478
670
|
|
|
671
|
+
# Grant the app access to the storage account
|
|
479
672
|
azure.authorization.RoleAssignment(
|
|
480
673
|
f"ra-{name}-storage",
|
|
481
674
|
principal_id=principal_id,
|
|
482
675
|
principal_type=azure.authorization.PrincipalType.SERVICE_PRINCIPAL,
|
|
483
676
|
role_definition_id=storage_role.id,
|
|
484
677
|
scope=self._storage_account.id,
|
|
485
|
-
)
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Grant the app to send e-mails
|
|
681
|
+
if comms:
|
|
682
|
+
comms_role = comms.id.apply(
|
|
683
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
684
|
+
# Contributor
|
|
685
|
+
role_definition_id="b24988ac-6180-42a0-ab88-20f7382dd24c",
|
|
686
|
+
scope=scope,
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
azure.authorization.RoleAssignment(
|
|
691
|
+
f"ra-{name}-comms",
|
|
692
|
+
principal_id=principal_id,
|
|
693
|
+
principal_type=azure.authorization.PrincipalType.SERVICE_PRINCIPAL,
|
|
694
|
+
role_definition_id=comms_role.id,
|
|
695
|
+
scope=comms.id,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Create a CORS rules for this website
|
|
699
|
+
if website_hosts:
|
|
700
|
+
origins = [f"https://{host}" for host in website_hosts]
|
|
701
|
+
else:
|
|
702
|
+
origins = ["*"]
|
|
703
|
+
|
|
704
|
+
azure.storage.BlobServiceProperties(
|
|
705
|
+
f"sa-{name}-blob-properties",
|
|
706
|
+
resource_group_name=self._rg,
|
|
707
|
+
account_name=self._storage_account.name,
|
|
708
|
+
blob_services_name="default",
|
|
709
|
+
cors=azure.storage.CorsRulesArgs(
|
|
710
|
+
cors_rules=[
|
|
711
|
+
azure.storage.CorsRuleArgs(
|
|
712
|
+
allowed_headers=["*"],
|
|
713
|
+
allowed_methods=["GET", "OPTIONS", "HEAD"],
|
|
714
|
+
allowed_origins=origins,
|
|
715
|
+
exposed_headers=["Access-Control-Allow-Origin"],
|
|
716
|
+
max_age_in_seconds=86400,
|
|
717
|
+
)
|
|
718
|
+
]
|
|
719
|
+
),
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
return app
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pulumi-django-azure
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.10
|
|
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
|
|
@@ -24,6 +24,7 @@ License: MIT License
|
|
|
24
24
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
25
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
26
|
SOFTWARE.
|
|
27
|
+
|
|
27
28
|
Project-URL: Homepage, https://gitlab.com/MaartenUreel/pulumi-django-azure
|
|
28
29
|
Keywords: django,pulumi,azure
|
|
29
30
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -44,9 +45,87 @@ To have a proper and secure environment, we need these components:
|
|
|
44
45
|
* Storage account for media and static files
|
|
45
46
|
* CDN endpoint in front with a domain name of our choosing
|
|
46
47
|
* PostgreSQL server
|
|
48
|
+
* Azure Communication Services to send e-mails
|
|
47
49
|
* Webapp with multiple custom host names and managed SSL for the website itself
|
|
50
|
+
* Azure Key Vault per application
|
|
48
51
|
* Webapp running pgAdmin
|
|
49
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
|
+
gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
|
|
63
|
+
```
|
|
64
|
+
Be sure to change `yourapplication` in the above.
|
|
65
|
+
## Installation
|
|
66
|
+
This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
|
|
67
|
+
|
|
68
|
+
To use a specific branch in your project, add to pyproject.toml dependencies:
|
|
69
|
+
```
|
|
70
|
+
pulumi-django-azure = { git = "git@gitlab.com:MaartenUreel/pulumi-django-azure.git", branch = "dev" }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A simple project could look like this:
|
|
74
|
+
```python
|
|
75
|
+
import pulumi
|
|
76
|
+
import pulumi_azure_native as azure
|
|
77
|
+
from pulumi_django_azure import DjangoDeployment
|
|
78
|
+
|
|
79
|
+
stack = pulumi.get_stack()
|
|
80
|
+
config = pulumi.Config()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Create resource group
|
|
84
|
+
rg = azure.resources.ResourceGroup(f"rg-{stack}")
|
|
85
|
+
|
|
86
|
+
# Create VNet
|
|
87
|
+
vnet = azure.network.VirtualNetwork(
|
|
88
|
+
f"vnet-{stack}",
|
|
89
|
+
resource_group_name=rg.name,
|
|
90
|
+
address_space=azure.network.AddressSpaceArgs(
|
|
91
|
+
address_prefixes=["10.0.0.0/16"],
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Deploy the website and all its components
|
|
96
|
+
django = DjangoDeployment(
|
|
97
|
+
stack,
|
|
98
|
+
tenant_id="abc123...",
|
|
99
|
+
resource_group_name=rg.name,
|
|
100
|
+
vnet=vnet,
|
|
101
|
+
pgsql_ip_prefix="10.0.10.0/24",
|
|
102
|
+
appservice_ip_prefix="10.0.20.0/24",
|
|
103
|
+
app_service_sku=azure.web.SkuDescriptionArgs(
|
|
104
|
+
name="B2",
|
|
105
|
+
tier="Basic",
|
|
106
|
+
),
|
|
107
|
+
storage_account_name="mystorageaccount",
|
|
108
|
+
cdn_host="cdn.example.com",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
django.add_django_website(
|
|
112
|
+
name="web",
|
|
113
|
+
db_name="mywebsite",
|
|
114
|
+
repository_url="git@gitlab.com:project/website.git",
|
|
115
|
+
repository_branch="main",
|
|
116
|
+
website_hosts=["example.com", "www.example.com"],
|
|
117
|
+
django_settings_module="mywebsite.settings.production",
|
|
118
|
+
comms_data_location="europe",
|
|
119
|
+
comms_domains=["mydomain.com"],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
django.add_database_administrator(
|
|
123
|
+
object_id="a1b2c3....",
|
|
124
|
+
user_name="user@example.com",
|
|
125
|
+
tenant_id="a1b2c3....",
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
50
129
|
## Deployment steps
|
|
51
130
|
|
|
52
131
|
1. Deploy without custom hosts (for CDN and websites)
|
|
@@ -57,6 +136,7 @@ To have a proper and secure environment, we need these components:
|
|
|
57
136
|
6. Re-deploy with custom hosts
|
|
58
137
|
7. Re-deploy once more to enable HTTPS on website domains
|
|
59
138
|
8. Manually activate HTTPS on the CDN host
|
|
139
|
+
9. Go to the e-mail communications service on Azure and configure DKIM, SPF,... for your custom domains.
|
|
60
140
|
|
|
61
141
|
## Custom domain name for CDN
|
|
62
142
|
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.
|
|
@@ -109,6 +189,55 @@ pgAdmin will be created with a default login:
|
|
|
109
189
|
|
|
110
190
|
Best practice is to log in right away, create a user for yourself and delete this default user.
|
|
111
191
|
|
|
192
|
+
## Azure OAuth2 / Django Social Auth
|
|
193
|
+
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:
|
|
194
|
+
```
|
|
195
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.key' secret_ID
|
|
196
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.secret' secret_value
|
|
197
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.tenant_id' directory_tenant_id
|
|
198
|
+
pulumi config set --secret --path 'mywebsite_social_auth_azure.client_id' application_id
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Then in your Django deployment, pass to the `add_django_website` command:
|
|
202
|
+
```
|
|
203
|
+
secrets={
|
|
204
|
+
"mywebsite_social_auth_azure": "AZURE_OAUTH",
|
|
205
|
+
},
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The value will be automatically stored in the vault where the application has access to.
|
|
209
|
+
The environment variable will be suffixed with `_SECRET_NAME`.
|
|
210
|
+
|
|
211
|
+
Then, in your application, retrieve this data from the vault, e.g.:
|
|
212
|
+
```python
|
|
213
|
+
from azure.keyvault.secrets import SecretClient
|
|
214
|
+
from azure.identity import DefaultAzureCredential
|
|
215
|
+
|
|
216
|
+
# Azure credentials
|
|
217
|
+
azure_credential = DefaultAzureCredential()
|
|
218
|
+
|
|
219
|
+
# Azure Key Vault
|
|
220
|
+
AZURE_KEY_VAULT = env("AZURE_KEY_VAULT")
|
|
221
|
+
AZURE_KEY_VAULT_URI = f"https://{AZURE_KEY_VAULT}.vault.azure.net"
|
|
222
|
+
azure_key_vault_client = SecretClient(vault_url=AZURE_KEY_VAULT_URI, credential=azure_credential)
|
|
223
|
+
|
|
224
|
+
# Social Auth settings
|
|
225
|
+
oauth_secret = azure_key_vault_client.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
|
|
226
|
+
oauth_secret = json.loads(oauth_secret.value)
|
|
227
|
+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = oauth_secret["client_id"]
|
|
228
|
+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = oauth_secret["secret"]
|
|
229
|
+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = oauth_secret["tenant_id"]
|
|
230
|
+
SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ["username", "first_name", "last_name", "email"]
|
|
231
|
+
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
|
|
232
|
+
|
|
233
|
+
AUTHENTICATION_BACKENDS = (
|
|
234
|
+
"social_core.backends.azuread_tenant.AzureADTenantOAuth2",
|
|
235
|
+
"django.contrib.auth.backends.ModelBackend",
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
And of course add the login button somewhere, following Django Social Auth instructions.
|
|
240
|
+
|
|
112
241
|
## Automate deployments
|
|
113
242
|
When using a service like GitLab, you can configure a Webhook to fire upon a push to your branch.
|
|
114
243
|
|
|
@@ -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=SgMLoQ2Jwm8anADzrEjVhtfJVAJrYJYFoaOL9bDZngE,30275
|
|
3
|
+
pulumi_django_azure-1.0.10.dist-info/LICENSE,sha256=NX2LN3U319Zaac8b7ZgfNOco_nTBbN531X_M_13niSg,1087
|
|
4
|
+
pulumi_django_azure-1.0.10.dist-info/METADATA,sha256=rAAqdkW9F0I0OWn7XQA2fHKIpqxmJDQWx39TmH0FEnc,11083
|
|
5
|
+
pulumi_django_azure-1.0.10.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
6
|
+
pulumi_django_azure-1.0.10.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
|
|
7
|
+
pulumi_django_azure-1.0.10.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pulumi_django_azure
|
pulumi-django-azure/__init__.py
DELETED
|
File without changes
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pulumi-django-azure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
pulumi-django-azure/django_deployment.py,sha256=GaQCEEKN8VSxFl7dmGN7dzCST7RPMkeamPJH3im2Be4,20162
|
|
3
|
-
pulumi_django_azure-1.0.0.dist-info/LICENSE,sha256=z4HD1y8njTvCggIu-d4Nt_Ha_lkNXUqJt1TeFhmQL-Y,1086
|
|
4
|
-
pulumi_django_azure-1.0.0.dist-info/METADATA,sha256=Rt0oQyqTtmou0hL3_zPALHh6enZWERrkz8uklKLPMIA,6206
|
|
5
|
-
pulumi_django_azure-1.0.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
6
|
-
pulumi_django_azure-1.0.0.dist-info/top_level.txt,sha256=1VBZjtWWLt9__5oAjjq4zj96rfdE79Kr1ve8F38aVNc,20
|
|
7
|
-
pulumi_django_azure-1.0.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pulumi-django-azure
|