pulumi-django-azure 1.0.12__tar.gz → 1.0.14__tar.gz
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-1.0.12/src/pulumi_django_azure.egg-info → pulumi_django_azure-1.0.14}/PKG-INFO +1 -1
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/pyproject.toml +26 -9
- pulumi_django_azure-1.0.14/src/pulumi_django_azure/__init__.py +1 -0
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure/django_deployment.py +416 -100
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14/src/pulumi_django_azure.egg-info}/PKG-INFO +1 -1
- pulumi_django_azure-1.0.12/src/pulumi_django_azure/__init__.py +0 -1
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/LICENSE +0 -0
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/README.md +0 -0
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/setup.cfg +0 -0
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure.egg-info/SOURCES.txt +0 -0
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure.egg-info/dependency_links.txt +0 -0
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure.egg-info/requires.txt +0 -0
- {pulumi_django_azure-1.0.12 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pulumi-django-azure"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.14"
|
|
8
8
|
description = "Simply deployment of Django on Azure with Pulumi"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Maarten Ureel", email = "maarten@youreal.eu" }]
|
|
@@ -27,19 +27,36 @@ Homepage = "https://gitlab.com/MaartenUreel/pulumi-django-azure"
|
|
|
27
27
|
|
|
28
28
|
[tool.poetry]
|
|
29
29
|
name = "pulumi-django-azure"
|
|
30
|
-
version = "1.0.
|
|
30
|
+
version = "1.0.14"
|
|
31
31
|
description = "Simply deployment of Django on Azure with Pulumi"
|
|
32
32
|
authors = ["Maarten Ureel <maarten@youreal.eu>"]
|
|
33
33
|
|
|
34
34
|
[tool.poetry.dependencies]
|
|
35
35
|
python = "^3.11"
|
|
36
|
-
pulumi-azure-native = "^2.
|
|
37
|
-
pulumi = "^3.
|
|
38
|
-
pulumi-random = "^4.
|
|
36
|
+
pulumi-azure-native = "^2.64.2"
|
|
37
|
+
pulumi = "^3.135.0"
|
|
38
|
+
pulumi-random = "^4.16.6"
|
|
39
39
|
|
|
40
40
|
[tool.poetry.group.dev.dependencies]
|
|
41
|
-
twine = "^
|
|
42
|
-
build = "^1.
|
|
41
|
+
twine = "^5.1.1"
|
|
42
|
+
build = "^1.2.2"
|
|
43
|
+
ruff = "^0.4.9"
|
|
43
44
|
|
|
44
|
-
[tool.
|
|
45
|
-
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 140
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = [
|
|
50
|
+
# pycodestyle
|
|
51
|
+
"E",
|
|
52
|
+
# Pyflakes
|
|
53
|
+
"F",
|
|
54
|
+
# pyupgrade
|
|
55
|
+
"UP",
|
|
56
|
+
# flake8-bugbear
|
|
57
|
+
"B",
|
|
58
|
+
# flake8-simplify
|
|
59
|
+
"SIM",
|
|
60
|
+
# isort
|
|
61
|
+
"I",
|
|
62
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .django_deployment import DjangoDeployment, HostDefinition # noqa: F401
|
|
@@ -1,10 +1,56 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Optional, Union
|
|
2
3
|
|
|
3
4
|
import pulumi
|
|
4
5
|
import pulumi_azure_native as azure
|
|
5
6
|
import pulumi_random
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
class HostDefinition:
|
|
10
|
+
"""
|
|
11
|
+
A definition for a custom host name, optionally with a DNS zone.
|
|
12
|
+
|
|
13
|
+
:param host: The host name. If a zone is given, this is the relative host name.
|
|
14
|
+
:param zone: The DNS zone (optional).
|
|
15
|
+
:param identifier: An identifier for this host definition (optional).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, host: str, zone: Optional[azure.network.Zone] = None, identifier: Optional[str] = None):
|
|
19
|
+
self.host = host
|
|
20
|
+
self.zone = zone
|
|
21
|
+
self._identifier = identifier
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def identifier(self) -> str:
|
|
25
|
+
"""
|
|
26
|
+
The identifier for this host definition.
|
|
27
|
+
|
|
28
|
+
:return: The identifier
|
|
29
|
+
"""
|
|
30
|
+
if not self._identifier:
|
|
31
|
+
if self.zone:
|
|
32
|
+
raise ValueError(f"An identifier is required for the HostDefinition with host '{self.host}' ensure uniqueness.")
|
|
33
|
+
else:
|
|
34
|
+
# Use the host name as the identifier
|
|
35
|
+
return self.host.replace(".", "-")
|
|
36
|
+
else:
|
|
37
|
+
return self._identifier
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def full_host(self) -> pulumi.Output[str]:
|
|
41
|
+
"""
|
|
42
|
+
The full host name, including the zone.
|
|
43
|
+
|
|
44
|
+
:return: The full host name
|
|
45
|
+
"""
|
|
46
|
+
if not self.zone:
|
|
47
|
+
return pulumi.Output.concat(self.host)
|
|
48
|
+
elif self.host == "@":
|
|
49
|
+
return self.zone.name
|
|
50
|
+
else:
|
|
51
|
+
return pulumi.Output.concat(self.host, ".", self.zone.name)
|
|
52
|
+
|
|
53
|
+
|
|
8
54
|
class DjangoDeployment(pulumi.ComponentResource):
|
|
9
55
|
HEALTH_CHECK_PATH = "/health-check"
|
|
10
56
|
|
|
@@ -19,7 +65,9 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
19
65
|
appservice_ip_prefix: str,
|
|
20
66
|
app_service_sku: azure.web.SkuDescriptionArgs,
|
|
21
67
|
storage_account_name: str,
|
|
22
|
-
|
|
68
|
+
pgadmin_access_ip: Optional[Sequence[str]] = None,
|
|
69
|
+
pgadmin_dns_zone: Optional[azure.network.Zone] = None,
|
|
70
|
+
cdn_host: Optional[HostDefinition] = None,
|
|
23
71
|
opts=None,
|
|
24
72
|
):
|
|
25
73
|
"""
|
|
@@ -34,7 +82,9 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
34
82
|
:param appservice_ip_prefix: The IP prefix for the app service subnet.
|
|
35
83
|
:param app_service_sku: The SKU for the app service plan.
|
|
36
84
|
:param storage_account_name: The name of the storage account. Should be unique across Azure.
|
|
37
|
-
:param
|
|
85
|
+
:param pgadmin_access_ip: The IP addresses to allow access to pgAdmin. If empty, all IP addresses are allowed.
|
|
86
|
+
:param pgadmin_dns_zone: The Azure DNS zone to a pgadmin DNS record in. (optional)
|
|
87
|
+
:param cdn_host: A custom CDN host name. (optional)
|
|
38
88
|
:param opts: The resource options
|
|
39
89
|
"""
|
|
40
90
|
|
|
@@ -67,7 +117,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
67
117
|
self._app_service_plan = self._create_app_service_plan(sku=app_service_sku)
|
|
68
118
|
|
|
69
119
|
# Create a pgAdmin app
|
|
70
|
-
self._create_pgadmin_app()
|
|
120
|
+
self._create_pgadmin_app(access_ip=pgadmin_access_ip, dns_zone=pgadmin_dns_zone)
|
|
71
121
|
|
|
72
122
|
def _create_storage(self, account_name: str):
|
|
73
123
|
# Create blob storage
|
|
@@ -85,7 +135,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
85
135
|
enable_https_traffic_only=True,
|
|
86
136
|
)
|
|
87
137
|
|
|
88
|
-
def _create_cdn(self, custom_host: Optional[
|
|
138
|
+
def _create_cdn(self, custom_host: Optional[HostDefinition]) -> pulumi.Output[str]:
|
|
89
139
|
"""
|
|
90
140
|
Create a CDN endpoint. If a host name is given, it will be used as the custom domain.
|
|
91
141
|
Otherwise, the default CDN host name will be returned.
|
|
@@ -135,15 +185,41 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
135
185
|
|
|
136
186
|
# Add custom domain if given
|
|
137
187
|
if custom_host:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
if custom_host.zone:
|
|
189
|
+
# Create a DNS record for the custom host in the given zone
|
|
190
|
+
rs = azure.network.RecordSet(
|
|
191
|
+
f"cdn-cname-{self._name}",
|
|
192
|
+
resource_group_name=self._rg,
|
|
193
|
+
zone_name=custom_host.zone.name,
|
|
194
|
+
relative_record_set_name=custom_host.host,
|
|
195
|
+
record_type="CNAME",
|
|
196
|
+
ttl=3600,
|
|
197
|
+
target_resource=azure.network.SubResourceArgs(
|
|
198
|
+
id=self._cdn_endpoint.id,
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
azure.cdn.CustomDomain(
|
|
203
|
+
f"cdn-custom-domain-{self._name}",
|
|
204
|
+
resource_group_name=self._rg,
|
|
205
|
+
profile_name=self._cdn_profile.name,
|
|
206
|
+
endpoint_name=self._cdn_endpoint.name,
|
|
207
|
+
host_name=custom_host.full_host,
|
|
208
|
+
opts=pulumi.ResourceOptions(depends_on=rs),
|
|
209
|
+
)
|
|
145
210
|
|
|
146
|
-
|
|
211
|
+
return custom_host.full_host
|
|
212
|
+
else:
|
|
213
|
+
# Add custom hostname without a zone
|
|
214
|
+
azure.cdn.CustomDomain(
|
|
215
|
+
f"cdn-custom-domain-{self._name}",
|
|
216
|
+
resource_group_name=self._rg,
|
|
217
|
+
profile_name=self._cdn_profile.name,
|
|
218
|
+
endpoint_name=self._cdn_endpoint.name,
|
|
219
|
+
host_name=custom_host.host,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return custom_host.host
|
|
147
223
|
else:
|
|
148
224
|
# Return the default CDN host name
|
|
149
225
|
return self._cdn_endpoint.host_name
|
|
@@ -200,7 +276,11 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
200
276
|
pulumi.export("pgsql_host", self._pgsql.fully_qualified_domain_name)
|
|
201
277
|
|
|
202
278
|
def _create_subnet(
|
|
203
|
-
self,
|
|
279
|
+
self,
|
|
280
|
+
name,
|
|
281
|
+
prefix,
|
|
282
|
+
delegation_service: Optional[str] = None,
|
|
283
|
+
service_endpoints: Sequence[str] = [],
|
|
204
284
|
) -> azure.network.Subnet:
|
|
205
285
|
"""
|
|
206
286
|
Generic method to create a subnet with a delegation.
|
|
@@ -239,7 +319,22 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
239
319
|
sku=sku,
|
|
240
320
|
)
|
|
241
321
|
|
|
242
|
-
def _create_pgadmin_app(self):
|
|
322
|
+
def _create_pgadmin_app(self, access_ip: Optional[Sequence[str]] = None, dns_zone: Optional[azure.network.Zone] = None):
|
|
323
|
+
# Determine the IP restrictions
|
|
324
|
+
ip_restrictions = []
|
|
325
|
+
default_restriction = azure.web.DefaultAction.ALLOW
|
|
326
|
+
if access_ip:
|
|
327
|
+
default_restriction = azure.web.DefaultAction.DENY
|
|
328
|
+
|
|
329
|
+
for ip in access_ip:
|
|
330
|
+
ip_restrictions.append(
|
|
331
|
+
azure.web.IpSecurityRestrictionArgs(
|
|
332
|
+
action="Allow",
|
|
333
|
+
ip_address=ip,
|
|
334
|
+
priority=300,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
243
338
|
# The app itself
|
|
244
339
|
app = azure.web.WebApp(
|
|
245
340
|
f"app-pgadmin-{self._name}",
|
|
@@ -255,7 +350,10 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
255
350
|
linux_fx_version="DOCKER|dpage/pgadmin4",
|
|
256
351
|
health_check_path="/misc/ping",
|
|
257
352
|
app_settings=[
|
|
258
|
-
azure.web.NameValuePairArgs(
|
|
353
|
+
azure.web.NameValuePairArgs(
|
|
354
|
+
name="DOCKER_REGISTRY_SERVER_URL",
|
|
355
|
+
value="https://index.docker.io/v1",
|
|
356
|
+
),
|
|
259
357
|
azure.web.NameValuePairArgs(name="DOCKER_ENABLE_CI", value="true"),
|
|
260
358
|
# azure.web.NameValuePairArgs(name="WEBSITE_HTTPLOGGING_RETENTION_DAYS", value="7"),
|
|
261
359
|
# pgAdmin settings
|
|
@@ -263,6 +361,9 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
263
361
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_EMAIL", value="dbadmin@dbadmin.net"),
|
|
264
362
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_PASSWORD", value="dbadmin"),
|
|
265
363
|
],
|
|
364
|
+
# IP restrictions
|
|
365
|
+
ip_security_restrictions_default_action=default_restriction,
|
|
366
|
+
ip_security_restrictions=ip_restrictions,
|
|
266
367
|
),
|
|
267
368
|
)
|
|
268
369
|
|
|
@@ -274,6 +375,50 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
274
375
|
share_name="pgadmin",
|
|
275
376
|
)
|
|
276
377
|
|
|
378
|
+
if dns_zone:
|
|
379
|
+
# Create a DNS record for the pgAdmin app
|
|
380
|
+
cname = azure.network.RecordSet(
|
|
381
|
+
f"dns-cname-pgadmin-{self._name}",
|
|
382
|
+
resource_group_name=self._rg,
|
|
383
|
+
zone_name=dns_zone.name,
|
|
384
|
+
relative_record_set_name="pgadmin",
|
|
385
|
+
record_type="CNAME",
|
|
386
|
+
ttl=3600,
|
|
387
|
+
cname_record=azure.network.CnameRecordArgs(
|
|
388
|
+
cname=app.default_host_name,
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# For the certificate validation to work
|
|
393
|
+
txt_validation = azure.network.RecordSet(
|
|
394
|
+
f"dns-txt-pgadmin-{self._name}",
|
|
395
|
+
resource_group_name=self._rg,
|
|
396
|
+
zone_name=dns_zone.name,
|
|
397
|
+
relative_record_set_name="asuid.pgadmin",
|
|
398
|
+
record_type="TXT",
|
|
399
|
+
ttl=3600,
|
|
400
|
+
txt_records=[
|
|
401
|
+
azure.network.TxtRecordArgs(
|
|
402
|
+
value=[app.custom_domain_verification_id],
|
|
403
|
+
)
|
|
404
|
+
],
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Add custom hostname
|
|
408
|
+
self._add_webapp_host(
|
|
409
|
+
app=app,
|
|
410
|
+
host=dns_zone.name.apply(lambda name: f"pgadmin.{name}"),
|
|
411
|
+
suffix=self._name,
|
|
412
|
+
depends_on=[cname, txt_validation],
|
|
413
|
+
identifier="pgadmin",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Export the custom hostname
|
|
417
|
+
pulumi.export("pgadmin_url", dns_zone.name.apply(lambda name: f"https://pgadmin.{name}"))
|
|
418
|
+
else:
|
|
419
|
+
# Export the default hostname
|
|
420
|
+
pulumi.export("pgadmin_url", app.default_host_name.apply(lambda host: f"https://{host}"))
|
|
421
|
+
|
|
277
422
|
# Mount the storage container
|
|
278
423
|
azure.web.WebAppAzureStorageAccounts(
|
|
279
424
|
f"app-pgadmin-mount-{self._name}",
|
|
@@ -290,9 +435,24 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
290
435
|
},
|
|
291
436
|
)
|
|
292
437
|
|
|
293
|
-
|
|
438
|
+
def _get_existing_web_app_host_name_binding(self, resource_group_name: str, app_name: str, host_name: str):
|
|
439
|
+
try:
|
|
440
|
+
return azure.web.get_web_app_host_name_binding(
|
|
441
|
+
resource_group_name=resource_group_name,
|
|
442
|
+
name=app_name,
|
|
443
|
+
host_name=host_name,
|
|
444
|
+
)
|
|
445
|
+
except Exception:
|
|
446
|
+
return None
|
|
294
447
|
|
|
295
|
-
def _add_webapp_host(
|
|
448
|
+
def _add_webapp_host(
|
|
449
|
+
self,
|
|
450
|
+
app: azure.web.WebApp,
|
|
451
|
+
host: Union[str, pulumi.Input[str]],
|
|
452
|
+
suffix: str,
|
|
453
|
+
identifier: str,
|
|
454
|
+
depends_on: Optional[Sequence[pulumi.Resource]] = None,
|
|
455
|
+
):
|
|
296
456
|
"""
|
|
297
457
|
Because of a circular dependency, we need to create the certificate and the binding in two steps.
|
|
298
458
|
First we create a binding without a certificate,
|
|
@@ -305,48 +465,93 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
305
465
|
:param app: The web app
|
|
306
466
|
:param host: The host name
|
|
307
467
|
:param suffix: A suffix to make the resource name unique
|
|
468
|
+
:param depend_on: The resource to depend on (optional)
|
|
308
469
|
"""
|
|
309
470
|
|
|
310
|
-
|
|
471
|
+
if not depends_on:
|
|
472
|
+
depends_on = []
|
|
311
473
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
resource_group_name=
|
|
316
|
-
|
|
317
|
-
host_name=
|
|
474
|
+
# Retrieve the existing binding (None if it doesn't exist)
|
|
475
|
+
existing_binding = pulumi.Output.all(app.resource_group, app.name, host).apply(
|
|
476
|
+
lambda args: self._get_existing_web_app_host_name_binding(
|
|
477
|
+
resource_group_name=args[0],
|
|
478
|
+
app_name=args[1],
|
|
479
|
+
host_name=args[2],
|
|
318
480
|
)
|
|
481
|
+
)
|
|
319
482
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
483
|
+
# Create an inline function that we will invoke through the Output.apply lambda
|
|
484
|
+
def _create_binding_with_cert(existing_binding):
|
|
485
|
+
if existing_binding:
|
|
486
|
+
# Create a managed certificate
|
|
487
|
+
# This will work because the binding exists actually
|
|
488
|
+
certificate = azure.web.Certificate(
|
|
489
|
+
f"cert-{suffix}-{identifier}",
|
|
490
|
+
resource_group_name=self._rg,
|
|
491
|
+
server_farm_id=app.server_farm_id,
|
|
492
|
+
canonical_name=host,
|
|
493
|
+
host_names=[host],
|
|
494
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
495
|
+
)
|
|
329
496
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
497
|
+
# Create a new binding, replacing the old one,
|
|
498
|
+
# with the certificate
|
|
499
|
+
azure.web.WebAppHostNameBinding(
|
|
500
|
+
f"host-binding-{suffix}-{identifier}",
|
|
501
|
+
resource_group_name=self._rg,
|
|
502
|
+
name=app.name,
|
|
503
|
+
host_name=host,
|
|
504
|
+
ssl_state=azure.web.SslState.SNI_ENABLED,
|
|
505
|
+
thumbprint=certificate.thumbprint,
|
|
506
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
else:
|
|
510
|
+
# Create a binding without a certificate
|
|
511
|
+
azure.web.WebAppHostNameBinding(
|
|
512
|
+
f"host-binding-{suffix}-{identifier}",
|
|
513
|
+
resource_group_name=self._rg,
|
|
514
|
+
name=app.name,
|
|
515
|
+
host_name=host,
|
|
516
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
existing_binding.apply(_create_binding_with_cert)
|
|
520
|
+
|
|
521
|
+
def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.network.RecordSet]:
|
|
522
|
+
created_records = []
|
|
523
|
+
|
|
524
|
+
# Domain validation and SPF record (one TXT record with multiple values)
|
|
525
|
+
r = azure.network.RecordSet(
|
|
526
|
+
f"dns-comms-{suffix}-{host.identifier}-domain",
|
|
527
|
+
resource_group_name=self._rg,
|
|
528
|
+
zone_name=host.zone.name,
|
|
529
|
+
relative_record_set_name="@",
|
|
530
|
+
record_type="TXT",
|
|
531
|
+
ttl=3600,
|
|
532
|
+
txt_records=[
|
|
533
|
+
azure.network.TxtRecordArgs(value=[records["domain"]["value"]]),
|
|
534
|
+
azure.network.TxtRecordArgs(value=[records["s_pf"]["value"]]),
|
|
535
|
+
],
|
|
536
|
+
)
|
|
537
|
+
created_records.append(r)
|
|
538
|
+
|
|
539
|
+
# DKIM records (two CNAME records)
|
|
540
|
+
for record in ("d_kim", "d_kim2"):
|
|
541
|
+
r = azure.network.RecordSet(
|
|
542
|
+
f"dns-comms-{suffix}-{host.identifier}-{record}",
|
|
344
543
|
resource_group_name=self._rg,
|
|
345
|
-
|
|
346
|
-
|
|
544
|
+
zone_name=host.zone.name,
|
|
545
|
+
relative_record_set_name=records[record]["name"],
|
|
546
|
+
record_type="CNAME",
|
|
547
|
+
ttl=records[record]["ttl"],
|
|
548
|
+
cname_record=azure.network.CnameRecordArgs(cname=records[record]["value"]),
|
|
347
549
|
)
|
|
550
|
+
created_records.append(r)
|
|
348
551
|
|
|
349
|
-
|
|
552
|
+
return created_records
|
|
553
|
+
|
|
554
|
+
def _add_webapp_comms(self, data_location: str, domains: list[HostDefinition], suffix: str) -> azure.communication.CommunicationService:
|
|
350
555
|
email_service = azure.communication.EmailService(
|
|
351
556
|
f"comms-email-{suffix}",
|
|
352
557
|
resource_group_name=self._rg,
|
|
@@ -355,30 +560,37 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
355
560
|
)
|
|
356
561
|
|
|
357
562
|
domain_resources = []
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
563
|
+
comm_dependencies = []
|
|
564
|
+
|
|
565
|
+
# Add our own custom domains
|
|
566
|
+
for domain in domains:
|
|
373
567
|
d = azure.communication.Domain(
|
|
374
|
-
f"comms-email-domain-{suffix}-
|
|
568
|
+
f"comms-email-domain-{suffix}-{domain.identifier}",
|
|
375
569
|
resource_group_name=self._rg,
|
|
376
570
|
location="global",
|
|
377
|
-
domain_management=azure.communication.DomainManagement.
|
|
378
|
-
domain_name=
|
|
571
|
+
domain_management=azure.communication.DomainManagement.CUSTOMER_MANAGED,
|
|
572
|
+
domain_name=domain.full_host,
|
|
379
573
|
email_service_name=email_service.name,
|
|
380
574
|
)
|
|
381
|
-
|
|
575
|
+
|
|
576
|
+
if domain.zone:
|
|
577
|
+
# Create DNS records in the managed zone
|
|
578
|
+
comm_dependencies = pulumi.Output.all(suffix, domain, d.verification_records).apply(
|
|
579
|
+
lambda args: self._create_comms_dns_records(suffix=args[0], host=args[1], records=args[2])
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
domain_resources.append(d.id)
|
|
583
|
+
|
|
584
|
+
# Add an Azure managed domain
|
|
585
|
+
d = azure.communication.Domain(
|
|
586
|
+
f"comms-email-domain-{suffix}-azure",
|
|
587
|
+
resource_group_name=self._rg,
|
|
588
|
+
location="global",
|
|
589
|
+
domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
|
|
590
|
+
domain_name="AzureManagedDomain",
|
|
591
|
+
email_service_name=email_service.name,
|
|
592
|
+
)
|
|
593
|
+
domain_resources.append(d.id)
|
|
382
594
|
|
|
383
595
|
# Create Communication Services and link the domains
|
|
384
596
|
comm_service = azure.communication.CommunicationService(
|
|
@@ -387,6 +599,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
387
599
|
location="global",
|
|
388
600
|
data_location=data_location,
|
|
389
601
|
linked_domains=domain_resources,
|
|
602
|
+
opts=pulumi.ResourceOptions(depends_on=comm_dependencies),
|
|
390
603
|
)
|
|
391
604
|
|
|
392
605
|
return comm_service
|
|
@@ -415,27 +628,35 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
415
628
|
),
|
|
416
629
|
)
|
|
417
630
|
|
|
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
631
|
# Add vault administrators
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
632
|
+
if administrators:
|
|
633
|
+
# Find the Key Vault Administrator role
|
|
634
|
+
administrator_role = vault.id.apply(
|
|
635
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
636
|
+
role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
|
|
637
|
+
scope=scope,
|
|
638
|
+
)
|
|
434
639
|
)
|
|
435
640
|
|
|
641
|
+
# Actual administrator roles
|
|
642
|
+
for a in administrators:
|
|
643
|
+
azure.authorization.RoleAssignment(
|
|
644
|
+
f"ra-{suffix}-vault-admin-{a}",
|
|
645
|
+
principal_id=a,
|
|
646
|
+
principal_type=azure.authorization.PrincipalType.USER,
|
|
647
|
+
role_definition_id=administrator_role.id,
|
|
648
|
+
scope=vault.id,
|
|
649
|
+
)
|
|
650
|
+
|
|
436
651
|
return vault
|
|
437
652
|
|
|
438
|
-
def _add_webapp_secret(
|
|
653
|
+
def _add_webapp_secret(
|
|
654
|
+
self,
|
|
655
|
+
vault: azure.keyvault.Vault,
|
|
656
|
+
secret_name: str,
|
|
657
|
+
config_secret_name: str,
|
|
658
|
+
suffix: str,
|
|
659
|
+
):
|
|
439
660
|
secret = self._config.require_secret(config_secret_name)
|
|
440
661
|
|
|
441
662
|
# Normalize the secret name
|
|
@@ -495,13 +716,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
495
716
|
db_name: str,
|
|
496
717
|
repository_url: str,
|
|
497
718
|
repository_branch: str,
|
|
498
|
-
website_hosts: list[
|
|
719
|
+
website_hosts: list[HostDefinition],
|
|
499
720
|
django_settings_module: str,
|
|
500
|
-
environment_variables: dict[str, str] =
|
|
501
|
-
secrets: dict[str, str] =
|
|
721
|
+
environment_variables: Optional[dict[str, str]] = None,
|
|
722
|
+
secrets: Optional[dict[str, str]] = None,
|
|
502
723
|
comms_data_location: Optional[str] = None,
|
|
503
|
-
comms_domains: Optional[list[
|
|
504
|
-
vault_administrators: Optional[list[str]] =
|
|
724
|
+
comms_domains: Optional[list[HostDefinition]] = None,
|
|
725
|
+
vault_administrators: Optional[list[str]] = None,
|
|
505
726
|
) -> azure.web.WebApp:
|
|
506
727
|
"""
|
|
507
728
|
Create a Django website with it's own database and storage containers.
|
|
@@ -549,7 +770,11 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
549
770
|
|
|
550
771
|
# Communication Services (optional)
|
|
551
772
|
if comms_data_location:
|
|
773
|
+
if not comms_domains:
|
|
774
|
+
comms_domains = []
|
|
775
|
+
|
|
552
776
|
comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
|
|
777
|
+
|
|
553
778
|
# Add the service endpoint as environment variable
|
|
554
779
|
environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
|
|
555
780
|
else:
|
|
@@ -575,6 +800,8 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
575
800
|
for key, value in environment_variables.items()
|
|
576
801
|
]
|
|
577
802
|
|
|
803
|
+
allowed_hosts = pulumi.Output.concat(*[pulumi.Output.concat(host.full_host, ",") for host in website_hosts])
|
|
804
|
+
|
|
578
805
|
app = azure.web.WebApp(
|
|
579
806
|
f"app-{name}-{self._name}",
|
|
580
807
|
resource_group_name=self._rg,
|
|
@@ -603,12 +830,18 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
603
830
|
# azure.web.NameValuePairArgs(name="DEBUG", value="true"),
|
|
604
831
|
azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
|
|
605
832
|
azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
|
|
606
|
-
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=
|
|
833
|
+
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=allowed_hosts),
|
|
607
834
|
# Vault settings
|
|
608
835
|
azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
|
|
609
836
|
# Storage settings
|
|
610
|
-
azure.web.NameValuePairArgs(
|
|
611
|
-
|
|
837
|
+
azure.web.NameValuePairArgs(
|
|
838
|
+
name="AZURE_STORAGE_ACCOUNT_NAME",
|
|
839
|
+
value=self._storage_account.name,
|
|
840
|
+
),
|
|
841
|
+
azure.web.NameValuePairArgs(
|
|
842
|
+
name="AZURE_STORAGE_CONTAINER_STATICFILES",
|
|
843
|
+
value=static_container.name,
|
|
844
|
+
),
|
|
612
845
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_MEDIA", value=media_container.name),
|
|
613
846
|
# CDN
|
|
614
847
|
azure.web.NameValuePairArgs(name="CDN_HOST", value=self._cdn_host),
|
|
@@ -631,9 +864,85 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
631
864
|
# We need this to verify custom domains
|
|
632
865
|
pulumi.export(f"{name}_site_domain_verification_id", app.custom_domain_verification_id)
|
|
633
866
|
pulumi.export(f"{name}_site_domain_cname", app.default_host_name)
|
|
867
|
+
virtual_ip = app.outbound_ip_addresses.apply(lambda addresses: addresses.split(",")[-1])
|
|
868
|
+
pulumi.export(f"{name}_site_virtual_ip", virtual_ip)
|
|
869
|
+
|
|
870
|
+
# Get the URL of the publish profile.
|
|
871
|
+
# Use app.identity here too to ensure the app is actually created before getting credentials.
|
|
872
|
+
credentials = pulumi.Output.all(self._rg, app.name, app.identity).apply(
|
|
873
|
+
lambda args: azure.web.list_web_app_publishing_credentials(
|
|
874
|
+
resource_group_name=args[0],
|
|
875
|
+
name=args[1],
|
|
876
|
+
)
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
pulumi.export(f"{name}_deploy_url", pulumi.Output.concat(credentials.scm_uri, "/deploy"))
|
|
634
880
|
|
|
635
881
|
for host in website_hosts:
|
|
636
|
-
|
|
882
|
+
dependencies = []
|
|
883
|
+
|
|
884
|
+
if host.zone:
|
|
885
|
+
# Create a DNS record in the zone
|
|
886
|
+
|
|
887
|
+
if host.host == "@":
|
|
888
|
+
# Create a A record for the virtual IP address
|
|
889
|
+
a = azure.network.RecordSet(
|
|
890
|
+
f"dns-a-{name}-{self._name}-{host.identifier}",
|
|
891
|
+
resource_group_name=self._rg,
|
|
892
|
+
zone_name=host.zone.name,
|
|
893
|
+
relative_record_set_name=host.host,
|
|
894
|
+
record_type="A",
|
|
895
|
+
ttl=3600,
|
|
896
|
+
a_records=[
|
|
897
|
+
azure.network.ARecordArgs(
|
|
898
|
+
ipv4_address=virtual_ip,
|
|
899
|
+
)
|
|
900
|
+
],
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
dependencies.append(a)
|
|
904
|
+
else:
|
|
905
|
+
# Create a CNAME record for the custom hostname
|
|
906
|
+
cname = azure.network.RecordSet(
|
|
907
|
+
f"dns-cname-{name}-{self._name}-{host.identifier}",
|
|
908
|
+
resource_group_name=self._rg,
|
|
909
|
+
zone_name=host.zone.name,
|
|
910
|
+
relative_record_set_name=host.host,
|
|
911
|
+
record_type="CNAME",
|
|
912
|
+
ttl=3600,
|
|
913
|
+
cname_record=azure.network.CnameRecordArgs(
|
|
914
|
+
cname=app.default_host_name,
|
|
915
|
+
),
|
|
916
|
+
)
|
|
917
|
+
dependencies.append(cname)
|
|
918
|
+
|
|
919
|
+
# For the certificate validation to work
|
|
920
|
+
relative_record_set_name = "asuid" if host.host == "@" else pulumi.Output.concat("asuid.", host.host)
|
|
921
|
+
|
|
922
|
+
txt_validation = azure.network.RecordSet(
|
|
923
|
+
f"dns-txt-{name}-{self._name}-{host.identifier}",
|
|
924
|
+
resource_group_name=self._rg,
|
|
925
|
+
zone_name=host.zone.name,
|
|
926
|
+
relative_record_set_name=relative_record_set_name,
|
|
927
|
+
record_type="TXT",
|
|
928
|
+
ttl=3600,
|
|
929
|
+
txt_records=[
|
|
930
|
+
azure.network.TxtRecordArgs(
|
|
931
|
+
value=[app.custom_domain_verification_id],
|
|
932
|
+
)
|
|
933
|
+
],
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
dependencies.append(txt_validation)
|
|
937
|
+
|
|
938
|
+
# Add the host with optional dependencies
|
|
939
|
+
self._add_webapp_host(
|
|
940
|
+
app=app,
|
|
941
|
+
host=host.full_host,
|
|
942
|
+
suffix=f"{name}-{self._name}",
|
|
943
|
+
identifier=host.identifier,
|
|
944
|
+
depends_on=dependencies,
|
|
945
|
+
)
|
|
637
946
|
|
|
638
947
|
# To enable deployment from GitLab
|
|
639
948
|
azure.web.WebAppSourceControl(
|
|
@@ -649,7 +958,8 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
649
958
|
|
|
650
959
|
# Where we can retrieve the SSH key
|
|
651
960
|
pulumi.export(
|
|
652
|
-
f"{name}_deploy_ssh_key_url",
|
|
961
|
+
f"{name}_deploy_ssh_key_url",
|
|
962
|
+
app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1"),
|
|
653
963
|
)
|
|
654
964
|
|
|
655
965
|
# Find the role for Key Vault Secrets User
|
|
@@ -722,10 +1032,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
722
1032
|
)
|
|
723
1033
|
|
|
724
1034
|
# Create a CORS rules for this website
|
|
725
|
-
if website_hosts
|
|
726
|
-
origins = [f"https://{host}" for host in website_hosts]
|
|
727
|
-
else:
|
|
728
|
-
origins = ["*"]
|
|
1035
|
+
origins = [pulumi.Output.concat("https://", host.full_host) for host in website_hosts] if website_hosts else ["*"]
|
|
729
1036
|
|
|
730
1037
|
azure.storage.BlobServiceProperties(
|
|
731
1038
|
f"sa-{name}-blob-properties",
|
|
@@ -746,3 +1053,12 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
746
1053
|
)
|
|
747
1054
|
|
|
748
1055
|
return app
|
|
1056
|
+
|
|
1057
|
+
def _strip_off_dns_zone_name(self, host: str, zone: azure.network.Zone) -> pulumi.Output[str]:
|
|
1058
|
+
"""
|
|
1059
|
+
Strip off the DNS zone name from the host name.
|
|
1060
|
+
|
|
1061
|
+
:param host: The host name
|
|
1062
|
+
:return: The host name without the DNS zone
|
|
1063
|
+
"""
|
|
1064
|
+
return zone.name.apply(lambda name: host[: -len(name) - 1])
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .django_deployment import DjangoDeployment # noqa: F401
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|