pulumi-django-azure 1.0.4__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.

@@ -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(
@@ -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=azure.dbforpostgresql.SkuArgs(
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,104 @@ 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
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
+
351
447
  def _get_storage_account_access_keys(
352
448
  self, storage_account: azure.storage.StorageAccount
353
449
  ) -> Sequence[azure.storage.outputs.StorageAccountKeyResponse]:
@@ -394,6 +490,10 @@ class DjangoDeployment(pulumi.ComponentResource):
394
490
  website_hosts: list[str],
395
491
  django_settings_module: str,
396
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]] = [],
397
497
  ) -> azure.web.WebApp:
398
498
  """
399
499
  Create a Django website with it's own database and storage containers.
@@ -405,6 +505,12 @@ class DjangoDeployment(pulumi.ComponentResource):
405
505
  :param website_hosts: The list of custom host names for the website.
406
506
  :param django_settings_module: The Django settings module to load.
407
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).
408
514
  """
409
515
 
410
516
  # Create a database
@@ -433,6 +539,22 @@ class DjangoDeployment(pulumi.ComponentResource):
433
539
  container_name=f"{name}-static",
434
540
  )
435
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
+
436
558
  # Create a Django Secret Key (random)
437
559
  secret_key = pulumi_random.RandomString(f"django-secret-{name}-{self._name}", length=50)
438
560
 
@@ -455,6 +577,7 @@ class DjangoDeployment(pulumi.ComponentResource):
455
577
  ),
456
578
  https_only=True,
457
579
  site_config=azure.web.SiteConfigArgs(
580
+ app_command_line="cicd/startup.sh",
458
581
  always_on=True,
459
582
  health_check_path=self.HEALTH_CHECK_PATH,
460
583
  ftps_state=azure.web.FtpsState.DISABLED,
@@ -473,6 +596,8 @@ class DjangoDeployment(pulumi.ComponentResource):
473
596
  azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
474
597
  azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
475
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),
476
601
  # Storage settings
477
602
  azure.web.NameValuePairArgs(name="AZURE_STORAGE_ACCOUNT_NAME", value=self._storage_account.name),
478
603
  azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_STATICFILES", value=static_container.name),
@@ -517,6 +642,24 @@ class DjangoDeployment(pulumi.ComponentResource):
517
642
  f"{name}_deploy_ssh_key_url", app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1")
518
643
  )
519
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
+
520
663
  # Find the role for Storage Blob Data Contributor
521
664
  storage_role = self._storage_account.id.apply(
522
665
  lambda scope: azure.authorization.get_role_definition(
@@ -534,6 +677,24 @@ class DjangoDeployment(pulumi.ComponentResource):
534
677
  scope=self._storage_account.id,
535
678
  )
536
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
+
537
698
  # Create a CORS rules for this website
538
699
  if website_hosts:
539
700
  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.4
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
@@ -45,9 +45,23 @@ 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
+ 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.
51
65
  ## Installation
52
66
  This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
53
67
 
@@ -101,6 +115,8 @@ django.add_django_website(
101
115
  repository_branch="main",
102
116
  website_hosts=["example.com", "www.example.com"],
103
117
  django_settings_module="mywebsite.settings.production",
118
+ comms_data_location="europe",
119
+ comms_domains=["mydomain.com"],
104
120
  )
105
121
 
106
122
  django.add_database_administrator(
@@ -120,6 +136,7 @@ django.add_database_administrator(
120
136
  6. Re-deploy with custom hosts
121
137
  7. Re-deploy once more to enable HTTPS on website domains
122
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.
123
140
 
124
141
  ## Custom domain name for CDN
125
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.
@@ -172,6 +189,55 @@ pgAdmin will be created with a default login:
172
189
 
173
190
  Best practice is to log in right away, create a user for yourself and delete this default user.
174
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
+
175
241
  ## Automate deployments
176
242
  When using a service like GitLab, you can configure a Webhook to fire upon a push to your branch.
177
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
 
@@ -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,,