pulumi-django-azure 1.0.11__tar.gz → 1.0.17__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.11/src/pulumi_django_azure.egg-info → pulumi_django_azure-1.0.17}/PKG-INFO +83 -6
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/README.md +78 -1
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/pyproject.toml +29 -12
- pulumi_django_azure-1.0.17/src/pulumi_django_azure/__init__.py +1 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/src/pulumi_django_azure/django_deployment.py +458 -128
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17/src/pulumi_django_azure.egg-info}/PKG-INFO +83 -6
- pulumi_django_azure-1.0.17/src/pulumi_django_azure.egg-info/requires.txt +3 -0
- pulumi_django_azure-1.0.11/src/pulumi_django_azure/__init__.py +0 -1
- pulumi_django_azure-1.0.11/src/pulumi_django_azure.egg-info/requires.txt +0 -3
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/LICENSE +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/setup.cfg +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/src/pulumi_django_azure.egg-info/SOURCES.txt +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/src/pulumi_django_azure.egg-info/dependency_links.txt +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17}/src/pulumi_django_azure.egg-info/top_level.txt +0 -0
{pulumi_django_azure-1.0.11/src/pulumi_django_azure.egg-info → pulumi_django_azure-1.0.17}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pulumi-django-azure
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.17
|
|
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>=3.
|
|
37
|
-
Requires-Dist: pulumi-azure-native>=2.
|
|
38
|
-
Requires-Dist: pulumi-random>=4.
|
|
36
|
+
Requires-Dist: pulumi>=3.146.0
|
|
37
|
+
Requires-Dist: pulumi-azure-native>=2.82.0
|
|
38
|
+
Requires-Dist: pulumi-random>=4.17.0
|
|
39
39
|
|
|
40
40
|
# Pulumi Django Deployment
|
|
41
41
|
|
|
@@ -59,9 +59,10 @@ Your Django project should contain a folder `cicd` with these files:
|
|
|
59
59
|
```bash
|
|
60
60
|
python manage.py migrate
|
|
61
61
|
python manage.py collectstatic --noinput
|
|
62
|
+
python manage.py purge_cdn
|
|
62
63
|
gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
|
|
63
64
|
```
|
|
64
|
-
Be sure to change `yourapplication` in the above.
|
|
65
|
+
Be sure to change `yourapplication` in the above. To see the `purge_cdn` management command we use here, see below.
|
|
65
66
|
## Installation
|
|
66
67
|
This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
|
|
67
68
|
|
|
@@ -256,6 +257,82 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
|
|
|
256
257
|
|
|
257
258
|
This would then trigger a redeploy everytime you make a commit to your live branch.
|
|
258
259
|
|
|
260
|
+
## CDN Purging
|
|
261
|
+
We added a management command to Django to purge the CDN cache, and added that to the startup script. Our version is here:
|
|
262
|
+
```python
|
|
263
|
+
import os
|
|
264
|
+
|
|
265
|
+
from azure.mgmt.cdn import CdnManagementClient
|
|
266
|
+
from azure.mgmt.cdn.models import PurgeParameters
|
|
267
|
+
from django.core.management.base import BaseCommand
|
|
268
|
+
|
|
269
|
+
from core.azure_helper import AZURE_CREDENTIAL, get_subscription_id
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class Command(BaseCommand):
|
|
273
|
+
help = "Purges the CDN endpoint"
|
|
274
|
+
|
|
275
|
+
def add_arguments(self, parser):
|
|
276
|
+
parser.add_argument(
|
|
277
|
+
"--wait",
|
|
278
|
+
action="store_true",
|
|
279
|
+
help="Wait for the purge operation to complete",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def handle(self, *args, **options):
|
|
283
|
+
# Read environment variables
|
|
284
|
+
resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
|
|
285
|
+
profile_name = os.getenv("CDN_PROFILE")
|
|
286
|
+
endpoint_name = os.getenv("CDN_ENDPOINT")
|
|
287
|
+
content_paths = ["/*"]
|
|
288
|
+
|
|
289
|
+
# Ensure all required environment variables are set
|
|
290
|
+
if not all([resource_group, profile_name, endpoint_name]):
|
|
291
|
+
self.stderr.write(self.style.ERROR("Missing required environment variables."))
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
# Authenticate with Azure
|
|
295
|
+
cdn_client = CdnManagementClient(AZURE_CREDENTIAL, get_subscription_id())
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
# Purge the CDN endpoint
|
|
299
|
+
purge_operation = cdn_client.endpoints.begin_purge_content(
|
|
300
|
+
resource_group_name=resource_group,
|
|
301
|
+
profile_name=profile_name,
|
|
302
|
+
endpoint_name=endpoint_name,
|
|
303
|
+
content_file_paths=PurgeParameters(content_paths=content_paths),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Check if the --wait argument was provided
|
|
307
|
+
if options["wait"]:
|
|
308
|
+
purge_operation.result() # Wait for the operation to complete
|
|
309
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
|
|
310
|
+
else:
|
|
311
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
And our azure_helper:
|
|
318
|
+
```python
|
|
319
|
+
from azure.identity import DefaultAzureCredential
|
|
320
|
+
from azure.mgmt.resource import SubscriptionClient
|
|
321
|
+
|
|
322
|
+
# Azure credentials
|
|
323
|
+
AZURE_CREDENTIAL = DefaultAzureCredential()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_db_password() -> str:
|
|
327
|
+
return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def get_subscription_id() -> str:
|
|
331
|
+
subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
|
|
332
|
+
subscriptions = list(subscription_client.subscriptions.list())
|
|
333
|
+
return subscriptions[0].subscription_id
|
|
334
|
+
```
|
|
335
|
+
|
|
259
336
|
## Change requests
|
|
260
337
|
I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
|
|
261
338
|
Therefore this project is not super generic, but tailored to my needs. I am however open to pull or change requests to improve this project or to make it more usable for others.
|
|
@@ -20,9 +20,10 @@ Your Django project should contain a folder `cicd` with these files:
|
|
|
20
20
|
```bash
|
|
21
21
|
python manage.py migrate
|
|
22
22
|
python manage.py collectstatic --noinput
|
|
23
|
+
python manage.py purge_cdn
|
|
23
24
|
gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
|
|
24
25
|
```
|
|
25
|
-
Be sure to change `yourapplication` in the above.
|
|
26
|
+
Be sure to change `yourapplication` in the above. To see the `purge_cdn` management command we use here, see below.
|
|
26
27
|
## Installation
|
|
27
28
|
This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
|
|
28
29
|
|
|
@@ -217,6 +218,82 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
|
|
|
217
218
|
|
|
218
219
|
This would then trigger a redeploy everytime you make a commit to your live branch.
|
|
219
220
|
|
|
221
|
+
## CDN Purging
|
|
222
|
+
We added a management command to Django to purge the CDN cache, and added that to the startup script. Our version is here:
|
|
223
|
+
```python
|
|
224
|
+
import os
|
|
225
|
+
|
|
226
|
+
from azure.mgmt.cdn import CdnManagementClient
|
|
227
|
+
from azure.mgmt.cdn.models import PurgeParameters
|
|
228
|
+
from django.core.management.base import BaseCommand
|
|
229
|
+
|
|
230
|
+
from core.azure_helper import AZURE_CREDENTIAL, get_subscription_id
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Command(BaseCommand):
|
|
234
|
+
help = "Purges the CDN endpoint"
|
|
235
|
+
|
|
236
|
+
def add_arguments(self, parser):
|
|
237
|
+
parser.add_argument(
|
|
238
|
+
"--wait",
|
|
239
|
+
action="store_true",
|
|
240
|
+
help="Wait for the purge operation to complete",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def handle(self, *args, **options):
|
|
244
|
+
# Read environment variables
|
|
245
|
+
resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
|
|
246
|
+
profile_name = os.getenv("CDN_PROFILE")
|
|
247
|
+
endpoint_name = os.getenv("CDN_ENDPOINT")
|
|
248
|
+
content_paths = ["/*"]
|
|
249
|
+
|
|
250
|
+
# Ensure all required environment variables are set
|
|
251
|
+
if not all([resource_group, profile_name, endpoint_name]):
|
|
252
|
+
self.stderr.write(self.style.ERROR("Missing required environment variables."))
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Authenticate with Azure
|
|
256
|
+
cdn_client = CdnManagementClient(AZURE_CREDENTIAL, get_subscription_id())
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
# Purge the CDN endpoint
|
|
260
|
+
purge_operation = cdn_client.endpoints.begin_purge_content(
|
|
261
|
+
resource_group_name=resource_group,
|
|
262
|
+
profile_name=profile_name,
|
|
263
|
+
endpoint_name=endpoint_name,
|
|
264
|
+
content_file_paths=PurgeParameters(content_paths=content_paths),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Check if the --wait argument was provided
|
|
268
|
+
if options["wait"]:
|
|
269
|
+
purge_operation.result() # Wait for the operation to complete
|
|
270
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
|
|
271
|
+
else:
|
|
272
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
And our azure_helper:
|
|
279
|
+
```python
|
|
280
|
+
from azure.identity import DefaultAzureCredential
|
|
281
|
+
from azure.mgmt.resource import SubscriptionClient
|
|
282
|
+
|
|
283
|
+
# Azure credentials
|
|
284
|
+
AZURE_CREDENTIAL = DefaultAzureCredential()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_db_password() -> str:
|
|
288
|
+
return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_subscription_id() -> str:
|
|
292
|
+
subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
|
|
293
|
+
subscriptions = list(subscription_client.subscriptions.list())
|
|
294
|
+
return subscriptions[0].subscription_id
|
|
295
|
+
```
|
|
296
|
+
|
|
220
297
|
## Change requests
|
|
221
298
|
I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
|
|
222
299
|
Therefore this project is not super generic, but tailored to my needs. I am however open to pull or change requests to improve this project or to make it more usable for others.
|
|
@@ -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.17"
|
|
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" }]
|
|
@@ -16,9 +16,9 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
keywords = ["django", "pulumi", "azure"]
|
|
18
18
|
dependencies = [
|
|
19
|
-
"pulumi >=
|
|
20
|
-
"pulumi-azure-native >=
|
|
21
|
-
"pulumi-random >=
|
|
19
|
+
"pulumi (>=3.146.0)",
|
|
20
|
+
"pulumi-azure-native (>=2.82.0)",
|
|
21
|
+
"pulumi-random (>=4.17.0)",
|
|
22
22
|
]
|
|
23
23
|
requires-python = ">=3.9"
|
|
24
24
|
|
|
@@ -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.17"
|
|
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 = "
|
|
37
|
-
pulumi = "
|
|
38
|
-
pulumi-random = "
|
|
36
|
+
pulumi-azure-native = ">=2.82.0"
|
|
37
|
+
pulumi = ">=3.146.0"
|
|
38
|
+
pulumi-random = ">=4.17.0"
|
|
39
39
|
|
|
40
40
|
[tool.poetry.group.dev.dependencies]
|
|
41
|
-
twine = "^
|
|
42
|
-
build = "^1.
|
|
41
|
+
twine = "^6.0.1"
|
|
42
|
+
build = "^1.2.2.post1"
|
|
43
|
+
ruff = "^0.9.2"
|
|
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,59 @@
|
|
|
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
|
+
def __eq__(self, other):
|
|
24
|
+
return self.host == other.host and self.zone.name == other.zone.name
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def identifier(self) -> str:
|
|
28
|
+
"""
|
|
29
|
+
The identifier for this host definition.
|
|
30
|
+
|
|
31
|
+
:return: The identifier
|
|
32
|
+
"""
|
|
33
|
+
if not self._identifier:
|
|
34
|
+
if self.zone:
|
|
35
|
+
raise ValueError(f"An identifier is required for the HostDefinition with host '{self.host}' ensure uniqueness.")
|
|
36
|
+
else:
|
|
37
|
+
# Use the host name as the identifier
|
|
38
|
+
return self.host.replace(".", "-")
|
|
39
|
+
else:
|
|
40
|
+
return self._identifier
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def full_host(self) -> pulumi.Output[str]:
|
|
44
|
+
"""
|
|
45
|
+
The full host name, including the zone.
|
|
46
|
+
|
|
47
|
+
:return: The full host name
|
|
48
|
+
"""
|
|
49
|
+
if not self.zone:
|
|
50
|
+
return pulumi.Output.concat(self.host)
|
|
51
|
+
elif self.host == "@":
|
|
52
|
+
return self.zone.name
|
|
53
|
+
else:
|
|
54
|
+
return pulumi.Output.concat(self.host, ".", self.zone.name)
|
|
55
|
+
|
|
56
|
+
|
|
8
57
|
class DjangoDeployment(pulumi.ComponentResource):
|
|
9
58
|
HEALTH_CHECK_PATH = "/health-check"
|
|
10
59
|
|
|
@@ -16,10 +65,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
16
65
|
vnet: azure.network.VirtualNetwork,
|
|
17
66
|
pgsql_sku: azure.dbforpostgresql.SkuArgs,
|
|
18
67
|
pgsql_ip_prefix: str,
|
|
19
|
-
|
|
68
|
+
app_service_ip_prefix: str,
|
|
20
69
|
app_service_sku: azure.web.SkuDescriptionArgs,
|
|
21
70
|
storage_account_name: str,
|
|
22
|
-
|
|
71
|
+
storage_allowed_origins: Optional[Sequence[str]] = None,
|
|
72
|
+
pgadmin_access_ip: Optional[Sequence[str]] = None,
|
|
73
|
+
pgadmin_dns_zone: Optional[azure.network.Zone] = None,
|
|
74
|
+
cdn_host: Optional[HostDefinition] = None,
|
|
23
75
|
opts=None,
|
|
24
76
|
):
|
|
25
77
|
"""
|
|
@@ -31,10 +83,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
31
83
|
:param vnet: The virtual network to create the subnets in.
|
|
32
84
|
:param pgsql_sku: The SKU for the PostgreSQL server.
|
|
33
85
|
:param pgsql_ip_prefix: The IP prefix for the PostgreSQL subnet.
|
|
34
|
-
:param
|
|
86
|
+
:param app_service_ip_prefix: The IP prefix for the app service subnet.
|
|
35
87
|
:param app_service_sku: The SKU for the app service plan.
|
|
36
88
|
:param storage_account_name: The name of the storage account. Should be unique across Azure.
|
|
37
|
-
:param
|
|
89
|
+
:param storage_allowed_origins: The origins (hosts) to allow access through CORS policy. You can specify '*' to allow all.
|
|
90
|
+
:param pgadmin_access_ip: The IP addresses to allow access to pgAdmin. If empty, all IP addresses are allowed.
|
|
91
|
+
:param pgadmin_dns_zone: The Azure DNS zone to a pgadmin DNS record in. (optional)
|
|
92
|
+
:param cdn_host: A custom CDN host name. (optional)
|
|
38
93
|
:param opts: The resource options
|
|
39
94
|
"""
|
|
40
95
|
|
|
@@ -49,7 +104,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
49
104
|
self._vnet = vnet
|
|
50
105
|
|
|
51
106
|
# Storage resources
|
|
52
|
-
self._create_storage(account_name=storage_account_name)
|
|
107
|
+
self._create_storage(account_name=storage_account_name, allowed_origins=storage_allowed_origins)
|
|
53
108
|
self._cdn_host = self._create_cdn(custom_host=cdn_host)
|
|
54
109
|
|
|
55
110
|
# PostgreSQL resources
|
|
@@ -58,7 +113,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
58
113
|
# Subnet for the apps
|
|
59
114
|
self._app_subnet = self._create_subnet(
|
|
60
115
|
name="app-service",
|
|
61
|
-
prefix=
|
|
116
|
+
prefix=app_service_ip_prefix,
|
|
62
117
|
delegation_service="Microsoft.Web/serverFarms",
|
|
63
118
|
service_endpoints=["Microsoft.Storage"],
|
|
64
119
|
)
|
|
@@ -67,9 +122,9 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
67
122
|
self._app_service_plan = self._create_app_service_plan(sku=app_service_sku)
|
|
68
123
|
|
|
69
124
|
# Create a pgAdmin app
|
|
70
|
-
self._create_pgadmin_app()
|
|
125
|
+
self._create_pgadmin_app(access_ip=pgadmin_access_ip, dns_zone=pgadmin_dns_zone)
|
|
71
126
|
|
|
72
|
-
def _create_storage(self, account_name: str):
|
|
127
|
+
def _create_storage(self, account_name: str, allowed_origins: Optional[Sequence[str]] = None):
|
|
73
128
|
# Create blob storage
|
|
74
129
|
self._storage_account = azure.storage.StorageAccount(
|
|
75
130
|
f"sa-{self._name}",
|
|
@@ -85,7 +140,26 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
85
140
|
enable_https_traffic_only=True,
|
|
86
141
|
)
|
|
87
142
|
|
|
88
|
-
|
|
143
|
+
if allowed_origins:
|
|
144
|
+
azure.storage.BlobServiceProperties(
|
|
145
|
+
f"sa-{self._name}-blob-properties",
|
|
146
|
+
resource_group_name=self._rg,
|
|
147
|
+
account_name=self._storage_account.name,
|
|
148
|
+
blob_services_name="default",
|
|
149
|
+
cors=azure.storage.CorsRulesArgs(
|
|
150
|
+
cors_rules=[
|
|
151
|
+
azure.storage.CorsRuleArgs(
|
|
152
|
+
allowed_headers=["*"],
|
|
153
|
+
allowed_methods=["GET", "OPTIONS", "HEAD"],
|
|
154
|
+
allowed_origins=allowed_origins,
|
|
155
|
+
exposed_headers=["Access-Control-Allow-Origin"],
|
|
156
|
+
max_age_in_seconds=86400,
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _create_cdn(self, custom_host: Optional[HostDefinition]) -> pulumi.Output[str]:
|
|
89
163
|
"""
|
|
90
164
|
Create a CDN endpoint. If a host name is given, it will be used as the custom domain.
|
|
91
165
|
Otherwise, the default CDN host name will be returned.
|
|
@@ -128,22 +202,48 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
128
202
|
profile_name=self._cdn_profile.name,
|
|
129
203
|
origin_host_header=endpoint_origin,
|
|
130
204
|
origins=[azure.cdn.DeepCreatedOriginArgs(name="origin-storage", host_name=endpoint_origin)],
|
|
131
|
-
query_string_caching_behavior=azure.cdn.QueryStringCachingBehavior.
|
|
205
|
+
query_string_caching_behavior=azure.cdn.QueryStringCachingBehavior.USE_QUERY_STRING,
|
|
132
206
|
)
|
|
133
207
|
|
|
134
208
|
pulumi.export("cdn_cname", self._cdn_endpoint.host_name)
|
|
135
209
|
|
|
136
210
|
# Add custom domain if given
|
|
137
211
|
if custom_host:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
212
|
+
if custom_host.zone:
|
|
213
|
+
# Create a DNS record for the custom host in the given zone
|
|
214
|
+
rs = azure.network.RecordSet(
|
|
215
|
+
f"cdn-cname-{self._name}",
|
|
216
|
+
resource_group_name=self._rg,
|
|
217
|
+
zone_name=custom_host.zone.name,
|
|
218
|
+
relative_record_set_name=custom_host.host,
|
|
219
|
+
record_type="CNAME",
|
|
220
|
+
ttl=3600,
|
|
221
|
+
target_resource=azure.network.SubResourceArgs(
|
|
222
|
+
id=self._cdn_endpoint.id,
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
azure.cdn.CustomDomain(
|
|
227
|
+
f"cdn-custom-domain-{self._name}",
|
|
228
|
+
resource_group_name=self._rg,
|
|
229
|
+
profile_name=self._cdn_profile.name,
|
|
230
|
+
endpoint_name=self._cdn_endpoint.name,
|
|
231
|
+
host_name=custom_host.full_host,
|
|
232
|
+
opts=pulumi.ResourceOptions(depends_on=rs),
|
|
233
|
+
)
|
|
145
234
|
|
|
146
|
-
|
|
235
|
+
return custom_host.full_host
|
|
236
|
+
else:
|
|
237
|
+
# Add custom hostname without a zone
|
|
238
|
+
azure.cdn.CustomDomain(
|
|
239
|
+
f"cdn-custom-domain-{self._name}",
|
|
240
|
+
resource_group_name=self._rg,
|
|
241
|
+
profile_name=self._cdn_profile.name,
|
|
242
|
+
endpoint_name=self._cdn_endpoint.name,
|
|
243
|
+
host_name=custom_host.host,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return custom_host.host
|
|
147
247
|
else:
|
|
148
248
|
# Return the default CDN host name
|
|
149
249
|
return self._cdn_endpoint.host_name
|
|
@@ -200,7 +300,11 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
200
300
|
pulumi.export("pgsql_host", self._pgsql.fully_qualified_domain_name)
|
|
201
301
|
|
|
202
302
|
def _create_subnet(
|
|
203
|
-
self,
|
|
303
|
+
self,
|
|
304
|
+
name,
|
|
305
|
+
prefix,
|
|
306
|
+
delegation_service: Optional[str] = None,
|
|
307
|
+
service_endpoints: Sequence[str] = [],
|
|
204
308
|
) -> azure.network.Subnet:
|
|
205
309
|
"""
|
|
206
310
|
Generic method to create a subnet with a delegation.
|
|
@@ -239,7 +343,22 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
239
343
|
sku=sku,
|
|
240
344
|
)
|
|
241
345
|
|
|
242
|
-
def _create_pgadmin_app(self):
|
|
346
|
+
def _create_pgadmin_app(self, access_ip: Optional[Sequence[str]] = None, dns_zone: Optional[azure.network.Zone] = None):
|
|
347
|
+
# Determine the IP restrictions
|
|
348
|
+
ip_restrictions = []
|
|
349
|
+
default_restriction = azure.web.DefaultAction.ALLOW
|
|
350
|
+
if access_ip:
|
|
351
|
+
default_restriction = azure.web.DefaultAction.DENY
|
|
352
|
+
|
|
353
|
+
for ip in access_ip:
|
|
354
|
+
ip_restrictions.append(
|
|
355
|
+
azure.web.IpSecurityRestrictionArgs(
|
|
356
|
+
action="Allow",
|
|
357
|
+
ip_address=ip,
|
|
358
|
+
priority=300,
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
243
362
|
# The app itself
|
|
244
363
|
app = azure.web.WebApp(
|
|
245
364
|
f"app-pgadmin-{self._name}",
|
|
@@ -255,7 +374,10 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
255
374
|
linux_fx_version="DOCKER|dpage/pgadmin4",
|
|
256
375
|
health_check_path="/misc/ping",
|
|
257
376
|
app_settings=[
|
|
258
|
-
azure.web.NameValuePairArgs(
|
|
377
|
+
azure.web.NameValuePairArgs(
|
|
378
|
+
name="DOCKER_REGISTRY_SERVER_URL",
|
|
379
|
+
value="https://index.docker.io/v1",
|
|
380
|
+
),
|
|
259
381
|
azure.web.NameValuePairArgs(name="DOCKER_ENABLE_CI", value="true"),
|
|
260
382
|
# azure.web.NameValuePairArgs(name="WEBSITE_HTTPLOGGING_RETENTION_DAYS", value="7"),
|
|
261
383
|
# pgAdmin settings
|
|
@@ -263,6 +385,9 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
263
385
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_EMAIL", value="dbadmin@dbadmin.net"),
|
|
264
386
|
azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_PASSWORD", value="dbadmin"),
|
|
265
387
|
],
|
|
388
|
+
# IP restrictions
|
|
389
|
+
ip_security_restrictions_default_action=default_restriction,
|
|
390
|
+
ip_security_restrictions=ip_restrictions,
|
|
266
391
|
),
|
|
267
392
|
)
|
|
268
393
|
|
|
@@ -274,6 +399,50 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
274
399
|
share_name="pgadmin",
|
|
275
400
|
)
|
|
276
401
|
|
|
402
|
+
if dns_zone:
|
|
403
|
+
# Create a DNS record for the pgAdmin app
|
|
404
|
+
cname = azure.network.RecordSet(
|
|
405
|
+
f"dns-cname-pgadmin-{self._name}",
|
|
406
|
+
resource_group_name=self._rg,
|
|
407
|
+
zone_name=dns_zone.name,
|
|
408
|
+
relative_record_set_name="pgadmin",
|
|
409
|
+
record_type="CNAME",
|
|
410
|
+
ttl=3600,
|
|
411
|
+
cname_record=azure.network.CnameRecordArgs(
|
|
412
|
+
cname=app.default_host_name,
|
|
413
|
+
),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# For the certificate validation to work
|
|
417
|
+
txt_validation = azure.network.RecordSet(
|
|
418
|
+
f"dns-txt-pgadmin-{self._name}",
|
|
419
|
+
resource_group_name=self._rg,
|
|
420
|
+
zone_name=dns_zone.name,
|
|
421
|
+
relative_record_set_name="asuid.pgadmin",
|
|
422
|
+
record_type="TXT",
|
|
423
|
+
ttl=3600,
|
|
424
|
+
txt_records=[
|
|
425
|
+
azure.network.TxtRecordArgs(
|
|
426
|
+
value=[app.custom_domain_verification_id],
|
|
427
|
+
)
|
|
428
|
+
],
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Add custom hostname
|
|
432
|
+
self._add_webapp_host(
|
|
433
|
+
app=app,
|
|
434
|
+
host=dns_zone.name.apply(lambda name: f"pgadmin.{name}"),
|
|
435
|
+
suffix=self._name,
|
|
436
|
+
depends_on=[cname, txt_validation],
|
|
437
|
+
identifier="pgadmin",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Export the custom hostname
|
|
441
|
+
pulumi.export("pgadmin_url", dns_zone.name.apply(lambda name: f"https://pgadmin.{name}"))
|
|
442
|
+
else:
|
|
443
|
+
# Export the default hostname
|
|
444
|
+
pulumi.export("pgadmin_url", app.default_host_name.apply(lambda host: f"https://{host}"))
|
|
445
|
+
|
|
277
446
|
# Mount the storage container
|
|
278
447
|
azure.web.WebAppAzureStorageAccounts(
|
|
279
448
|
f"app-pgadmin-mount-{self._name}",
|
|
@@ -290,9 +459,24 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
290
459
|
},
|
|
291
460
|
)
|
|
292
461
|
|
|
293
|
-
|
|
462
|
+
def _get_existing_web_app_host_name_binding(self, resource_group_name: str, app_name: str, host_name: str):
|
|
463
|
+
try:
|
|
464
|
+
return azure.web.get_web_app_host_name_binding(
|
|
465
|
+
resource_group_name=resource_group_name,
|
|
466
|
+
name=app_name,
|
|
467
|
+
host_name=host_name,
|
|
468
|
+
)
|
|
469
|
+
except Exception:
|
|
470
|
+
return None
|
|
294
471
|
|
|
295
|
-
def _add_webapp_host(
|
|
472
|
+
def _add_webapp_host(
|
|
473
|
+
self,
|
|
474
|
+
app: azure.web.WebApp,
|
|
475
|
+
host: Union[str, pulumi.Input[str]],
|
|
476
|
+
suffix: str,
|
|
477
|
+
identifier: str,
|
|
478
|
+
depends_on: Optional[Sequence[pulumi.Resource]] = None,
|
|
479
|
+
):
|
|
296
480
|
"""
|
|
297
481
|
Because of a circular dependency, we need to create the certificate and the binding in two steps.
|
|
298
482
|
First we create a binding without a certificate,
|
|
@@ -305,48 +489,98 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
305
489
|
:param app: The web app
|
|
306
490
|
:param host: The host name
|
|
307
491
|
:param suffix: A suffix to make the resource name unique
|
|
492
|
+
:param depend_on: The resource to depend on (optional)
|
|
308
493
|
"""
|
|
309
494
|
|
|
310
|
-
|
|
495
|
+
if not depends_on:
|
|
496
|
+
depends_on = []
|
|
311
497
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
resource_group_name=
|
|
316
|
-
|
|
317
|
-
host_name=
|
|
498
|
+
# Retrieve the existing binding (None if it doesn't exist)
|
|
499
|
+
existing_binding = pulumi.Output.all(app.resource_group, app.name, host).apply(
|
|
500
|
+
lambda args: self._get_existing_web_app_host_name_binding(
|
|
501
|
+
resource_group_name=args[0],
|
|
502
|
+
app_name=args[1],
|
|
503
|
+
host_name=args[2],
|
|
318
504
|
)
|
|
505
|
+
)
|
|
319
506
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
507
|
+
# Create an inline function that we will invoke through the Output.apply lambda
|
|
508
|
+
def _create_binding_with_cert(existing_binding):
|
|
509
|
+
if existing_binding:
|
|
510
|
+
# Create a managed certificate
|
|
511
|
+
# This will work because the binding exists actually
|
|
512
|
+
certificate = azure.web.Certificate(
|
|
513
|
+
f"cert-{suffix}-{identifier}",
|
|
514
|
+
resource_group_name=self._rg,
|
|
515
|
+
server_farm_id=app.server_farm_id,
|
|
516
|
+
canonical_name=host,
|
|
517
|
+
host_names=[host],
|
|
518
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
519
|
+
)
|
|
329
520
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
521
|
+
# Create a new binding, replacing the old one,
|
|
522
|
+
# with the certificate
|
|
523
|
+
azure.web.WebAppHostNameBinding(
|
|
524
|
+
f"host-binding-{suffix}-{identifier}",
|
|
525
|
+
resource_group_name=self._rg,
|
|
526
|
+
name=app.name,
|
|
527
|
+
host_name=host,
|
|
528
|
+
ssl_state=azure.web.SslState.SNI_ENABLED,
|
|
529
|
+
thumbprint=certificate.thumbprint,
|
|
530
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
else:
|
|
534
|
+
# Create a binding without a certificate
|
|
535
|
+
azure.web.WebAppHostNameBinding(
|
|
536
|
+
f"host-binding-{suffix}-{identifier}",
|
|
537
|
+
resource_group_name=self._rg,
|
|
538
|
+
name=app.name,
|
|
539
|
+
host_name=host,
|
|
540
|
+
opts=pulumi.ResourceOptions(depends_on=depends_on),
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
existing_binding.apply(_create_binding_with_cert)
|
|
544
|
+
|
|
545
|
+
def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.network.RecordSet]:
|
|
546
|
+
created_records = []
|
|
547
|
+
|
|
548
|
+
# Domain validation and SPF record (one TXT record with multiple values)
|
|
549
|
+
r = azure.network.RecordSet(
|
|
550
|
+
f"dns-comms-{suffix}-{host.identifier}-domain",
|
|
551
|
+
resource_group_name=self._rg,
|
|
552
|
+
zone_name=host.zone.name,
|
|
553
|
+
relative_record_set_name=host.host,
|
|
554
|
+
record_type="TXT",
|
|
555
|
+
ttl=3600,
|
|
556
|
+
txt_records=[
|
|
557
|
+
azure.network.TxtRecordArgs(value=[records["domain"]["value"]]),
|
|
558
|
+
azure.network.TxtRecordArgs(value=[records["s_pf"]["value"]]),
|
|
559
|
+
],
|
|
560
|
+
)
|
|
561
|
+
created_records.append(r)
|
|
562
|
+
|
|
563
|
+
# DKIM records (two CNAME records)
|
|
564
|
+
for record in ("d_kim", "d_kim2"):
|
|
565
|
+
if host.host == "@":
|
|
566
|
+
relative_record_set_name = records[record]["name"]
|
|
567
|
+
else:
|
|
568
|
+
relative_record_set_name = f"{records[record]['name']}.{host.host}"
|
|
569
|
+
|
|
570
|
+
r = azure.network.RecordSet(
|
|
571
|
+
f"dns-comms-{suffix}-{host.identifier}-{record}",
|
|
344
572
|
resource_group_name=self._rg,
|
|
345
|
-
|
|
346
|
-
|
|
573
|
+
zone_name=host.zone.name,
|
|
574
|
+
relative_record_set_name=relative_record_set_name,
|
|
575
|
+
record_type="CNAME",
|
|
576
|
+
ttl=records[record]["ttl"],
|
|
577
|
+
cname_record=azure.network.CnameRecordArgs(cname=records[record]["value"]),
|
|
347
578
|
)
|
|
579
|
+
created_records.append(r)
|
|
348
580
|
|
|
349
|
-
|
|
581
|
+
return created_records
|
|
582
|
+
|
|
583
|
+
def _add_webapp_comms(self, data_location: str, domains: list[HostDefinition], suffix: str) -> azure.communication.CommunicationService:
|
|
350
584
|
email_service = azure.communication.EmailService(
|
|
351
585
|
f"comms-email-{suffix}",
|
|
352
586
|
resource_group_name=self._rg,
|
|
@@ -355,30 +589,37 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
355
589
|
)
|
|
356
590
|
|
|
357
591
|
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
|
|
592
|
+
comm_dependencies = []
|
|
593
|
+
|
|
594
|
+
# Add our own custom domains
|
|
595
|
+
for domain in domains:
|
|
373
596
|
d = azure.communication.Domain(
|
|
374
|
-
f"comms-email-domain-{suffix}-
|
|
597
|
+
f"comms-email-domain-{suffix}-{domain.identifier}",
|
|
375
598
|
resource_group_name=self._rg,
|
|
376
599
|
location="global",
|
|
377
|
-
domain_management=azure.communication.DomainManagement.
|
|
378
|
-
domain_name=
|
|
600
|
+
domain_management=azure.communication.DomainManagement.CUSTOMER_MANAGED,
|
|
601
|
+
domain_name=domain.full_host,
|
|
379
602
|
email_service_name=email_service.name,
|
|
380
603
|
)
|
|
381
|
-
|
|
604
|
+
|
|
605
|
+
if domain.zone:
|
|
606
|
+
# Create DNS records in the managed zone
|
|
607
|
+
comm_dependencies = pulumi.Output.all(suffix, domain, d.verification_records).apply(
|
|
608
|
+
lambda args: self._create_comms_dns_records(suffix=args[0], host=args[1], records=args[2])
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
domain_resources.append(d.id)
|
|
612
|
+
|
|
613
|
+
# Add an Azure managed domain
|
|
614
|
+
d = azure.communication.Domain(
|
|
615
|
+
f"comms-email-domain-{suffix}-azure",
|
|
616
|
+
resource_group_name=self._rg,
|
|
617
|
+
location="global",
|
|
618
|
+
domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
|
|
619
|
+
domain_name="AzureManagedDomain",
|
|
620
|
+
email_service_name=email_service.name,
|
|
621
|
+
)
|
|
622
|
+
domain_resources.append(d.id)
|
|
382
623
|
|
|
383
624
|
# Create Communication Services and link the domains
|
|
384
625
|
comm_service = azure.communication.CommunicationService(
|
|
@@ -387,16 +628,25 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
387
628
|
location="global",
|
|
388
629
|
data_location=data_location,
|
|
389
630
|
linked_domains=domain_resources,
|
|
631
|
+
opts=pulumi.ResourceOptions(depends_on=comm_dependencies),
|
|
390
632
|
)
|
|
391
633
|
|
|
392
634
|
return comm_service
|
|
393
635
|
|
|
394
636
|
def _add_webapp_vault(self, administrators: list[str], suffix: str) -> azure.keyvault.Vault:
|
|
395
|
-
# Create a keyvault
|
|
637
|
+
# Create a keyvault with a random suffix to make the name unique
|
|
638
|
+
random_suffix = pulumi_random.RandomString(
|
|
639
|
+
f"vault-suffix-{suffix}",
|
|
640
|
+
# Total length is 24, so deduct the length of the suffix
|
|
641
|
+
length=(24 - 7 - len(suffix)),
|
|
642
|
+
special=False,
|
|
643
|
+
upper=False,
|
|
644
|
+
)
|
|
645
|
+
|
|
396
646
|
vault = azure.keyvault.Vault(
|
|
397
647
|
f"vault-{suffix}",
|
|
398
648
|
resource_group_name=self._rg,
|
|
399
|
-
vault_name=f"vault-{suffix}",
|
|
649
|
+
vault_name=random_suffix.result.apply(lambda r: f"vault-{suffix}-{r}"),
|
|
400
650
|
properties=azure.keyvault.VaultPropertiesArgs(
|
|
401
651
|
tenant_id=self._tenant_id,
|
|
402
652
|
sku=azure.keyvault.SkuArgs(
|
|
@@ -407,27 +657,35 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
407
657
|
),
|
|
408
658
|
)
|
|
409
659
|
|
|
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
660
|
# Add vault administrators
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
661
|
+
if administrators:
|
|
662
|
+
# Find the Key Vault Administrator role
|
|
663
|
+
administrator_role = vault.id.apply(
|
|
664
|
+
lambda scope: azure.authorization.get_role_definition(
|
|
665
|
+
role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
|
|
666
|
+
scope=scope,
|
|
667
|
+
)
|
|
426
668
|
)
|
|
427
669
|
|
|
670
|
+
# Actual administrator roles
|
|
671
|
+
for a in administrators:
|
|
672
|
+
azure.authorization.RoleAssignment(
|
|
673
|
+
f"ra-{suffix}-vault-admin-{a}",
|
|
674
|
+
principal_id=a,
|
|
675
|
+
principal_type=azure.authorization.PrincipalType.USER,
|
|
676
|
+
role_definition_id=administrator_role.id,
|
|
677
|
+
scope=vault.id,
|
|
678
|
+
)
|
|
679
|
+
|
|
428
680
|
return vault
|
|
429
681
|
|
|
430
|
-
def _add_webapp_secret(
|
|
682
|
+
def _add_webapp_secret(
|
|
683
|
+
self,
|
|
684
|
+
vault: azure.keyvault.Vault,
|
|
685
|
+
secret_name: str,
|
|
686
|
+
config_secret_name: str,
|
|
687
|
+
suffix: str,
|
|
688
|
+
):
|
|
431
689
|
secret = self._config.require_secret(config_secret_name)
|
|
432
690
|
|
|
433
691
|
# Normalize the secret name
|
|
@@ -487,13 +745,14 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
487
745
|
db_name: str,
|
|
488
746
|
repository_url: str,
|
|
489
747
|
repository_branch: str,
|
|
490
|
-
website_hosts: list[
|
|
748
|
+
website_hosts: list[HostDefinition],
|
|
491
749
|
django_settings_module: str,
|
|
492
|
-
environment_variables: dict[str, str] =
|
|
493
|
-
secrets: dict[str, str] =
|
|
750
|
+
environment_variables: Optional[dict[str, str]] = None,
|
|
751
|
+
secrets: Optional[dict[str, str]] = None,
|
|
494
752
|
comms_data_location: Optional[str] = None,
|
|
495
|
-
comms_domains: Optional[list[
|
|
496
|
-
|
|
753
|
+
comms_domains: Optional[list[HostDefinition]] = None,
|
|
754
|
+
dedicated_app_service_sku: Optional[azure.web.SkuDescriptionArgs] = None,
|
|
755
|
+
vault_administrators: Optional[list[str]] = None,
|
|
497
756
|
) -> azure.web.WebApp:
|
|
498
757
|
"""
|
|
499
758
|
Create a Django website with it's own database and storage containers.
|
|
@@ -510,6 +769,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
510
769
|
and the name of the secret in the Key Vault.
|
|
511
770
|
:param comms_data_location: The data location for the Communication Services (optional if you don't need it).
|
|
512
771
|
:param comms_domains: The list of custom domains for the E-mail Communication Services (optional).
|
|
772
|
+
:param dedicated_app_service_sku: The SKU for the dedicated App Service Plan (optional).
|
|
513
773
|
:param vault_administrator: The principal ID of the vault administrator (optional).
|
|
514
774
|
"""
|
|
515
775
|
|
|
@@ -541,7 +801,11 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
541
801
|
|
|
542
802
|
# Communication Services (optional)
|
|
543
803
|
if comms_data_location:
|
|
804
|
+
if not comms_domains:
|
|
805
|
+
comms_domains = []
|
|
806
|
+
|
|
544
807
|
comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
|
|
808
|
+
|
|
545
809
|
# Add the service endpoint as environment variable
|
|
546
810
|
environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
|
|
547
811
|
else:
|
|
@@ -567,10 +831,24 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
567
831
|
for key, value in environment_variables.items()
|
|
568
832
|
]
|
|
569
833
|
|
|
834
|
+
allowed_hosts = pulumi.Output.concat(*[pulumi.Output.concat(host.full_host, ",") for host in website_hosts])
|
|
835
|
+
|
|
836
|
+
# Create a dedicated App Service Plan if requested
|
|
837
|
+
if dedicated_app_service_sku:
|
|
838
|
+
app_service_plan = azure.web.AppServicePlan(
|
|
839
|
+
f"asp-{self._name}-{name}",
|
|
840
|
+
resource_group_name=self._rg,
|
|
841
|
+
kind="Linux",
|
|
842
|
+
reserved=True,
|
|
843
|
+
sku=dedicated_app_service_sku,
|
|
844
|
+
)
|
|
845
|
+
else:
|
|
846
|
+
app_service_plan = self._app_service_plan
|
|
847
|
+
|
|
570
848
|
app = azure.web.WebApp(
|
|
571
849
|
f"app-{name}-{self._name}",
|
|
572
850
|
resource_group_name=self._rg,
|
|
573
|
-
server_farm_id=
|
|
851
|
+
server_farm_id=app_service_plan.id,
|
|
574
852
|
virtual_network_subnet_id=self._app_subnet.id,
|
|
575
853
|
identity=azure.web.ManagedServiceIdentityArgs(
|
|
576
854
|
type=azure.web.ManagedServiceIdentityType.SYSTEM_ASSIGNED,
|
|
@@ -595,12 +873,18 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
595
873
|
# azure.web.NameValuePairArgs(name="DEBUG", value="true"),
|
|
596
874
|
azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
|
|
597
875
|
azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
|
|
598
|
-
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=
|
|
876
|
+
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=allowed_hosts),
|
|
599
877
|
# Vault settings
|
|
600
878
|
azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
|
|
601
879
|
# Storage settings
|
|
602
|
-
azure.web.NameValuePairArgs(
|
|
603
|
-
|
|
880
|
+
azure.web.NameValuePairArgs(
|
|
881
|
+
name="AZURE_STORAGE_ACCOUNT_NAME",
|
|
882
|
+
value=self._storage_account.name,
|
|
883
|
+
),
|
|
884
|
+
azure.web.NameValuePairArgs(
|
|
885
|
+
name="AZURE_STORAGE_CONTAINER_STATICFILES",
|
|
886
|
+
value=static_container.name,
|
|
887
|
+
),
|
|
604
888
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_MEDIA", value=media_container.name),
|
|
605
889
|
# CDN
|
|
606
890
|
azure.web.NameValuePairArgs(name="CDN_HOST", value=self._cdn_host),
|
|
@@ -623,9 +907,69 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
623
907
|
# We need this to verify custom domains
|
|
624
908
|
pulumi.export(f"{name}_site_domain_verification_id", app.custom_domain_verification_id)
|
|
625
909
|
pulumi.export(f"{name}_site_domain_cname", app.default_host_name)
|
|
910
|
+
virtual_ip = app.outbound_ip_addresses.apply(lambda addresses: addresses.split(",")[-1])
|
|
911
|
+
pulumi.export(f"{name}_site_virtual_ip", virtual_ip)
|
|
912
|
+
|
|
913
|
+
# Get the URL of the publish profile.
|
|
914
|
+
# Use app.identity here too to ensure the app is actually created before getting credentials.
|
|
915
|
+
credentials = pulumi.Output.all(self._rg, app.name, app.identity).apply(
|
|
916
|
+
lambda args: azure.web.list_web_app_publishing_credentials(
|
|
917
|
+
resource_group_name=args[0],
|
|
918
|
+
name=args[1],
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
pulumi.export(f"{name}_deploy_url", pulumi.Output.concat(credentials.scm_uri, "/deploy"))
|
|
626
923
|
|
|
627
924
|
for host in website_hosts:
|
|
628
|
-
|
|
925
|
+
dependencies = []
|
|
926
|
+
|
|
927
|
+
if host.zone:
|
|
928
|
+
# Create a DNS record in the zone.
|
|
929
|
+
# We always use an A record instead of CNAME to avoid collisions with TXT records.
|
|
930
|
+
a = azure.network.RecordSet(
|
|
931
|
+
f"dns-a-{name}-{self._name}-{host.identifier}",
|
|
932
|
+
resource_group_name=self._rg,
|
|
933
|
+
zone_name=host.zone.name,
|
|
934
|
+
relative_record_set_name=host.host,
|
|
935
|
+
record_type="A",
|
|
936
|
+
ttl=3600,
|
|
937
|
+
a_records=[
|
|
938
|
+
azure.network.ARecordArgs(
|
|
939
|
+
ipv4_address=virtual_ip,
|
|
940
|
+
)
|
|
941
|
+
],
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
dependencies.append(a)
|
|
945
|
+
|
|
946
|
+
# For the certificate validation to work
|
|
947
|
+
relative_record_set_name = "asuid" if host.host == "@" else pulumi.Output.concat("asuid.", host.host)
|
|
948
|
+
|
|
949
|
+
txt_validation = azure.network.RecordSet(
|
|
950
|
+
f"dns-txt-{name}-{self._name}-{host.identifier}",
|
|
951
|
+
resource_group_name=self._rg,
|
|
952
|
+
zone_name=host.zone.name,
|
|
953
|
+
relative_record_set_name=relative_record_set_name,
|
|
954
|
+
record_type="TXT",
|
|
955
|
+
ttl=3600,
|
|
956
|
+
txt_records=[
|
|
957
|
+
azure.network.TxtRecordArgs(
|
|
958
|
+
value=[app.custom_domain_verification_id],
|
|
959
|
+
)
|
|
960
|
+
],
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
dependencies.append(txt_validation)
|
|
964
|
+
|
|
965
|
+
# Add the host with optional dependencies
|
|
966
|
+
self._add_webapp_host(
|
|
967
|
+
app=app,
|
|
968
|
+
host=host.full_host,
|
|
969
|
+
suffix=f"{name}-{self._name}",
|
|
970
|
+
identifier=host.identifier,
|
|
971
|
+
depends_on=dependencies,
|
|
972
|
+
)
|
|
629
973
|
|
|
630
974
|
# To enable deployment from GitLab
|
|
631
975
|
azure.web.WebAppSourceControl(
|
|
@@ -641,7 +985,8 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
641
985
|
|
|
642
986
|
# Where we can retrieve the SSH key
|
|
643
987
|
pulumi.export(
|
|
644
|
-
f"{name}_deploy_ssh_key_url",
|
|
988
|
+
f"{name}_deploy_ssh_key_url",
|
|
989
|
+
app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1"),
|
|
645
990
|
)
|
|
646
991
|
|
|
647
992
|
# Find the role for Key Vault Secrets User
|
|
@@ -713,28 +1058,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
713
1058
|
scope=self._cdn_endpoint.id,
|
|
714
1059
|
)
|
|
715
1060
|
|
|
716
|
-
|
|
717
|
-
if website_hosts:
|
|
718
|
-
origins = [f"https://{host}" for host in website_hosts]
|
|
719
|
-
else:
|
|
720
|
-
origins = ["*"]
|
|
1061
|
+
return app
|
|
721
1062
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
account_name=self._storage_account.name,
|
|
726
|
-
blob_services_name="default",
|
|
727
|
-
cors=azure.storage.CorsRulesArgs(
|
|
728
|
-
cors_rules=[
|
|
729
|
-
azure.storage.CorsRuleArgs(
|
|
730
|
-
allowed_headers=["*"],
|
|
731
|
-
allowed_methods=["GET", "OPTIONS", "HEAD"],
|
|
732
|
-
allowed_origins=origins,
|
|
733
|
-
exposed_headers=["Access-Control-Allow-Origin"],
|
|
734
|
-
max_age_in_seconds=86400,
|
|
735
|
-
)
|
|
736
|
-
]
|
|
737
|
-
),
|
|
738
|
-
)
|
|
1063
|
+
def _strip_off_dns_zone_name(self, host: str, zone: azure.network.Zone) -> pulumi.Output[str]:
|
|
1064
|
+
"""
|
|
1065
|
+
Strip off the DNS zone name from the host name.
|
|
739
1066
|
|
|
740
|
-
|
|
1067
|
+
:param host: The host name
|
|
1068
|
+
:return: The host name without the DNS zone
|
|
1069
|
+
"""
|
|
1070
|
+
return zone.name.apply(lambda name: host[: -len(name) - 1])
|
{pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.17/src/pulumi_django_azure.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pulumi-django-azure
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.17
|
|
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>=3.
|
|
37
|
-
Requires-Dist: pulumi-azure-native>=2.
|
|
38
|
-
Requires-Dist: pulumi-random>=4.
|
|
36
|
+
Requires-Dist: pulumi>=3.146.0
|
|
37
|
+
Requires-Dist: pulumi-azure-native>=2.82.0
|
|
38
|
+
Requires-Dist: pulumi-random>=4.17.0
|
|
39
39
|
|
|
40
40
|
# Pulumi Django Deployment
|
|
41
41
|
|
|
@@ -59,9 +59,10 @@ Your Django project should contain a folder `cicd` with these files:
|
|
|
59
59
|
```bash
|
|
60
60
|
python manage.py migrate
|
|
61
61
|
python manage.py collectstatic --noinput
|
|
62
|
+
python manage.py purge_cdn
|
|
62
63
|
gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
|
|
63
64
|
```
|
|
64
|
-
Be sure to change `yourapplication` in the above.
|
|
65
|
+
Be sure to change `yourapplication` in the above. To see the `purge_cdn` management command we use here, see below.
|
|
65
66
|
## Installation
|
|
66
67
|
This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
|
|
67
68
|
|
|
@@ -256,6 +257,82 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
|
|
|
256
257
|
|
|
257
258
|
This would then trigger a redeploy everytime you make a commit to your live branch.
|
|
258
259
|
|
|
260
|
+
## CDN Purging
|
|
261
|
+
We added a management command to Django to purge the CDN cache, and added that to the startup script. Our version is here:
|
|
262
|
+
```python
|
|
263
|
+
import os
|
|
264
|
+
|
|
265
|
+
from azure.mgmt.cdn import CdnManagementClient
|
|
266
|
+
from azure.mgmt.cdn.models import PurgeParameters
|
|
267
|
+
from django.core.management.base import BaseCommand
|
|
268
|
+
|
|
269
|
+
from core.azure_helper import AZURE_CREDENTIAL, get_subscription_id
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class Command(BaseCommand):
|
|
273
|
+
help = "Purges the CDN endpoint"
|
|
274
|
+
|
|
275
|
+
def add_arguments(self, parser):
|
|
276
|
+
parser.add_argument(
|
|
277
|
+
"--wait",
|
|
278
|
+
action="store_true",
|
|
279
|
+
help="Wait for the purge operation to complete",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def handle(self, *args, **options):
|
|
283
|
+
# Read environment variables
|
|
284
|
+
resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
|
|
285
|
+
profile_name = os.getenv("CDN_PROFILE")
|
|
286
|
+
endpoint_name = os.getenv("CDN_ENDPOINT")
|
|
287
|
+
content_paths = ["/*"]
|
|
288
|
+
|
|
289
|
+
# Ensure all required environment variables are set
|
|
290
|
+
if not all([resource_group, profile_name, endpoint_name]):
|
|
291
|
+
self.stderr.write(self.style.ERROR("Missing required environment variables."))
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
# Authenticate with Azure
|
|
295
|
+
cdn_client = CdnManagementClient(AZURE_CREDENTIAL, get_subscription_id())
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
# Purge the CDN endpoint
|
|
299
|
+
purge_operation = cdn_client.endpoints.begin_purge_content(
|
|
300
|
+
resource_group_name=resource_group,
|
|
301
|
+
profile_name=profile_name,
|
|
302
|
+
endpoint_name=endpoint_name,
|
|
303
|
+
content_file_paths=PurgeParameters(content_paths=content_paths),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Check if the --wait argument was provided
|
|
307
|
+
if options["wait"]:
|
|
308
|
+
purge_operation.result() # Wait for the operation to complete
|
|
309
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
|
|
310
|
+
else:
|
|
311
|
+
self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
And our azure_helper:
|
|
318
|
+
```python
|
|
319
|
+
from azure.identity import DefaultAzureCredential
|
|
320
|
+
from azure.mgmt.resource import SubscriptionClient
|
|
321
|
+
|
|
322
|
+
# Azure credentials
|
|
323
|
+
AZURE_CREDENTIAL = DefaultAzureCredential()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_db_password() -> str:
|
|
327
|
+
return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def get_subscription_id() -> str:
|
|
331
|
+
subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
|
|
332
|
+
subscriptions = list(subscription_client.subscriptions.list())
|
|
333
|
+
return subscriptions[0].subscription_id
|
|
334
|
+
```
|
|
335
|
+
|
|
259
336
|
## Change requests
|
|
260
337
|
I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
|
|
261
338
|
Therefore this project is not super generic, but tailored to my needs. I am however open to pull or change requests to improve this project or to make it more usable for others.
|
|
@@ -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
|