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.

@@ -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=azure.dbforpostgresql.SkuArgs(
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=config.require("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
- keys = azure.storage.list_storage_account_keys(
345
- resource_group_name=self._rg,
346
- account_name=storage_account.name,
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, tenant_id: 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=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.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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1 @@
1
+ pulumi_django_azure
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