pulumi-django-azure 1.0.11__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.11/src/pulumi_django_azure.egg-info → pulumi_django_azure-1.0.14}/PKG-INFO +79 -2
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14}/README.md +78 -1
- {pulumi_django_azure-1.0.11 → 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.11 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure/django_deployment.py +426 -102
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14/src/pulumi_django_azure.egg-info}/PKG-INFO +79 -2
- pulumi_django_azure-1.0.11/src/pulumi_django_azure/__init__.py +0 -1
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14}/LICENSE +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14}/setup.cfg +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure.egg-info/SOURCES.txt +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure.egg-info/dependency_links.txt +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14}/src/pulumi_django_azure.egg-info/requires.txt +0 -0
- {pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14}/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.14}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pulumi-django-azure
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.14
|
|
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
|
|
@@ -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.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)
|
|
551
|
+
|
|
552
|
+
return created_records
|
|
348
553
|
|
|
349
|
-
def _add_webapp_comms(self, data_location: str, domains: list[
|
|
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,16 +599,25 @@ 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
|
|
393
606
|
|
|
394
607
|
def _add_webapp_vault(self, administrators: list[str], suffix: str) -> azure.keyvault.Vault:
|
|
395
|
-
# Create a keyvault
|
|
608
|
+
# Create a keyvault with a random suffix to make the name unique
|
|
609
|
+
random_suffix = pulumi_random.RandomString(
|
|
610
|
+
f"vault-suffix-{suffix}",
|
|
611
|
+
# Total length is 24, so deduct the length of the suffix
|
|
612
|
+
length=(24 - 7 - len(suffix)),
|
|
613
|
+
special=False,
|
|
614
|
+
upper=False,
|
|
615
|
+
)
|
|
616
|
+
|
|
396
617
|
vault = azure.keyvault.Vault(
|
|
397
618
|
f"vault-{suffix}",
|
|
398
619
|
resource_group_name=self._rg,
|
|
399
|
-
vault_name=f"vault-{suffix}",
|
|
620
|
+
vault_name=random_suffix.result.apply(lambda r: f"vault-{suffix}-{r}"),
|
|
400
621
|
properties=azure.keyvault.VaultPropertiesArgs(
|
|
401
622
|
tenant_id=self._tenant_id,
|
|
402
623
|
sku=azure.keyvault.SkuArgs(
|
|
@@ -407,27 +628,35 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
407
628
|
),
|
|
408
629
|
)
|
|
409
630
|
|
|
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
631
|
# Add vault administrators
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
+
)
|
|
426
639
|
)
|
|
427
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
|
+
|
|
428
651
|
return vault
|
|
429
652
|
|
|
430
|
-
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
|
+
):
|
|
431
660
|
secret = self._config.require_secret(config_secret_name)
|
|
432
661
|
|
|
433
662
|
# Normalize the secret name
|
|
@@ -487,13 +716,13 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
487
716
|
db_name: str,
|
|
488
717
|
repository_url: str,
|
|
489
718
|
repository_branch: str,
|
|
490
|
-
website_hosts: list[
|
|
719
|
+
website_hosts: list[HostDefinition],
|
|
491
720
|
django_settings_module: str,
|
|
492
|
-
environment_variables: dict[str, str] =
|
|
493
|
-
secrets: dict[str, str] =
|
|
721
|
+
environment_variables: Optional[dict[str, str]] = None,
|
|
722
|
+
secrets: Optional[dict[str, str]] = None,
|
|
494
723
|
comms_data_location: Optional[str] = None,
|
|
495
|
-
comms_domains: Optional[list[
|
|
496
|
-
vault_administrators: Optional[list[str]] =
|
|
724
|
+
comms_domains: Optional[list[HostDefinition]] = None,
|
|
725
|
+
vault_administrators: Optional[list[str]] = None,
|
|
497
726
|
) -> azure.web.WebApp:
|
|
498
727
|
"""
|
|
499
728
|
Create a Django website with it's own database and storage containers.
|
|
@@ -541,7 +770,11 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
541
770
|
|
|
542
771
|
# Communication Services (optional)
|
|
543
772
|
if comms_data_location:
|
|
773
|
+
if not comms_domains:
|
|
774
|
+
comms_domains = []
|
|
775
|
+
|
|
544
776
|
comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
|
|
777
|
+
|
|
545
778
|
# Add the service endpoint as environment variable
|
|
546
779
|
environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
|
|
547
780
|
else:
|
|
@@ -567,6 +800,8 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
567
800
|
for key, value in environment_variables.items()
|
|
568
801
|
]
|
|
569
802
|
|
|
803
|
+
allowed_hosts = pulumi.Output.concat(*[pulumi.Output.concat(host.full_host, ",") for host in website_hosts])
|
|
804
|
+
|
|
570
805
|
app = azure.web.WebApp(
|
|
571
806
|
f"app-{name}-{self._name}",
|
|
572
807
|
resource_group_name=self._rg,
|
|
@@ -595,12 +830,18 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
595
830
|
# azure.web.NameValuePairArgs(name="DEBUG", value="true"),
|
|
596
831
|
azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
|
|
597
832
|
azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
|
|
598
|
-
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=
|
|
833
|
+
azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=allowed_hosts),
|
|
599
834
|
# Vault settings
|
|
600
835
|
azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
|
|
601
836
|
# Storage settings
|
|
602
|
-
azure.web.NameValuePairArgs(
|
|
603
|
-
|
|
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
|
+
),
|
|
604
845
|
azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_MEDIA", value=media_container.name),
|
|
605
846
|
# CDN
|
|
606
847
|
azure.web.NameValuePairArgs(name="CDN_HOST", value=self._cdn_host),
|
|
@@ -623,9 +864,85 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
623
864
|
# We need this to verify custom domains
|
|
624
865
|
pulumi.export(f"{name}_site_domain_verification_id", app.custom_domain_verification_id)
|
|
625
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"))
|
|
626
880
|
|
|
627
881
|
for host in website_hosts:
|
|
628
|
-
|
|
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
|
+
)
|
|
629
946
|
|
|
630
947
|
# To enable deployment from GitLab
|
|
631
948
|
azure.web.WebAppSourceControl(
|
|
@@ -641,7 +958,8 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
641
958
|
|
|
642
959
|
# Where we can retrieve the SSH key
|
|
643
960
|
pulumi.export(
|
|
644
|
-
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"),
|
|
645
963
|
)
|
|
646
964
|
|
|
647
965
|
# Find the role for Key Vault Secrets User
|
|
@@ -714,10 +1032,7 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
714
1032
|
)
|
|
715
1033
|
|
|
716
1034
|
# Create a CORS rules for this website
|
|
717
|
-
if website_hosts
|
|
718
|
-
origins = [f"https://{host}" for host in website_hosts]
|
|
719
|
-
else:
|
|
720
|
-
origins = ["*"]
|
|
1035
|
+
origins = [pulumi.Output.concat("https://", host.full_host) for host in website_hosts] if website_hosts else ["*"]
|
|
721
1036
|
|
|
722
1037
|
azure.storage.BlobServiceProperties(
|
|
723
1038
|
f"sa-{name}-blob-properties",
|
|
@@ -738,3 +1053,12 @@ class DjangoDeployment(pulumi.ComponentResource):
|
|
|
738
1053
|
)
|
|
739
1054
|
|
|
740
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])
|
{pulumi_django_azure-1.0.11 → pulumi_django_azure-1.0.14/src/pulumi_django_azure.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pulumi-django-azure
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.14
|
|
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
|
|
@@ -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
|
|
File without changes
|