pulumi-django-azure 1.0.12__py3-none-any.whl → 1.0.15__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 -1
- pulumi_django_azure/django_deployment.py +438 -122
- {pulumi_django_azure-1.0.12.dist-info → pulumi_django_azure-1.0.15.dist-info}/LICENSE +1 -1
- {pulumi_django_azure-1.0.12.dist-info → pulumi_django_azure-1.0.15.dist-info}/METADATA +4 -4
- pulumi_django_azure-1.0.15.dist-info/RECORD +7 -0
- {pulumi_django_azure-1.0.12.dist-info → pulumi_django_azure-1.0.15.dist-info}/WHEEL +1 -1
- pulumi_django_azure-1.0.12.dist-info/RECORD +0 -7
- {pulumi_django_azure-1.0.12.dist-info → pulumi_django_azure-1.0.15.dist-info}/top_level.txt +0 -0
pulumi_django_azure/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from .django_deployment import DjangoDeployment # noqa: F401
|
|
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,10 @@ 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
|
+
storage_allowed_origins: Optional[Sequence[str]] = None,
|
|
69
|
+
pgadmin_access_ip: Optional[Sequence[str]] = None,
|
|
70
|
+
pgadmin_dns_zone: Optional[azure.network.Zone] = None,
|
|
71
|
+
cdn_host: Optional[HostDefinition] = None,
|
|
23
72
|
opts=None,
|
|
24
73
|
):
|
|
25
74
|
"""
|
|
@@ -34,7 +83,10 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
34
83
|
:param appservice_ip_prefix: The IP prefix for the app service subnet.
|
|
35
84
|
:param app_service_sku: The SKU for the app service plan.
|
|
36
85
|
:param storage_account_name: The name of the storage account. Should be unique across Azure.
|
|
37
|
-
:param
|
|
86
|
+
:param storage_allowed_origins: The origins (hosts) to allow access through CORS policy. You can specify '*' to allow all.
|
|
87
|
+
:param pgadmin_access_ip: The IP addresses to allow access to pgAdmin. If empty, all IP addresses are allowed.
|
|
88
|
+
:param pgadmin_dns_zone: The Azure DNS zone to a pgadmin DNS record in. (optional)
|
|
89
|
+
:param cdn_host: A custom CDN host name. (optional)
|
|
38
90
|
:param opts: The resource options
|
|
39
91
|
"""
|
|
40
92
|
|
|
@@ -49,7 +101,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
49
101
|
self._vnet = vnet
|
|
50
102
|
|
|
51
103
|
# Storage resources
|
|
52
|
-
self._create_storage(account_name=storage_account_name)
|
|
104
|
+
self._create_storage(account_name=storage_account_name, allowed_origins=storage_allowed_origins)
|
|
53
105
|
self._cdn_host = self._create_cdn(custom_host=cdn_host)
|
|
54
106
|
|
|
55
107
|
# PostgreSQL resources
|
|
@@ -67,9 +119,9 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
67
119
|
self._app_service_plan = self._create_app_service_plan(sku=app_service_sku)
|
|
68
120
|
|
|
69
121
|
# Create a pgAdmin app
|
|
70
|
-
self._create_pgadmin_app()
|
|
122
|
+
self._create_pgadmin_app(access_ip=pgadmin_access_ip, dns_zone=pgadmin_dns_zone)
|
|
71
123
|
|
|
72
|
-
def _create_storage(self, account_name: str):
|
|
124
|
+
def _create_storage(self, account_name: str, allowed_origins: Optional[Sequence[str]] = None):
|
|
73
125
|
# Create blob storage
|
|
74
126
|
self._storage_account = azure.storage.StorageAccount(
|
|
75
127
|
f"sa-{self._name}",
|
|
@@ -85,7 +137,26 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
85
137
|
enable_https_traffic_only=True,
|
|
86
138
|
)
|
|
87
139
|
|
|
88
|
-
|
|
140
|
+
if allowed_origins:
|
|
141
|
+
azure.storage.BlobServiceProperties(
|
|
142
|
+
f"sa-{self._name}-blob-properties",
|
|
143
|
+
resource_group_name=self._rg,
|
|
144
|
+
account_name=self._storage_account.name,
|
|
145
|
+
blob_services_name="default",
|
|
146
|
+
cors=azure.storage.CorsRulesArgs(
|
|
147
|
+
cors_rules=[
|
|
148
|
+
azure.storage.CorsRuleArgs(
|
|
149
|
+
allowed_headers=["*"],
|
|
150
|
+
allowed_methods=["GET", "OPTIONS", "HEAD"],
|
|
151
|
+
allowed_origins=allowed_origins,
|
|
152
|
+
exposed_headers=["Access-Control-Allow-Origin"],
|
|
153
|
+
max_age_in_seconds=86400,
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _create_cdn(self, custom_host: Optional[HostDefinition]) -> pulumi.Output[str]:
|
|
89
160
|
"""
|
|
90
161
|
Create a CDN endpoint. If a host name is given, it will be used as the custom domain.
|
|
91
162
|
Otherwise, the default CDN host name will be returned.
|
|
@@ -128,22 +199,48 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
128
199
|
profile_name=self._cdn_profile.name,
|
|
129
200
|
origin_host_header=endpoint_origin,
|
|
130
201
|
origins=[azure.cdn.DeepCreatedOriginArgs(name="origin-storage", host_name=endpoint_origin)],
|
|
131
|
-
query_string_caching_behavior=azure.cdn.QueryStringCachingBehavior.
|
|
202
|
+
query_string_caching_behavior=azure.cdn.QueryStringCachingBehavior.USE_QUERY_STRING,
|
|
132
203
|
)
|
|
133
204
|
|
|
134
205
|
pulumi.export("cdn_cname", self._cdn_endpoint.host_name)
|
|
135
206
|
|
|
136
207
|
# Add custom domain if given
|
|
137
208
|
if custom_host:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
if custom_host.zone:
|
|
210
|
+
# Create a DNS record for the custom host in the given zone
|
|
211
|
+
rs = azure.network.RecordSet(
|
|
212
|
+
f"cdn-cname-{self._name}",
|
|
213
|
+
resource_group_name=self._rg,
|
|
214
|
+
zone_name=custom_host.zone.name,
|
|
215
|
+
relative_record_set_name=custom_host.host,
|
|
216
|
+
record_type="CNAME",
|
|
217
|
+
ttl=3600,
|
|
218
|
+
target_resource=azure.network.SubResourceArgs(
|
|
219
|
+
id=self._cdn_endpoint.id,
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
azure.cdn.CustomDomain(
|
|
224
|
+
f"cdn-custom-domain-{self._name}",
|
|
225
|
+
resource_group_name=self._rg,
|
|
226
|
+
profile_name=self._cdn_profile.name,
|
|
227
|
+
endpoint_name=self._cdn_endpoint.name,
|
|
228
|
+
host_name=custom_host.full_host,
|
|
229
|
+
opts=pulumi.ResourceOptions(depends_on=rs),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return custom_host.full_host
|
|
233
|
+
else:
|
|
234
|
+
# Add custom hostname without a zone
|
|
235
|
+
azure.cdn.CustomDomain(
|
|
236
|
+
f"cdn-custom-domain-{self._name}",
|
|
237
|
+
resource_group_name=self._rg,
|
|
238
|
+
profile_name=self._cdn_profile.name,
|
|
239
|
+
endpoint_name=self._cdn_endpoint.name,
|
|
240
|
+
host_name=custom_host.host,
|
|
241
|
+
)
|
|
145
242
|
|
|
146
|
-
|
|
243
|
+
return custom_host.host
|
|
147
244
|
else:
|
|
148
245
|
# Return the default CDN host name
|
|
149
246
|
return self._cdn_endpoint.host_name
|
|
@@ -200,7 +297,11 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
200
297
|
pulumi.export("pgsql_host", self._pgsql.fully_qualified_domain_name)
|
|
201
298
|
|
|
202
299
|
def _create_subnet(
|
|
203
|
-
self,
|
|
300
|
+
self,
|
|
301
|
+
name,
|
|
302
|
+
prefix,
|
|
303
|
+
delegation_service: Optional[str] = None,
|
|
304
|
+
service_endpoints: Sequence[str] = [],
|
|
204
305
|
) -> azure.network.Subnet:
|
|
205
306
|
"""
|
|
206
307
|
Generic method to create a subnet with a delegation.
|
|
@@ -239,7 +340,22 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
239
340
|
sku=sku,
|
|
240
341
|
)
|
|
241
342
|
|
|
242
|
-
def _create_pgadmin_app(self):
|
|
343
|
+
def _create_pgadmin_app(self, access_ip: Optional[Sequence[str]] = None, dns_zone: Optional[azure.network.Zone] = None):
|
|
344
|
+
# Determine the IP restrictions
|
|
345
|
+
ip_restrictions = []
|
|
346
|
+
default_restriction = azure.web.DefaultAction.ALLOW
|
|
347
|
+
if access_ip:
|
|
348
|
+
default_restriction = azure.web.DefaultAction.DENY
|
|
349
|
+
|
|
350
|
+
for ip in access_ip:
|
|
351
|
+
ip_restrictions.append(
|
|
352
|
+
azure.web.IpSecurityRestrictionArgs(
|
|
353
|
+
action="Allow",
|
|
354
|
+
ip_address=ip,
|
|
355
|
+
priority=300,
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
|
|
243
359
|
# The app itself
|
|
244
360
|
app = azure.web.WebApp(
|
|
245
361
|
f"app-pgadmin-{self._name}",
|
|
@@ -255,7 +371,10 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
255
371
|
linux_fx_version="DOCKER|dpage/pgadmin4",
|
|
256
372
|
health_check_path="/misc/ping",
|
|
257
373
|
app_settings=[
|
|
258
|
-
azure.web.NameValuePairArgs(
|
|
374
|
+
azure.web.NameValuePairArgs(
|
|
375
|
+
name="DOCKER_REGISTRY_SERVER_URL",
|
|
376
|
+
value="https://index.docker.io/v1",
|
|
377
|
+
),
|
|
259
378
|
azure.web.NameValuePairArgs(name="DOCKER_ENABLE_CI", value="true"),
|
|
260
379
|
# azure.web.NameValuePairArgs(name="WEBSITE_HTTPLOGGING_RETENTION_DAYS", value="7"),
|
|
261
380
|
# pgAdmin settings
|
|
@@ -263,6 +382,9 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
263
382
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_EMAIL", value="dbadmin@dbadmin.net"),
|
|
264
383
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_PASSWORD", value="dbadmin"),
|
|
265
384
|
],
|
|
385
|
+
# IP restrictions
|
|
386
|
+
ip_security_restrictions_default_action=default_restriction,
|
|
387
|
+
ip_security_restrictions=ip_restrictions,
|
|
266
388
|
),
|
|
267
389
|
)
|
|
268
390
|
|
|
@@ -274,6 +396,50 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
274
396
|
share_name="pgadmin",
|
|
275
397
|
)
|
|
276
398
|
|
|
399
|
+
if dns_zone:
|
|
400
|
+
# Create a DNS record for the pgAdmin app
|
|
401
|
+
cname = azure.network.RecordSet(
|
|
402
|
+
f"dns-cname-pgadmin-{self._name}",
|
|
403
|
+
resource_group_name=self._rg,
|
|
404
|
+
zone_name=dns_zone.name,
|
|
405
|
+
relative_record_set_name="pgadmin",
|
|
406
|
+
record_type="CNAME",
|
|
407
|
+
ttl=3600,
|
|
408
|
+
cname_record=azure.network.CnameRecordArgs(
|
|
409
|
+
cname=app.default_host_name,
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# For the certificate validation to work
|
|
414
|
+
txt_validation = azure.network.RecordSet(
|
|
415
|
+
f"dns-txt-pgadmin-{self._name}",
|
|
416
|
+
resource_group_name=self._rg,
|
|
417
|
+
zone_name=dns_zone.name,
|
|
418
|
+
relative_record_set_name="asuid.pgadmin",
|
|
419
|
+
record_type="TXT",
|
|
420
|
+
ttl=3600,
|
|
421
|
+
txt_records=[
|
|
422
|
+
azure.network.TxtRecordArgs(
|
|
423
|
+
value=[app.custom_domain_verification_id],
|
|
424
|
+
)
|
|
425
|
+
],
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Add custom hostname
|
|
429
|
+
self._add_webapp_host(
|
|
430
|
+
app=app,
|
|
431
|
+
host=dns_zone.name.apply(lambda name: f"pgadmin.{name}"),
|
|
432
|
+
suffix=self._name,
|
|
433
|
+
depends_on=[cname, txt_validation],
|
|
434
|
+
identifier="pgadmin",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Export the custom hostname
|
|
438
|
+
pulumi.export("pgadmin_url", dns_zone.name.apply(lambda name: f"https://pgadmin.{name}"))
|
|
439
|
+
else:
|
|
440
|
+
# Export the default hostname
|
|
441
|
+
pulumi.export("pgadmin_url", app.default_host_name.apply(lambda host: f"https://{host}"))
|
|
442
|
+
|
|
277
443
|
# Mount the storage container
|
|
278
444
|
azure.web.WebAppAzureStorageAccounts(
|
|
279
445
|
f"app-pgadmin-mount-{self._name}",
|
|
@@ -290,9 +456,24 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
290
456
|
},
|
|
291
457
|
)
|
|
292
458
|
|
|
293
|
-
|
|
459
|
+
def _get_existing_web_app_host_name_binding(self, resource_group_name: str, app_name: str, host_name: str):
|
|
460
|
+
try:
|
|
461
|
+
return azure.web.get_web_app_host_name_binding(
|
|
462
|
+
resource_group_name=resource_group_name,
|
|
463
|
+
name=app_name,
|
|
464
|
+
host_name=host_name,
|
|
465
|
+
)
|
|
466
|
+
except Exception:
|
|
467
|
+
return None
|
|
294
468
|
|
|
295
|
-
def _add_webapp_host(
|
|
469
|
+
def _add_webapp_host(
|
|
470
|
+
self,
|
|
471
|
+
app: azure.web.WebApp,
|
|
472
|
+
host: Union[str, pulumi.Input[str]],
|
|
473
|
+
suffix: str,
|
|
474
|
+
identifier: str,
|
|
475
|
+
depends_on: Optional[Sequence[pulumi.Resource]] = None,
|
|
476
|
+
):
|
|
296
477
|
"""
|
|
297
478
|
Because of a circular dependency, we need to create the certificate and the binding in two steps.
|
|
298
479
|
First we create a binding without a certificate,
|
|
@@ -305,48 +486,93 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
305
486
|
:param app: The web app
|
|
306
487
|
:param host: The host name
|
|
307
488
|
:param suffix: A suffix to make the resource name unique
|
|
489
|
+
:param depend_on: The resource to depend on (optional)
|
|
308
490
|
"""
|
|
309
491
|
|
|
310
|
-
|
|
492
|
+
if not depends_on:
|
|
493
|
+
depends_on = []
|
|
311
494
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
resource_group_name=
|
|
316
|
-
|
|
317
|
-
host_name=
|
|
495
|
+
# Retrieve the existing binding (None if it doesn't exist)
|
|
496
|
+
existing_binding = pulumi.Output.all(app.resource_group, app.name, host).apply(
|
|
497
|
+
lambda args: self._get_existing_web_app_host_name_binding(
|
|
498
|
+
resource_group_name=args[0],
|
|
499
|
+
app_name=args[1],
|
|
500
|
+
host_name=args[2],
|
|
318
501
|
)
|
|
502
|
+
)
|
|
319
503
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
504
|
+
# Create an inline function that we will invoke through the Output.apply lambda
|
|
505
|
+
def _create_binding_with_cert(existing_binding):
|
|
506
|
+
if existing_binding:
|
|
507
|
+
# Create a managed certificate
|
|
508
|
+
# This will work because the binding exists actually
|
|
509
|
+
certificate = azure.web.Certificate(
|
|
510
|
+
f"cert-{suffix}-{identifier}",
|
|
511
|
+
resource_group_name=self._rg,
|
|
512
|
+
server_farm_id=app.server_farm_id,
|
|
513
|
+
canonical_name=host,
|
|
514
|
+
host_names=[host],
|
|
515
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
516
|
+
)
|
|
329
517
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
518
|
+
# Create a new binding, replacing the old one,
|
|
519
|
+
# with the certificate
|
|
520
|
+
azure.web.WebAppHostNameBinding(
|
|
521
|
+
f"host-binding-{suffix}-{identifier}",
|
|
522
|
+
resource_group_name=self._rg,
|
|
523
|
+
name=app.name,
|
|
524
|
+
host_name=host,
|
|
525
|
+
ssl_state=azure.web.SslState.SNI_ENABLED,
|
|
526
|
+
thumbprint=certificate.thumbprint,
|
|
527
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
else:
|
|
531
|
+
# Create a binding without a certificate
|
|
532
|
+
azure.web.WebAppHostNameBinding(
|
|
533
|
+
f"host-binding-{suffix}-{identifier}",
|
|
534
|
+
resource_group_name=self._rg,
|
|
535
|
+
name=app.name,
|
|
536
|
+
host_name=host,
|
|
537
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
existing_binding.apply(_create_binding_with_cert)
|
|
541
|
+
|
|
542
|
+
def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.network.RecordSet]:
|
|
543
|
+
created_records = []
|
|
544
|
+
|
|
545
|
+
# Domain validation and SPF record (one TXT record with multiple values)
|
|
546
|
+
r = azure.network.RecordSet(
|
|
547
|
+
f"dns-comms-{suffix}-{host.identifier}-domain",
|
|
548
|
+
resource_group_name=self._rg,
|
|
549
|
+
zone_name=host.zone.name,
|
|
550
|
+
relative_record_set_name="@",
|
|
551
|
+
record_type="TXT",
|
|
552
|
+
ttl=3600,
|
|
553
|
+
txt_records=[
|
|
554
|
+
azure.network.TxtRecordArgs(value=[records["domain"]["value"]]),
|
|
555
|
+
azure.network.TxtRecordArgs(value=[records["s_pf"]["value"]]),
|
|
556
|
+
],
|
|
557
|
+
)
|
|
558
|
+
created_records.append(r)
|
|
559
|
+
|
|
560
|
+
# DKIM records (two CNAME records)
|
|
561
|
+
for record in ("d_kim", "d_kim2"):
|
|
562
|
+
r = azure.network.RecordSet(
|
|
563
|
+
f"dns-comms-{suffix}-{host.identifier}-{record}",
|
|
344
564
|
resource_group_name=self._rg,
|
|
345
|
-
|
|
346
|
-
|
|
565
|
+
zone_name=host.zone.name,
|
|
566
|
+
relative_record_set_name=records[record]["name"],
|
|
567
|
+
record_type="CNAME",
|
|
568
|
+
ttl=records[record]["ttl"],
|
|
569
|
+
cname_record=azure.network.CnameRecordArgs(cname=records[record]["value"]),
|
|
347
570
|
)
|
|
571
|
+
created_records.append(r)
|
|
572
|
+
|
|
573
|
+
return created_records
|
|
348
574
|
|
|
349
|
-
def _add_webapp_comms(self, data_location: str, domains: list[
|
|
575
|
+
def _add_webapp_comms(self, data_location: str, domains: list[HostDefinition], suffix: str) -> azure.communication.CommunicationService:
|
|
350
576
|
email_service = azure.communication.EmailService(
|
|
351
577
|
f"comms-email-{suffix}",
|
|
352
578
|
resource_group_name=self._rg,
|
|
@@ -355,30 +581,37 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
355
581
|
)
|
|
356
582
|
|
|
357
583
|
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
|
|
584
|
+
comm_dependencies = []
|
|
585
|
+
|
|
586
|
+
# Add our own custom domains
|
|
587
|
+
for domain in domains:
|
|
373
588
|
d = azure.communication.Domain(
|
|
374
|
-
f"comms-email-domain-{suffix}-
|
|
589
|
+
f"comms-email-domain-{suffix}-{domain.identifier}",
|
|
375
590
|
resource_group_name=self._rg,
|
|
376
591
|
location="global",
|
|
377
|
-
domain_management=azure.communication.DomainManagement.
|
|
378
|
-
domain_name=
|
|
592
|
+
domain_management=azure.communication.DomainManagement.CUSTOMER_MANAGED,
|
|
593
|
+
domain_name=domain.full_host,
|
|
379
594
|
email_service_name=email_service.name,
|
|
380
595
|
)
|
|
381
|
-
|
|
596
|
+
|
|
597
|
+
if domain.zone:
|
|
598
|
+
# Create DNS records in the managed zone
|
|
599
|
+
comm_dependencies = pulumi.Output.all(suffix, domain, d.verification_records).apply(
|
|
600
|
+
lambda args: self._create_comms_dns_records(suffix=args[0], host=args[1], records=args[2])
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
domain_resources.append(d.id)
|
|
604
|
+
|
|
605
|
+
# Add an Azure managed domain
|
|
606
|
+
d = azure.communication.Domain(
|
|
607
|
+
f"comms-email-domain-{suffix}-azure",
|
|
608
|
+
resource_group_name=self._rg,
|
|
609
|
+
location="global",
|
|
610
|
+
domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
|
|
611
|
+
domain_name="AzureManagedDomain",
|
|
612
|
+
email_service_name=email_service.name,
|
|
613
|
+
)
|
|
614
|
+
domain_resources.append(d.id)
|
|
382
615
|
|
|
383
616
|
# Create Communication Services and link the domains
|
|
384
617
|
comm_service = azure.communication.CommunicationService(
|
|
@@ -387,6 +620,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
387
620
|
location="global",
|
|
388
621
|
data_location=data_location,
|
|
389
622
|
linked_domains=domain_resources,
|
|
623
|
+
opts=pulumi.ResourceOptions(depends_on=comm_dependencies),
|
|
390
624
|
)
|
|
391
625
|
|
|
392
626
|
return comm_service
|
|
@@ -415,27 +649,35 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
415
649
|
),
|
|
416
650
|
)
|
|
417
651
|
|
|
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
652
|
# Add vault administrators
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
653
|
+
if administrators:
|
|
654
|
+
# Find the Key Vault Administrator role
|
|
655
|
+
administrator_role = vault.id.apply(
|
|
656
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
657
|
+
role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
|
|
658
|
+
scope=scope,
|
|
659
|
+
)
|
|
434
660
|
)
|
|
435
661
|
|
|
662
|
+
# Actual administrator roles
|
|
663
|
+
for a in administrators:
|
|
664
|
+
azure.authorization.RoleAssignment(
|
|
665
|
+
f"ra-{suffix}-vault-admin-{a}",
|
|
666
|
+
principal_id=a,
|
|
667
|
+
principal_type=azure.authorization.PrincipalType.USER,
|
|
668
|
+
role_definition_id=administrator_role.id,
|
|
669
|
+
scope=vault.id,
|
|
670
|
+
)
|
|
671
|
+
|
|
436
672
|
return vault
|
|
437
673
|
|
|
438
|
-
def _add_webapp_secret(
|
|
674
|
+
def _add_webapp_secret(
|
|
675
|
+
self,
|
|
676
|
+
vault: azure.keyvault.Vault,
|
|
677
|
+
secret_name: str,
|
|
678
|
+
config_secret_name: str,
|
|
679
|
+
suffix: str,
|
|
680
|
+
):
|
|
439
681
|
secret = self._config.require_secret(config_secret_name)
|
|
440
682
|
|
|
441
683
|
# Normalize the secret name
|
|
@@ -495,13 +737,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
495
737
|
db_name: str,
|
|
496
738
|
repository_url: str,
|
|
497
739
|
repository_branch: str,
|
|
498
|
-
website_hosts: list[
|
|
740
|
+
website_hosts: list[HostDefinition],
|
|
499
741
|
django_settings_module: str,
|
|
500
|
-
environment_variables: dict[str, str] =
|
|
501
|
-
secrets: dict[str, str] =
|
|
742
|
+
environment_variables: Optional[dict[str, str]] = None,
|
|
743
|
+
secrets: Optional[dict[str, str]] = None,
|
|
502
744
|
comms_data_location: Optional[str] = None,
|
|
503
|
-
comms_domains: Optional[list[
|
|
504
|
-
vault_administrators: Optional[list[str]] =
|
|
745
|
+
comms_domains: Optional[list[HostDefinition]] = None,
|
|
746
|
+
vault_administrators: Optional[list[str]] = None,
|
|
505
747
|
) -> azure.web.WebApp:
|
|
506
748
|
"""
|
|
507
749
|
Create a Django website with it's own database and storage containers.
|
|
@@ -549,7 +791,11 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
549
791
|
|
|
550
792
|
# Communication Services (optional)
|
|
551
793
|
if comms_data_location:
|
|
794
|
+
if not comms_domains:
|
|
795
|
+
comms_domains = []
|
|
796
|
+
|
|
552
797
|
comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
|
|
798
|
+
|
|
553
799
|
# Add the service endpoint as environment variable
|
|
554
800
|
environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
|
|
555
801
|
else:
|
|
@@ -575,6 +821,8 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
575
821
|
for key, value in environment_variables.items()
|
|
576
822
|
]
|
|
577
823
|
|
|
824
|
+
allowed_hosts = pulumi.Output.concat(*[pulumi.Output.concat(host.full_host, ",") for host in website_hosts])
|
|
825
|
+
|
|
578
826
|
app = azure.web.WebApp(
|
|
579
827
|
f"app-{name}-{self._name}",
|
|
580
828
|
resource_group_name=self._rg,
|
|
@@ -603,12 +851,18 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
603
851
|
# azure.web.NameValuePairArgs(name="DEBUG", value="true"),
|
|
604
852
|
azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
|
|
605
853
|
azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
|
|
606
|
-
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=
|
|
854
|
+
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=allowed_hosts),
|
|
607
855
|
# Vault settings
|
|
608
856
|
azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
|
|
609
857
|
# Storage settings
|
|
610
|
-
azure.web.NameValuePairArgs(
|
|
611
|
-
|
|
858
|
+
azure.web.NameValuePairArgs(
|
|
859
|
+
name="AZURE_STORAGE_ACCOUNT_NAME",
|
|
860
|
+
value=self._storage_account.name,
|
|
861
|
+
),
|
|
862
|
+
azure.web.NameValuePairArgs(
|
|
863
|
+
name="AZURE_STORAGE_CONTAINER_STATICFILES",
|
|
864
|
+
value=static_container.name,
|
|
865
|
+
),
|
|
612
866
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_MEDIA", value=media_container.name),
|
|
613
867
|
# CDN
|
|
614
868
|
azure.web.NameValuePairArgs(name="CDN_HOST", value=self._cdn_host),
|
|
@@ -631,9 +885,85 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
631
885
|
# We need this to verify custom domains
|
|
632
886
|
pulumi.export(f"{name}_site_domain_verification_id", app.custom_domain_verification_id)
|
|
633
887
|
pulumi.export(f"{name}_site_domain_cname", app.default_host_name)
|
|
888
|
+
virtual_ip = app.outbound_ip_addresses.apply(lambda addresses: addresses.split(",")[-1])
|
|
889
|
+
pulumi.export(f"{name}_site_virtual_ip", virtual_ip)
|
|
890
|
+
|
|
891
|
+
# Get the URL of the publish profile.
|
|
892
|
+
# Use app.identity here too to ensure the app is actually created before getting credentials.
|
|
893
|
+
credentials = pulumi.Output.all(self._rg, app.name, app.identity).apply(
|
|
894
|
+
lambda args: azure.web.list_web_app_publishing_credentials(
|
|
895
|
+
resource_group_name=args[0],
|
|
896
|
+
name=args[1],
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
pulumi.export(f"{name}_deploy_url", pulumi.Output.concat(credentials.scm_uri, "/deploy"))
|
|
634
901
|
|
|
635
902
|
for host in website_hosts:
|
|
636
|
-
|
|
903
|
+
dependencies = []
|
|
904
|
+
|
|
905
|
+
if host.zone:
|
|
906
|
+
# Create a DNS record in the zone
|
|
907
|
+
|
|
908
|
+
if host.host == "@":
|
|
909
|
+
# Create a A record for the virtual IP address
|
|
910
|
+
a = azure.network.RecordSet(
|
|
911
|
+
f"dns-a-{name}-{self._name}-{host.identifier}",
|
|
912
|
+
resource_group_name=self._rg,
|
|
913
|
+
zone_name=host.zone.name,
|
|
914
|
+
relative_record_set_name=host.host,
|
|
915
|
+
record_type="A",
|
|
916
|
+
ttl=3600,
|
|
917
|
+
a_records=[
|
|
918
|
+
azure.network.ARecordArgs(
|
|
919
|
+
ipv4_address=virtual_ip,
|
|
920
|
+
)
|
|
921
|
+
],
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
dependencies.append(a)
|
|
925
|
+
else:
|
|
926
|
+
# Create a CNAME record for the custom hostname
|
|
927
|
+
cname = azure.network.RecordSet(
|
|
928
|
+
f"dns-cname-{name}-{self._name}-{host.identifier}",
|
|
929
|
+
resource_group_name=self._rg,
|
|
930
|
+
zone_name=host.zone.name,
|
|
931
|
+
relative_record_set_name=host.host,
|
|
932
|
+
record_type="CNAME",
|
|
933
|
+
ttl=3600,
|
|
934
|
+
cname_record=azure.network.CnameRecordArgs(
|
|
935
|
+
cname=app.default_host_name,
|
|
936
|
+
),
|
|
937
|
+
)
|
|
938
|
+
dependencies.append(cname)
|
|
939
|
+
|
|
940
|
+
# For the certificate validation to work
|
|
941
|
+
relative_record_set_name = "asuid" if host.host == "@" else pulumi.Output.concat("asuid.", host.host)
|
|
942
|
+
|
|
943
|
+
txt_validation = azure.network.RecordSet(
|
|
944
|
+
f"dns-txt-{name}-{self._name}-{host.identifier}",
|
|
945
|
+
resource_group_name=self._rg,
|
|
946
|
+
zone_name=host.zone.name,
|
|
947
|
+
relative_record_set_name=relative_record_set_name,
|
|
948
|
+
record_type="TXT",
|
|
949
|
+
ttl=3600,
|
|
950
|
+
txt_records=[
|
|
951
|
+
azure.network.TxtRecordArgs(
|
|
952
|
+
value=[app.custom_domain_verification_id],
|
|
953
|
+
)
|
|
954
|
+
],
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
dependencies.append(txt_validation)
|
|
958
|
+
|
|
959
|
+
# Add the host with optional dependencies
|
|
960
|
+
self._add_webapp_host(
|
|
961
|
+
app=app,
|
|
962
|
+
host=host.full_host,
|
|
963
|
+
suffix=f"{name}-{self._name}",
|
|
964
|
+
identifier=host.identifier,
|
|
965
|
+
depends_on=dependencies,
|
|
966
|
+
)
|
|
637
967
|
|
|
638
968
|
# To enable deployment from GitLab
|
|
639
969
|
azure.web.WebAppSourceControl(
|
|
@@ -649,7 +979,8 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
649
979
|
|
|
650
980
|
# Where we can retrieve the SSH key
|
|
651
981
|
pulumi.export(
|
|
652
|
-
f"{name}_deploy_ssh_key_url",
|
|
982
|
+
f"{name}_deploy_ssh_key_url",
|
|
983
|
+
app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1"),
|
|
653
984
|
)
|
|
654
985
|
|
|
655
986
|
# Find the role for Key Vault Secrets User
|
|
@@ -721,28 +1052,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
721
1052
|
scope=self._cdn_endpoint.id,
|
|
722
1053
|
)
|
|
723
1054
|
|
|
724
|
-
|
|
725
|
-
if website_hosts:
|
|
726
|
-
origins = [f"https://{host}" for host in website_hosts]
|
|
727
|
-
else:
|
|
728
|
-
origins = ["*"]
|
|
1055
|
+
return app
|
|
729
1056
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
account_name=self._storage_account.name,
|
|
734
|
-
blob_services_name="default",
|
|
735
|
-
cors=azure.storage.CorsRulesArgs(
|
|
736
|
-
cors_rules=[
|
|
737
|
-
azure.storage.CorsRuleArgs(
|
|
738
|
-
allowed_headers=["*"],
|
|
739
|
-
allowed_methods=["GET", "OPTIONS", "HEAD"],
|
|
740
|
-
allowed_origins=origins,
|
|
741
|
-
exposed_headers=["Access-Control-Allow-Origin"],
|
|
742
|
-
max_age_in_seconds=86400,
|
|
743
|
-
)
|
|
744
|
-
]
|
|
745
|
-
),
|
|
746
|
-
)
|
|
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.
|
|
747
1060
|
|
|
748
|
-
|
|
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])
|
|
@@ -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.15
|
|
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
|
|
@@ -33,9 +33,9 @@ Classifier: Programming Language :: Python :: 3
|
|
|
33
33
|
Requires-Python: >=3.9
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
|
-
Requires-Dist: pulumi
|
|
37
|
-
Requires-Dist: pulumi-azure-native
|
|
38
|
-
Requires-Dist: pulumi-random
|
|
36
|
+
Requires-Dist: pulumi>=3.99.0
|
|
37
|
+
Requires-Dist: pulumi-azure-native>=2.24.0
|
|
38
|
+
Requires-Dist: pulumi-random>=4.14.0
|
|
39
39
|
|
|
40
40
|
# Pulumi Django Deployment
|
|
41
41
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pulumi_django_azure/__init__.py,sha256=5RY9reSVNw-HULrOXfhcq3cyPne-94ojFmeV1m6kIVg,79
|
|
2
|
+
pulumi_django_azure/django_deployment.py,sha256=dTbzASxVeDSLzTx3MN5UcbZTVhtZBbsaplF-mrkOvoc,44421
|
|
3
|
+
pulumi_django_azure-1.0.15.dist-info/LICENSE,sha256=tlZQiilfsHDYlvhWMA5PvDV2FxpaCQbE9aapcygnhEQ,1088
|
|
4
|
+
pulumi_django_azure-1.0.15.dist-info/METADATA,sha256=FUubwm450nWLBkxdvinFLDu-jTH8G8POYpcDq_WcD3g,13954
|
|
5
|
+
pulumi_django_azure-1.0.15.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
6
|
+
pulumi_django_azure-1.0.15.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
|
|
7
|
+
pulumi_django_azure-1.0.15.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pulumi_django_azure/__init__.py,sha256=tXTvPfr8-Nll5cjMyY9yj_0z_PQ0XAcxihzHRCES-hU,63
|
|
2
|
-
pulumi_django_azure/django_deployment.py,sha256=MBdNH2iEOoNyWY5mxiLy8iUkW514LsG-wNjo4MVZlqY,31479
|
|
3
|
-
pulumi_django_azure-1.0.12.dist-info/LICENSE,sha256=NX2LN3U319Zaac8b7ZgfNOco_nTBbN531X_M_13niSg,1087
|
|
4
|
-
pulumi_django_azure-1.0.12.dist-info/METADATA,sha256=hPKNy_NeqAhvQGJFn-4Zw8AUiWU6pM0jpS9ypjzX2as,13957
|
|
5
|
-
pulumi_django_azure-1.0.12.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
6
|
-
pulumi_django_azure-1.0.12.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
|
|
7
|
-
pulumi_django_azure-1.0.12.dist-info/RECORD,,
|
|
File without changes
|