-
Notifications
You must be signed in to change notification settings - Fork 5
/
init-platform.ps1
380 lines (301 loc) · 15 KB
/
init-platform.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
[CmdletBinding()]
Param ()
"This script will set up the initial resources in your Azure Account and in your GitHub repository to allow for automated deployments."
""
"IMPORTANT: You must be a 'Global Administrator' in your Azure tenant and you must have admin rights in your GitHub repository to execute this script!"
""
"Changes to your Azure account:"
""
"* The shared platform resources will be created"
" - A platform resource group '{platform}-platform'"
" - A platform storage account '{platform}st'"
" - A platform container registry '{platform}cr'"
""
"* A managed identity '{platform}-github-id' will be created in the platform resource group"
" - The identity will be used by GitHub Actions to deploy resources to Azure"
" - The identity will be given 'Contributor' and 'User Access Administrator' roles in your current Azure subscription"
" - The identity will be given the AAD permission 'Group.Read.All'"
""
"* A managed identity '{environment}-sql-admin-id' will be created per environment (as configured in 'config.json')"
" - The identity will later be used by the SQL server to allow for Azure AD-based authentication"
" - The identity will be given the AAD permissions 'Application.Read.All', 'GroupMember.Read.All', 'User.Read.All'"
""
"* An AAD group '{environment}-sql-admins' will be created per environment (as configured in 'config.json')"
" - This group will later be set as the SQL server admin to allow for AAD based management of SQL admins"
" - The '{environment}-sql-admin-id' identity will be added to the group as the first member"
" - You can add additional admins to this group later"
""
"Changes to your GitHub repository:"
""
"* A 'platform'-environment and the environments configured in 'config.json' will be added to your GitHub repository"
" - You will be set as a required reviewer to prevent unindentional deployments"
" - You can manually change the protection rules later (Changes will NOT be overwritten if you call this script again afterwards)"
""
"* Your GitHub repository will be configured with the necessary secrets (to authenticate as the given managed identity)"
""
"NOTE: It is safe to run this script multiple times (e.g. when you add an environment)."
""
$decision = $Host.UI.PromptForChoice($null, "Are you sure you want to execute this script?", ('&Yes', '&No'), 1)
if ($decision -ne 0) {
Write-Error "Script aborted."
exit
}
$ErrorActionPreference = "Stop"
. .\_includes\helpers.ps1
############################
""
"Ensuring required tools are installed"
if (Get-Command Get-AzContext -ErrorAction Ignore) {
Write-Success "Azure PowerShell module"
} else {
throw "'Azure PowerShell' is not installed. See https://docs.microsoft.com/en-us/powershell/azure/install-az-ps"
}
if (Get-Command bicep -ErrorAction Ignore) {
Write-Success "Bicep CLI"
} else {
throw "'Bicep CLI' is not installed. See https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/install"
}
if (Get-Command gh -ErrorAction Ignore) {
Write-Success "GitHub CLI"
} else {
throw "'GitHub CLI' is not installed. See https://github.com/cli/cli#installation"
}
############################
""
"Confirming Azure Subscription"
$azContext = Get-AzContext
if (!$azContext) { throw "You are not signed in to an Azure subscription. Please login using 'Connect-AzAccount'" }
$subscriptionInfo = "You are connected to the subscription '$($azContext.Name)'. Are you sure you want to install the necessary resources here?"
$decision = $Host.UI.PromptForChoice($null, $subscriptionInfo, ('&Yes', '&No'), 1)
if ($decision -ne 0) {
Write-Error "Script aborted. Please use 'Connect-AzAccount' to sign in to a different subscription and re-run the script."
exit
}
############################
""
"Confirming GitHub account"
gh auth status
if ($LASTEXITCODE -ne 0) {
exit
} else {
$decision = $Host.UI.PromptForChoice($null, 'Are you sure you this is the correct GitHub account?', ('&Yes', '&No'), 1)
if ($decision -ne 0) {
Write-Error "Script aborted. Please use 'gh auth login' to sign in to a different account and re-run the script."
exit
}
}
############################
""
"Confirming GitHub repo"
$ghRepo = (gh repo view --json name,nameWithOwner,defaultBranchRef,url) | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) {
Write-Error "Script aborted. Please run this script in a folder that is connected with a GitHub repository."
exit
} else {
$repoInfo = "You are connected to the GitHub repo '$($ghRepo.url)'. Are you sure you want to install the necessary resources here?"
$decision = $Host.UI.PromptForChoice($null, $repoInfo, ('&Yes', '&No'), 1)
if ($decision -ne 0) {
Write-Error "Script aborted. Please run this script in a folder that is connected with a GitHub repository."
exit
}
}
############################
""
"Ensuring user is a 'Global Administrator'"
$currentUser = Get-AzADUser -SignedIn
$graphAccessToken = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com/"
$globalAdminRoleId = (Invoke-RestMethod -Method Get -Headers @{ Authorization = "Bearer $($graphAccessToken.Token)" } -Uri "https://graph.microsoft.com/v1.0/directoryRoles?`$filter=displayName eq 'Global Administrator'").value.id
$globalAdminMembers = (Invoke-RestMethod -Method Get -Headers @{ Authorization = "Bearer $($graphAccessToken.Token)" } -Uri "https://graph.microsoft.com/v1.0/directoryRoles/$($globalAdminRoleId)/members").value
$isGlobalAdmin = $globalAdminMembers | Where-Object { $_.id -eq $currentUser.Id }
if ($isGlobalAdmin) {
Write-Success "User '$($currentUser.UserPrincipalName)' is a 'Global Administrator'"
} else {
throw "Current user ($($currentUser.UserPrincipalName)) is not a 'Global Administrator' in Azure AD. You must run this script as a Global Administrator."
}
############################
""
"Loading config"
$names = Get-Content .\names.json | ConvertFrom-Json
$config = Get-Content .\config.json | ConvertFrom-Json
$environments = $config.environments | Get-Member -MemberType NoteProperty | ForEach-Object { $_.Name }
$githubIdentityMsGraphPermissions = @(
"Group.Read.All" # Required to get the SQL Admins AAD group in `deploy-environment.ps1`
)
# https://docs.microsoft.com/en-us/azure/azure-sql/database/authentication-azure-ad-user-assigned-managed-identity?view=azuresql#permissions
$sqlIdentityMsGraphPermissions = @(
"Application.Read.All",
"GroupMember.Read.All",
"User.Read.All"
)
Write-Success "Config loaded"
############################
""
"------------------------"
"Azure platform resources"
"------------------------"
""
"Creating Azure platform resources (this may take a minute)"
$platformDeployment = New-AzSubscriptionDeployment `
-Location $config.location `
-Name ("init-platform-" + (Get-Date).ToString("yyyyMMddHHmmss")) `
-TemplateFile .\platform\main.bicep `
-TemplateParameterObject @{
deployGitHubIdentity = $true
githubRepoNameWithOwner = $ghRepo.nameWithOwner
githubDefaultBranchName = $ghRepo.defaultBranchRef.name
}
Write-Success "Azure platform resources deployed"
# AAD replicates data so future queries might not immediately recognize the newly created object
$githubIdentity = $null
for ($i=1; $i -le 12; $i++) {
$githubIdentity = Get-AzADServicePrincipal -ObjectId $platformDeployment.Outputs.githubIdentityPrincipalId.Value -ErrorAction Ignore
if ($githubIdentity) {
if ($i -gt 1) { Write-Success "GitHub identity found in Azure AD API" }
break
} else {
" GitHub identity not yet available in Azure AD API. Waiting for 10 seconds"
Start-Sleep -Seconds 10
}
}
############################
""
"Assigning MS Graph API permissions to the GitHub identity"
# There is no Bicep-feature or Azure-PowerShell command, so we have to manually call the URL
# (There would be a separate AzureAD PowerShell-module but this would require a separate login, so it's easier to just call the Graph API directly)
$msGraphSp = Get-AzAdServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000"
$graphAccessToken = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com/"
$apiUrl = "https://graph.microsoft.com/v1.0/servicePrincipals/$($githubIdentity.Id)/appRoleAssignments"
$existingAssignments = Invoke-RestMethod -Uri $apiUrl -Method Get -Headers @{ Authorization = "Bearer $($graphAccessToken.Token)" }
foreach ($permissionName in $githubIdentityMsGraphPermissions) {
#$permissionName = "GroupMember.ReadWrite.All"
$appRoleId = ($msGraphSp.AppRole | Where-Object { $_.Value -eq $permissionName } | Select-Object).Id
$exists = $existingAssignments.value | Where-Object { $_.appRoleId -eq $appRoleId }
if ($exists) {
Write-Success "Permission '$permissionName' already exists"
} else {
$body = @{
appRoleId = $appRoleId
resourceId = $msGraphSp.Id
principalId = $githubIdentity.Id
}
Invoke-RestMethod -Uri $apiUrl -Method Post -ContentType "application/json" `
-Headers @{ Authorization = "Bearer $($graphAccessToken.Token)" } `
-Body $($body | convertto-json) | Out-Null
Write-Success "Permission '$permissionName' created"
}
}
############################
""
"-------------------"
"SQL Server identity"
"-------------------"
foreach ($environment in $environments) {
#$environment = "development"
$envConfig = $config.environments | Select-Object -ExpandProperty $environment
$sqlAdminAdGroupName = $($names.sqlAdminAdGroupName).Replace("{environment}", $envConfig.environmentAbbreviation)
############################
""
"Environment '$environment': Creating SQL Admins AAD group"
$sqlAdminAdGroup = Get-AzAdGroup -DisplayName $sqlAdminAdGroupName
if ($sqlAdminAdGroup) {
Write-Success "AAD group '$sqlAdminAdGroupName' already exists"
} else {
$sqlAdminAdGroup = New-AzAdGroup -DisplayName $sqlAdminAdGroupName -MailNickname $sqlAdminAdGroupName -IsAssignableToRole
Write-Success "AAD group '$sqlAdminAdGroupName' created"
}
############################
""
"Environment '$environment': Creating SQL identity (this may take a minute)"
$sqlDeployment = New-AzSubscriptionDeployment `
-Location $config.location `
-Name ("init-sql-" + (Get-Date).ToString("yyyyMMddHHmmss")) `
-TemplateFile .\environment\sql-identity.bicep `
-TemplateParameterObject @{
environment = $environment
}
Write-Success "SQL identity for environment '$environment' created"
# AAD replicates data so future queries might not immediately recognize the newly created object
$sqlIdentity = $null
for ($i=1; $i -le 12; $i++) {
$sqlIdentity = Get-AzADServicePrincipal -ObjectId $sqlDeployment.Outputs.sqlIdentityPrincipalId.Value -ErrorAction Ignore
if ($sqlIdentity) {
if ($i -gt 1) { Write-Success "Identity found in AAD API" }
break
} else {
" Identity not yet available in AAD API. Waiting for 10 seconds"
Start-Sleep -Seconds 10
}
}
############################
""
"Environment '$environment': Assigning MS Graph API permissions to the SQL identity"
# https://docs.microsoft.com/en-us/azure/azure-sql/database/authentication-azure-ad-user-assigned-managed-identity?view=azuresql#permissions
$msGraphSp = Get-AzAdServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000"
$graphAccessToken = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com/"
$apiUrl = "https://graph.microsoft.com/v1.0/servicePrincipals/$($sqlIdentity.Id)/appRoleAssignments"
$existingAssignments = Invoke-RestMethod -Uri $apiUrl -Method Get -Headers @{ Authorization = "Bearer $($graphAccessToken.Token)" }
foreach ($permissionName in $sqlIdentityMsGraphPermissions) {
#$permissionName = "GroupMember.Read.All"
$appRoleId = ($msGraphSp.AppRole | Where-Object { $_.Value -eq $permissionName } | Select-Object).Id
$exists = $existingAssignments.value | Where-Object { $_.appRoleId -eq $appRoleId }
if ($exists) {
Write-Success "Permission '$permissionName' already exists"
} else {
$body = @{
appRoleId = $appRoleId
resourceId = $msGraphSp.Id
principalId = $sqlIdentity.Id
}
Invoke-RestMethod -Uri $apiUrl -Method Post -ContentType "application/json" `
-Headers @{ Authorization = "Bearer $($graphAccessToken.Token)" } `
-Body $($body | convertto-json) | Out-Null
Write-Success "Permission '$permissionName' created"
}
}
############################
""
"Environment '$environment': Adding SQL server identity to SQL Admins AAD group"
$sqlAdminAdGroupMembers = Get-AzADGroupMember -GroupObjectId $sqlAdminAdGroup.Id
if ($sqlAdminAdGroupMembers | Where-Object { $_.Id -eq $sqlIdentity.Id }) {
Write-Success "Membership for SQL identity already exists in group"
} else {
Add-AzADGroupMember -TargetGroupObjectId $sqlAdminAdGroup.Id -MemberObjectId $sqlIdentity.Id
Write-Success "Member for SQL identity added to group"
}
}
############################
""
"-----------------"
"GitHub repository"
"-----------------"
""
"Creating GitHub environments"
$gitHubEnvironments = $environments
$gitHubEnvironments += "platform" # A special environment for deploying the platform resources
# There are no CLI methods for managing environments, so we have to use the REST API: https://github.com/cli/cli/issues/5149
$ghEnvironments = Exec { gh api "/repos/$($ghRepo.nameWithOwner)/environments" -H "Accept: application/vnd.github+json" } | ConvertFrom-Json
$ghUser = Exec { gh api "/user" -H "Accept: application/vnd.github+json" } | ConvertFrom-Json
foreach ($environment in $gitHubEnvironments) {
#$environment = "development"
if ($ghEnvironments.environments | Where-Object { $_.name -eq $environment }) {
Write-Success "Environment '$environment' already exists"
} else {
$body = @{
reviewers = @(
@{ type = "User"; id = $ghUser.id }
)
} | ConvertTo-Json -Compress
$ghEnv = Exec { $body | gh api "/repos/$($ghRepo.nameWithOwner)/environments/$environment" -X PUT -H "Accept: application/vnd.github+json" --input - } | ConvertFrom-Json
Write-Success "Environment '$environment' created with YOU ($($ghUser.login)) as a required reviewer."
" You can modify the protection rules here: $($ghRepo.url)/settings/environments/$($ghEnv.id)/edit"
}
}
############################
""
"Creating GitHub secrets"
Exec { gh secret set "AZURE_CLIENT_ID" -b $githubIdentity.AppId }
Exec { gh secret set "AZURE_SUBSCRIPTION_ID" -b $((Get-AzContext).Subscription.Id) }
Exec { gh secret set "AZURE_TENANT_ID" -b $((Get-AzContext).Subscription.TenantId) }
Exec { gh secret set "REGISTRY_SERVER" -b $platformDeployment.Outputs.platformContainerRegistryUrl.Value }
""
"Script finished. Push your code to your GitHub repository and use GitHub Actions to deploy the 'Environment'-resources next."