├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MigrateIISWebsiteToElasticBeanstalk.ps1 ├── NOTICE ├── README.md ├── cfn_stack └── WWAMALab.yml └── utils ├── iam_trust_relationship_eb.json ├── iam_trust_relationship_ec2.json ├── settings.txt └── templates ├── aws-windows-deployment-manifest.json ├── harden_iis.ps1 ├── iis_hardening.config ├── site_install.ps1 ├── site_post_install.ps1 ├── site_restart.ps1 └── site_uninstall.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /MigrateIISWebsiteToElasticBeanstalk.ps1: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | <#------------------------------------------------------------------------------------------------------------------------------# 15 | SYNOPSIS 16 | Microsoft .NET application modernization utility that migrates IIS websites from Windows servers to AWS Elastic Beanstalk. 17 | DEPENDENCIES 18 | MS PowerShell version 3.0 or above. 19 | #------------------------------------------------------------------------------------------------------------------------------#> 20 | 21 | param ( 22 | [switch]$NonInteractiveMode = $False, 23 | [string]$ProfileLocation, 24 | [string]$ProfileName, 25 | [string]$Region, 26 | [string]$ApplicationIndex, 27 | [string]$ConnectionStringIndex, 28 | [string]$NewConnectionString, 29 | [string]$WindowsPlatformIndex, 30 | [string]$InstanceType, 31 | [string]$ApplicationName, 32 | [string]$EnvironmentType, 33 | [switch]$ReportOnly = $False 34 | ) 35 | 36 | if ($NonInteractiveMode){ 37 | Write-Host "NonInteractiveMode: $NonInteractiveMode, Location: $ProfileLocation, Profile: $ProfileName, Region: $Region, ApplicationIndex: $ApplicationIndex, ConnectionStringIndex: $ConnectionStringIndex, NewConnectionString: $NewConnectionString, Platform Index: $WindowsPlatformIndex, InstanceType: $InstanceType, App Name: $ApplicationName, Environment Type: $EnvironmentType" 38 | } 39 | 40 | $Global:mfarg_awsprofilename = $ProfileName 41 | $Global:mfarg_awsprofilelocation = $ProfileLocation 42 | $Global:mfarg_region = $Region 43 | $Global:mfarg_websitenumstr = $ApplicationIndex 44 | $Global:mfarg_glb_ebAppName = $ApplicationName 45 | $Global:mfarg_userInputWindowsStringNum = $WindowsPlatformIndex 46 | $Global:mfarg_instanceType = $InstanceType 47 | $Global:mfarg_userInputConnectionString = $NewConnectionString 48 | $Global:mfarg_userInputConnectionStringNum = $ConnectionStringIndex 49 | $Global:mfarg_userInputEnvironmentTypeNum = $EnvironmentType 50 | 51 | $Global:mfarg_userconsent = $False 52 | $Global:mfarg_userinputI = "NonInteractiveMode" 53 | $Global:mfarg_userinputY = "Y" 54 | $Global:mfarg_userinputK = "K" 55 | 56 | 57 | #Requires -RunAsAdministrator 58 | $ErrorActionPreference = "Stop" 59 | 60 | function Global:Test-PowerShellSessionRole { 61 | <# 62 | .SYNOPSIS 63 | This function checks if the current session is of the specified windows built-in role 64 | .INPUTS 65 | Windows built-in role 66 | .OUTPUTS 67 | Boolean - if the input role matches with session role or not 68 | .EXAMPLE 69 | Test-PowerShellSessionRole -Role Administrator 70 | #> 71 | [CmdletBinding()] 72 | param ( 73 | [Parameter(Mandatory = $true)] 74 | [ValidateNotNullOrEmpty()] 75 | [System.Security.Principal.WindowsBuiltInRole] 76 | $Role 77 | ) 78 | 79 | $currentSessionIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() 80 | $currentSessionPrincipal = New-Object System.Security.Principal.WindowsPrincipal($currentSessionIdentity) 81 | $currentSessionPrincipal.IsInRole($Role) 82 | } 83 | 84 | function Global:Get-WebsiteByName { 85 | <# 86 | .SYNOPSIS 87 | Calls Get-Website with the name argument and returns the correct website - bugfix for MS 88 | .INPUTS 89 | Name of the website (website names are unique by default on a single server) 90 | .OUTPUTS 91 | ConfigurationElement object that represents the website 92 | .EXAMPLE 93 | Get-WebsiteByName NopCommerce380 94 | #> 95 | [CmdletBinding()] 96 | param ( 97 | [Parameter(Mandatory = $true)] 98 | [ValidateNotNullOrEmpty()] 99 | [String] 100 | $Name 101 | ) 102 | 103 | $website = Get-Website | where { $_.Name -eq $Name } 104 | if ($website -eq $null) { 105 | throw "ERROR: Cannot get website $Name" 106 | } 107 | $website 108 | } 109 | 110 | function Global:Get-WebDeployV3Exe { 111 | <# 112 | .SYNOPSIS 113 | Returns the path of Web Deploy Version 3 executable 114 | .INPUTS 115 | None 116 | .OUTPUTS 117 | String - local path of the msDeploy.exe 118 | #> 119 | [CmdletBinding()] 120 | param () 121 | 122 | $webDeployV3Key = "HKLM:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\3" 123 | if (!(Test-Path $webDeployV3Key)) { 124 | throw "ERROR: Cannot find Web Deploy v3" 125 | } 126 | 127 | $installPath = (Get-ItemProperty $webDeployV3Key -Name InstallPath | Select -ExpandProperty InstallPath) 128 | if ($installPath -eq $null) { 129 | throw "ERROR: Cannot find installation path of Web Deploy v3" 130 | } 131 | 132 | $installPath + "msdeploy.exe" 133 | } 134 | 135 | function Global:Verify-WebsiteExists { 136 | <# 137 | .SYNOPSIS 138 | Verifies if the given website exists on the local machine 139 | .INPUTS 140 | Name of the website 141 | .OUTPUTS 142 | None - will throw exception when test fails 143 | #> 144 | [CmdletBinding()] 145 | param ( 146 | [Parameter(Mandatory = $true)] 147 | [ValidateNotNullOrEmpty()] 148 | [String] 149 | $WebsiteName 150 | ) 151 | 152 | $site = Get-Website | Where-Object { $_.name -eq $WebsiteName } 153 | if (-Not $site) { 154 | Throw "ERROR: Cannot find website $WebsiteName" 155 | } 156 | } 157 | 158 | function Global:Verify-PathExists { 159 | <# 160 | .SYNOPSIS 161 | Verifies if the given path exists 162 | .INPUTS 163 | A full physical path 164 | .OUTPUTS 165 | None - will throw exception when test fails 166 | #> 167 | [CmdletBinding()] 168 | param ( 169 | [Parameter(Mandatory = $true)] 170 | [ValidateNotNullOrEmpty()] 171 | [String] 172 | $PathToTest 173 | ) 174 | 175 | if (!(Test-Path -Path $PathToTest)) { 176 | throw "ERROR: $PathToTest does not exist." 177 | } 178 | } 179 | 180 | function Global:Verify-FolderExistsAndEmpty { 181 | <# 182 | .SYNOPSIS 183 | Tests if the given path: 184 | 1. Exists 185 | 2. Is a folder 186 | 3. Is empty 187 | .INPUTS 188 | Full physical path of the folder 189 | .OUTPUTS 190 | None - will throw exception when test fails 191 | #> 192 | [CmdletBinding()] 193 | param ( 194 | [Parameter(Mandatory = $true)] 195 | [ValidateNotNullOrEmpty()] 196 | [String] 197 | $PathToTest 198 | ) 199 | 200 | Verify-PathExists $PathToTest 201 | 202 | if (!((Get-Item $PathToTest) -is [System.IO.DirectoryInfo])) { 203 | throw "ERROR: Path $PathToTest is not a folder." 204 | } 205 | 206 | $dirInfo = Get-ChildItem $PathToTest | Measure-Object 207 | if ($dirInfo.count -ne 0) { 208 | throw "ERROR: Folder $PathToTest is not empty." 209 | } 210 | } 211 | 212 | function Global:Get-ZippedFolder { 213 | <# 214 | .SYNOPSIS 215 | This function zips a folder, generating the result file into the output folder 216 | .INPUTS 217 | 1. Full physical path of the folder to be zipped 218 | 2. Full physical path of the output folder 219 | 3. Name of the result file 220 | .OUTPUTS 221 | Full physical path of the result zip file 222 | #> 223 | [CmdletBinding()] 224 | param ( 225 | [Parameter(Mandatory = $true)] 226 | [ValidateNotNullOrEmpty()] 227 | [String] 228 | $SourceFolderPath, 229 | 230 | [Parameter(Mandatory = $true)] 231 | [ValidateNotNullOrEmpty()] 232 | [String] 233 | $OutputFolderPath, 234 | 235 | [Parameter(Mandatory = $true)] 236 | [ValidateNotNullOrEmpty()] 237 | [String] 238 | $ZipFileName 239 | ) 240 | 241 | Verify-PathExists $SourceFolderPath 242 | Verify-PathExists $OutputFolderPath 243 | 244 | $resultFilePath = Join-Path $OutputFolderPath $ZipFileName 245 | 246 | $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal 247 | [System.IO.Compression.ZipFile]::CreateFromDirectory($SourceFolderPath, $resultFilePath, $compressionLevel, $false) 248 | 249 | $resultFilePath 250 | } 251 | 252 | function Global:Unzip-Folder { 253 | <# 254 | .SYNOPSIS 255 | This function unzips a zip file and releases the contents into the output folder 256 | .INPUTS 257 | 1. Full physical path of the zip file 258 | 2. Full physical path of the output folder - it must be an empty and existing folder 259 | .OUTPUTS 260 | None 261 | #> 262 | [CmdletBinding()] 263 | param ( 264 | [Parameter(Mandatory = $true)] 265 | [ValidateNotNullOrEmpty()] 266 | [String] 267 | $ZipFilePath, 268 | 269 | [Parameter(Mandatory = $true)] 270 | [ValidateNotNullOrEmpty()] 271 | [String] 272 | $OutputFolderPath 273 | ) 274 | 275 | Verify-PathExists $ZipFilePath 276 | Verify-FolderExistsAndEmpty $OutputFolderPath 277 | 278 | [System.IO.Compression.ZipFile]::ExtractToDirectory($ZipFilePath, $OutputFolderPath) 279 | } 280 | 281 | function Global:New-Folder { 282 | <# 283 | .SYNOPSIS 284 | Creates a new folder item and logs the event to (global) $ItemCreationLogFile 285 | .INPUTS 286 | 1. Path to the parent folder 287 | 2. Name of the new folder 288 | 3. Whether to return the full physical path of the new folder or not 289 | .OUTPUTS 290 | Full physical path of the new folder (on demand) 291 | #> 292 | [CmdletBinding()] 293 | param ( 294 | [Parameter(Mandatory = $true)] 295 | [ValidateNotNullOrEmpty()] 296 | [String] 297 | $ParentPath, 298 | 299 | [Parameter(Mandatory = $true)] 300 | [ValidateNotNullOrEmpty()] 301 | [String] 302 | $FolderName, 303 | 304 | [Parameter(Mandatory = $true)] 305 | [ValidateNotNullOrEmpty()] 306 | [Boolean] 307 | $ReturnFolderPath 308 | ) 309 | 310 | $folder = New-Item -Path $ParentPath -Name $FolderName -ItemType Directory -Force 311 | $folder | Out-File -append $ItemCreationLogFile 312 | 313 | if ($ReturnFolderPath) { 314 | $folder.ToString() 315 | } 316 | } 317 | 318 | function Global:New-File { 319 | <# 320 | .SYNOPSIS 321 | Creates a new file item and logs the event to (global) $ItemCreationLogFile 322 | .INPUTS 323 | 1. Path to the parent folder 324 | 2. Name of the new file 325 | 3. Whether to return the full physical path of the new file or not 326 | .OUTPUTS 327 | Full physical path of the new file (on demand) 328 | #> 329 | [CmdletBinding()] 330 | param ( 331 | [Parameter(Mandatory = $true)] 332 | [ValidateNotNullOrEmpty()] 333 | [String] 334 | $ParentPath, 335 | 336 | [Parameter(Mandatory = $true)] 337 | [ValidateNotNullOrEmpty()] 338 | [String] 339 | $FileName, 340 | 341 | [Parameter(Mandatory = $true)] 342 | [ValidateNotNullOrEmpty()] 343 | [Boolean] 344 | $ReturnFilePath 345 | ) 346 | 347 | $file = New-Item -Path $ParentPath -Name $FileName -ItemType File -Force 348 | $file | Out-File -append $ItemCreationLogFile 349 | 350 | if ($ReturnFilePath) { 351 | $file.ToString() 352 | } 353 | } 354 | 355 | function Global:Delete-Item { 356 | <# 357 | .SYNOPSIS 358 | Deletes a file or folder and all of its contents (use at clean-up time) 359 | .INPUTS 360 | Full physical path of the file or folder 361 | .OUTPUTS 362 | None 363 | #> 364 | [CmdletBinding()] 365 | param ( 366 | [Parameter(Mandatory = $true)] 367 | [ValidateNotNullOrEmpty()] 368 | [String] 369 | $PathToDelete 370 | ) 371 | Remove-Item $PathToDelete -Recurse -ErrorAction Ignore 372 | } 373 | 374 | function Global:Get-RandomPassword { 375 | ([char[]]([char]97..[char]122) + [char[]]([char]65..[char]90) + 0..9 | sort {Get-Random})[0..24] -join '' 376 | } 377 | 378 | function Global:Exit-WithError { 379 | exit 1 380 | } 381 | 382 | function Global:Exit-WithoutError { 383 | exit 0 384 | } 385 | 386 | # Types of log message 387 | $Global:InfoMsg = "Info" 388 | $Global:DebugMsg = "Debug" 389 | $Global:ErrorMsg = "Error" 390 | $Global:FatalMsg = "FATAL" 391 | # use this to exclude sensitive data from logs 392 | $Global:ConsoleOnlyMsg = "ConsoleOnly" 393 | 394 | # AWS EB Migration Support 395 | $Global:SupportTeamAWSRegion = "" 396 | 397 | function Global:Write-Log { 398 | <# 399 | .NOTES 400 | DO NOT USE THIS FUNCTION DIRECTLY - use New-Message 401 | except when the log files are not initialized yet 402 | .SYNOPSIS 403 | This function writes a log line into specified log file 404 | .INPUTS 405 | 1. Full physical path of the log file. It must be initialized before this function call 406 | 2. Type of the message ($InfoMsg, $DebugMsg, $ErrorMsg, or $FatalMsg) 407 | 3. The log message (a string) 408 | .OUTPUTS 409 | None 410 | #> 411 | [CmdletBinding()] 412 | param ( 413 | [Parameter(Mandatory = $true)] 414 | [String] 415 | $LogFilePath, 416 | 417 | [Parameter(Mandatory = $true)] 418 | [ValidateSet("Info", "Debug", "Error", "FATAL")] 419 | [String] 420 | $MessageType, 421 | 422 | [Parameter(Mandatory = $true)] 423 | [String] 424 | $LogMessage 425 | ) 426 | 427 | Verify-PathExists $LogFilePath 428 | 429 | $timeStampNow = [datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss.fff') 430 | $fullMessage = "$timeStampNow : $MessageType : $LogMessage" 431 | 432 | Add-Content $LogFilePath -Value $fullMessage 433 | } 434 | 435 | function Global:Get-SessionObjectFilePath ($ID) { 436 | $objectFileName = $ID + ".xml" 437 | $sessionFolderPath = Join-Path $MigrationRunDirPath $SessionFolderName 438 | Join-Path $sessionFolderPath $objectFileName 439 | } 440 | 441 | function Global:New-SessionFolder { 442 | <# 443 | .SYNOPSIS 444 | This function needs to be invoked before any function in this file is called. 445 | It creates a folder under the current migration run folder to contain session resumability files. 446 | The name of the folder needs to be defined in the global scope as $SessionFolderName 447 | .INPUTS 448 | None 449 | .OUTPUTS 450 | None 451 | .NOTES 452 | Please only create the folder right before you write the first set of session data 453 | Then you can use Test-SessionFolderExists to identify if a migration run is resumable 454 | #> 455 | Verify-PathExists $MigrationRunDirPath 456 | 457 | $sessionFolderPath = Join-Path $MigrationRunDirPath $SessionFolderName 458 | 459 | if (-Not (Test-Path $sessionFolderPath)) { 460 | New-Folder $MigrationRunDirPath $SessionFolderName $False 461 | } 462 | } 463 | 464 | function Global:Test-SessionFolderExists { 465 | <# 466 | .SYNOPSIS 467 | Tests if the session folder exists for ANY migration run 468 | .INPUTS 469 | Migration run ID 470 | .OUTPUTS 471 | Boolean 472 | #> 473 | [CmdletBinding()] 474 | param ( 475 | [Parameter(Mandatory = $true)] 476 | [ValidateNotNullOrEmpty()] 477 | [String] 478 | $MigrationRunID 479 | ) 480 | 481 | $testMigrationRunDirPath = Join-Path $MigrationRunsDirPath $MigrationRunID 482 | if (Test-Path $testMigrationRunDirPath) { 483 | $testSessionFolderPath = Join-Path $testMigrationRunDirPath $sessionFolderName 484 | Test-Path $testSessionFolderPath 485 | return 486 | } 487 | 488 | $False 489 | } 490 | 491 | function Global:Save-SessionObject { 492 | <# 493 | .SYNOPSIS 494 | This function serializes any PS object into a new or existing file under the session folder 495 | .INPUTS 496 | 1. the PS object to serialize 497 | 2. NAME (not path - and without extension) of the file to store the data 498 | .OUTPUTS 499 | None 500 | .EXAMPLE 501 | Save-SessionObject $iisSitesConfigStage "iis_sites_config_stage" 502 | #> 503 | [CmdletBinding()] 504 | param ( 505 | [Parameter(Mandatory = $true)] 506 | [ValidateNotNullOrEmpty()] 507 | [PSObject] 508 | $Object, 509 | 510 | [Parameter(Mandatory = $true)] 511 | [ValidateNotNullOrEmpty()] 512 | [String] 513 | $Name 514 | ) 515 | 516 | $objectFilePath = Get-SessionObjectFilePath $Name 517 | if (Test-Path $objectFilePath) { 518 | Remove-Item $objectFilePath 519 | } 520 | 521 | $serializedString = [System.Management.Automation.PSSerializer]::Serialize($Object) 522 | $serializedString | Out-File $objectFilePath 523 | } 524 | 525 | function Global:Restore-SessionObject { 526 | <# 527 | .SYNOPSIS 528 | This function restores any previously saved PS object 529 | .INPUTS 530 | NAME (not path - and without extension) of the file that stores the data 531 | .OUTPUTS 532 | PS object 533 | .EXAMPLE 534 | $iisSitesConfigStage = Restore-sessionObject "iis_sites_config_stage" 535 | #> 536 | [CmdletBinding()] 537 | param ( 538 | [Parameter(Mandatory = $true)] 539 | [ValidateNotNullOrEmpty()] 540 | [String] 541 | $Name 542 | ) 543 | 544 | $objectFilePath = Get-SessionObjectFilePath $Name 545 | if (-Not (Test-Path $objectFilePath)) { 546 | throw "Resumption Failed. Cannot find session file $objectFilePath" 547 | } 548 | 549 | $serializedString = Get-Content $objectFilePath 550 | [System.Management.Automation.PSSerializer]::Deserialize($serializedString) 551 | } 552 | 553 | <# 554 | Global Variables Defined in Setup-Workspace: 555 | $SessionFolderName 556 | $MigrationRunsDirPath 557 | 558 | Global Variables Defined in Setup-NewMigrationRun: 559 | $CurrentMigrationRunPath 560 | $LogFolderPath 561 | $ItemCreationLogFile 562 | $MigrationRunLogFile 563 | $EnvironmentInfoLogFile 564 | #> 565 | 566 | function Global:Setup-Workspace { 567 | <# 568 | .SYNOPSIS 569 | Call this function before anything else 570 | .INPUTS 571 | None 572 | .OUTPUTS 573 | None 574 | #> 575 | 576 | # Global & universal variables for all migration runs 577 | 578 | $Global:SessionFolderName = "session" 579 | $Global:MigrationRunsDirPath = Join-Path $runDirectory "runs" 580 | 581 | # need to test role first otherwise log file/folder creation may fail 582 | 583 | if (-Not (Test-PowerShellSessionRole -Role Administrator)) { 584 | Write-Host "Error: Run the migration assistant as Administrator." 585 | Exit-WithError 586 | } 587 | } 588 | 589 | function Global:Setup-NewMigrationRun { 590 | <# 591 | .SYNOPSIS 592 | Call this function to set up the workspace for a new migration run 593 | .INPUTS 594 | Migration run ID 595 | .OUTPUTS 596 | None 597 | #> 598 | [CmdletBinding()] 599 | param ( 600 | [Parameter(Mandatory = $true)] 601 | [ValidateNotNullOrEmpty()] 602 | [String] 603 | $MigrationRunID 604 | ) 605 | 606 | # Create the main runs folder if it doesn't exist 607 | if (-Not (Test-Path $MigrationRunsDirPath)) { 608 | New-Item -Path $MigrationRunsDirPath -Force -ItemType Directory | Out-Null 609 | } 610 | 611 | $Global:CurrentMigrationRunPath = Join-Path $MigrationRunsDirPath $MigrationRunId 612 | $Global:LogFolderPath = Join-Path $CurrentMigrationRunPath "logs" 613 | 614 | if (Test-Path -Path $CurrentMigrationRunPath) { 615 | Write-Host "Error: $MigrationRunId already exists. Run the migration assistant again." 616 | Exit-WithError 617 | } 618 | 619 | try { 620 | 621 | $currentMigrationRunPathObj = New-Item -Path $CurrentMigrationRunPath -Force -ItemType Directory 622 | $logFolderPathObj = New-Item -Path $LogFolderPath -Force -ItemType Directory 623 | 624 | $itemCreationLogFileName = "item_creation.log" 625 | $itemCreationLogFileObj = New-Item -Path $LogFolderPath -Name $itemCreationLogFileName -ItemType File 626 | $Global:ItemCreationLogFile = $itemCreationLogFileObj.ToString() 627 | 628 | $currentMigrationRunPathObj | Out-File -append $ItemCreationLogFile 629 | $logFolderPathObj | Out-File -append $ItemCreationLogFile 630 | $itemCreationLogFileObj | Out-File -append $ItemCreationLogFile 631 | 632 | $migrationRunLogFileName = "migration_run.log" 633 | $Global:MigrationRunLogFile = New-File $LogFolderPath $migrationRunLogFileName $True 634 | 635 | # does not initiate this log file - Write-IISServerInfo will make the file 636 | $Global:EnvironmentInfoLogFile = Join-Path $LogFolderPath "environment_info.log" 637 | 638 | } catch { 639 | Write-Host "Error: Can't create required items. Be sure you have write permission on the 'runs' folder." 640 | Exit-WithError 641 | } 642 | } 643 | 644 | function Global:Invoke-CommandsWithRetry { 645 | <# 646 | .SYNOPSIS 647 | This function automatically repeats the input script block when an exception is thrown within retry # limit 648 | Before repeating, the exception will be shown in the console as an error message 649 | Also logs the exception message into the specified log file 650 | If the max number of retry is reached, an exception will be thrown 651 | .INPUTS 652 | 1. Number of max retries 653 | 1. Full physical path of the log file 654 | 2. The script block to execute 655 | .OUTPUTS 656 | None 657 | .EXAMPLE 658 | $Global:myGlobalVar = "27" 659 | Invoke-CommandsWithRetry 3 $logFile { 660 | $string = Get-UserInputString $logFile "Type anything other than 27" 661 | if ($string -eq "27") { 662 | throw "That's the only number that doesn't work!" 663 | } 664 | $Global:myGlobalVar = $string 665 | } 666 | Echo $myGlobalVar # console will print the string user typed in 667 | .NOTES 668 | 1. if you declare any variable within the script block, it will not be accessible outside unless made global 669 | #> 670 | [CmdletBinding()] 671 | param ( 672 | [Parameter(Mandatory = $true)] 673 | [ValidateNotNullOrEmpty()] 674 | [Int] 675 | $MaxRetryNumber, 676 | 677 | [Parameter(Mandatory = $true)] 678 | [ValidateNotNullOrEmpty()] 679 | [String] 680 | $LogFilePath, 681 | 682 | [Parameter(Mandatory=$true)] 683 | [ScriptBlock]$ScriptBlock 684 | ) 685 | 686 | Begin { 687 | $retryCount = -1 688 | } 689 | 690 | Process { 691 | do { 692 | $retryCount++ 693 | try { 694 | $ScriptBlock.Invoke() 695 | return 696 | } catch { 697 | $lastExceptionMessage = $error[0].Exception.Message 698 | $innerExceptionMessage = $lastExceptionMessage.Replace("Exception calling `"Invoke`" with `"0`" argument(s): ", "") 699 | New-Message $ErrorMsg $innerExceptionMessage $LogFilePath 700 | New-Message $InfoMsg "Retrying command." $LogFilePath 701 | } 702 | } while ($retryCount -lt $MaxRetryNumber) 703 | 704 | $logLine = "Max retry number exceeded for script block: " + $ScriptBlock.ToString() 705 | New-Message $DebugMsg $logLine $LogFilePath 706 | 707 | throw "Max retry number exceeded." 708 | } 709 | } 710 | 711 | function Global:Get-UserFacingMessage ($Message) { 712 | 713 | $messagePrefix = "[AWS Migration] " 714 | 715 | if ($DisplayTimestampsInConsole) { 716 | $timestampNow = [datetime]::Now.ToString('yyyy-MM-dd HH:mm') 717 | $messagePrefix = "[$timestampNow] " 718 | } 719 | 720 | if ($Message) { 721 | $messagePrefix + $Message 722 | } else { 723 | $messagePrefix 724 | } 725 | } 726 | 727 | function Global:New-Message { 728 | <# 729 | .SYNOPSIS 730 | This function generates a new message. Depending on the message type, it 731 | 1. displays this message to the user via the console (with timestamp, when $DisplayTimestampsInConsole is on) 732 | 2. stores the message as a new log line (with timestamp) 733 | .INPUTS 734 | 1. Type of message ($InfoMsg, $DebugMsg, $ErrorMsg, or $FatalMsg) 735 | a. $InfoMsg: the message will go to the user & log file 736 | b. $DebugMsg: the message will only go to the log file 737 | c. $ErrorMsg: the message will go to the user & log file. Additional information is added to user facing message 738 | d. $FatalMsg: the message will go to the user & log file. Additional information is added to user facing message 739 | 2. The message itself (a string) 740 | 2. Full physical path of the log file. It must be initialized before this function call 741 | .OUTPUTS 742 | None 743 | #> 744 | [CmdletBinding()] 745 | param ( 746 | [Parameter(Mandatory = $true)] 747 | [ValidateSet("Info", "Debug", "Error", "FATAL", "ConsoleOnly")] 748 | [String] 749 | $MessageType, 750 | 751 | [Parameter(Mandatory = $true)] 752 | [String] 753 | $Message, 754 | 755 | [Parameter(Mandatory = $true)] 756 | [String] 757 | $LogFilePath 758 | ) 759 | 760 | if ($MessageType -ne "ConsoleOnly") { 761 | Write-Log $LogFilePath $MessageType $Message 762 | } 763 | 764 | $userFacingMessage = $Message 765 | 766 | if (($MessageType -eq "Error") -or ($MessageType -eq "FATAL")) { 767 | $userFacingPrefix = "[$MessageType] " 768 | $fullTimeStampNow = [datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss.fff') 769 | $userFacingSuffix = " (at $fullTimeStampNow)" 770 | $userFacingMessage = $userFacingPrefix + $userFacingMessage + $userFacingSuffix 771 | } 772 | 773 | $color = "White" 774 | switch ($MessageType) { 775 | "Error" { $color = "Yellow"; break } 776 | "FATAL" { $color = "Red"; break } 777 | default { break } 778 | } 779 | 780 | if ($MessageType -ne "Debug") { 781 | $messageToDisplay = Get-UserFacingMessage $userFacingMessage 782 | Write-Host $messageToDisplay -ForegroundColor $color 783 | } 784 | } 785 | 786 | function Global:Get-UserInputString { 787 | <# 788 | .SYNOPSIS 789 | This function does the following things: 790 | 1. display an optional prompt message to the user (with timestamp, when $DisplayTimestampsInConsole is on) 791 | 2. collect and return the text input from the user as a string 792 | 3. add the user input to the specified log file 793 | .INPUTS 794 | 1. full physical path to the log file 795 | 2. prompt message (optional) - please do not include ":" 796 | .OUTPUTS 797 | User input as a string 798 | #> 799 | [CmdletBinding()] 800 | param ( 801 | [Parameter(Mandatory = $true)] 802 | [String] 803 | $LogFilePath, 804 | 805 | [Parameter(Mandatory = $false)] 806 | [String] 807 | $PromptMessage 808 | ) 809 | 810 | if ($PromptMessage) { 811 | $promptMessageLogLine = "UserInterface-Prompt : " + $PromptMessage 812 | Write-Log $LogFilePath $InfoMsg $promptMessageLogLine 813 | } 814 | 815 | $userFacingPromptMessage = Get-UserFacingMessage $PromptMessage 816 | 817 | $userInput = Read-Host -Prompt $userFacingPromptMessage 818 | 819 | $userInputLogLine = "UserInterface-Input : " + $userInput 820 | 821 | Write-Log $LogFilePath $InfoMsg $userInputLogLine 822 | 823 | $userInput 824 | } 825 | 826 | function Global:Get-SensitiveUserInputString { 827 | <# 828 | .SYNOPSIS 829 | This function does the following things: 830 | 1. display an optional prompt message to the user (with timestamp, when $DisplayTimestampsInConsole is on) 831 | 2. collect and return the text input from the user as a string 832 | 3. Logs only the prompt, NOT the user input 833 | .INPUTS 834 | 1. full physical path to the log file 835 | 2. prompt message (optional) - please do not include ":" 836 | .OUTPUTS 837 | User input as a string 838 | #> 839 | [CmdletBinding()] 840 | param ( 841 | [Parameter(Mandatory = $true)] 842 | [String] 843 | $LogFilePath, 844 | 845 | [Parameter(Mandatory = $false)] 846 | [String] 847 | $PromptMessage 848 | ) 849 | 850 | if ($PromptMessage) { 851 | $promptMessageLogLine = "UserInterface-Prompt-SensitiveInput : " + $PromptMessage 852 | Write-Log $LogFilePath $InfoMsg $promptMessageLogLine 853 | } 854 | 855 | $userFacingPromptMessage = Get-UserFacingMessage $PromptMessage 856 | Read-Host -Prompt $userFacingPromptMessage 857 | } 858 | 859 | function Global:Append-DotsToLatestMessage { 860 | <# 861 | .SYNOPSIS 862 | This function appends a number of dots to the last message displayed in the console 863 | .INPUTS 864 | Number of dots to add 865 | .OUTPUTS 866 | None 867 | #> 868 | [CmdletBinding()] 869 | param ( 870 | [Parameter(Mandatory = $true)] 871 | [ValidateNotNullOrEmpty()] 872 | [Int] 873 | $NumberOfDots 874 | ) 875 | 876 | if ($NumberOfDots -lt 0) { 877 | throw "Error: cannot add negative number of dots to the message." 878 | } 879 | $numberAdded = 0; 880 | while ($numberAdded -lt $NumberOfDots) { 881 | Write-Host -NoNewline "." 882 | $numberAdded ++ 883 | } 884 | } 885 | 886 | function Global:Show-SpinnerAnimation { 887 | <# 888 | .SYNOPSIS 889 | This function shows a spinner animation in the PowerShell console 890 | .INPUTS 891 | Seconds to display the animation 892 | .OUTPUTS 893 | None 894 | .EXAMPLE 895 | $job = Start-Process $yourProcess 896 | while ($job.Running) { Show-SpinnerAnimation 3 } 897 | Write-Host " " 898 | .NOTES 899 | You can call a this function multiple times in a roll to have a continuous spin 900 | After you are done with spinning, please invoke 'Write-Host " "' to make a new line 901 | #> 902 | [CmdletBinding()] 903 | param ( 904 | [Parameter(Mandatory = $true)] 905 | [ValidateNotNullOrEmpty()] 906 | [Int] 907 | $DurationInSeconds 908 | ) 909 | 910 | $prefix = Get-UserFacingMessage 911 | $spinner = "/-\|/-\|" 912 | $frame = 0 913 | 914 | $count = 0 915 | $limit = 10 * $DurationInSeconds # because 100 milliseconds per frame 916 | 917 | $originalPosition = $Host.UI.RawUI.CursorPosition 918 | 919 | while ($count -le $limit) { 920 | $Host.UI.RawUI.CursorPosition = $originalPosition 921 | $currentFrame = "`r$prefix" + $spinner[$frame] 922 | 923 | Write-Host -NoNewline $currentFrame 924 | Start-Sleep -Milliseconds 100 925 | 926 | $frame++ 927 | $count++ 928 | 929 | if ($frame -ge $spinner.Length) { 930 | $frame = 0 931 | } 932 | } 933 | 934 | $Host.UI.RawUI.CursorPosition = $originalPosition 935 | } 936 | 937 | function Global:Get-IISServerInfoObject() 938 | { 939 | <# 940 | .SYNOPSIS 941 | Get list of objects with details of each website. 942 | .INPUTS 943 | None 944 | .OUTPUTS 945 | Returns object containing top-level IIS server information 946 | objects from IIS applicationHost.config and administration.config. 947 | IIS configurations are at %windir%\windows\system32\inetsrv\config 948 | 949 | Return object contains these objects: 950 | +--- Computer Name 951 | +--- OS Version 952 | +--- gac 953 | +--- applicationHost 954 | +--- webServer 955 | +--- ftpServer 956 | +--- location 957 | +--- IIS versions 958 | +--- appHost 959 | +--- location 960 | +--- admWebServer 961 | +--- admModuleProviders 962 | .EXAMPLE 963 | $serverObj = Get-IISServerInfoObjects 964 | #> 965 | 966 | [CmdletBinding()] 967 | [OutputType([psobject])] 968 | param() 969 | 970 | $windir = $Env:Windir 971 | [hashtable]$resultObject = @{} 972 | 973 | $computer = Get-WmiObject -Class Win32_ComputerSystem 974 | $osVersion = $([System.Environment]::OSVersion.Version) 975 | 976 | $resultObject.Add('computerName', $computer.name) 977 | $resultObject.Add('osVersion', $osVersion) 978 | 979 | $APP_HOST_CFG_FILE_PATH = $windir + "\system32\inetsrv\config\applicationHost.config" 980 | $APP_HOST_XML_PATH = "configuration/system.applicationHost" 981 | $WEB_SERVER_XML_PATH = "configuration/system.webServer" 982 | $FTP_SERVER_XML_PATH = "configuration/system.ftpServer" 983 | $LOCATION_XML_PATH = "configuration/location" 984 | $INETSRV_WEBADMIN_DLL= $windir + "\system32\inetsrv\Microsoft.Web.Administration.dll" 985 | 986 | $ADM_CFG_FILE_PATH = $windir + "\system32\inetsrv\config\administration.config" 987 | $ADM_WEBSERVER_XML_PATH = "configuration/system.webServer" 988 | $ADM_MODULE_PROVIDERS_XML_PATH = "configuration/moduleProviders" 989 | 990 | if (!(Test-Path $APP_HOST_CFG_FILE_PATH -PathType Leaf)) { 991 | Write-Output $APP_HOST_CFG_FILE_PATH 992 | throw "ERROR: Cannot get Application Host Config" 993 | } 994 | if (!(Test-Path $INETSRV_WEBADMIN_DLL -PathType Leaf)) { 995 | Write-Output $INETSRV_WEBADMIN_DLL 996 | throw "ERROR: Cannot get web admin dll" 997 | } 998 | 999 | $appHost = Select-Xml -Path $APP_HOST_CFG_FILE_PATH -XPath $APP_HOST_XML_PATH | Select-Object -ExpandProperty Node 1000 | $resultObject.Add('appHost', $appHost) 1001 | 1002 | $gac=[System.Reflection.Assembly]::LoadFrom($INETSRV_WEBADMIN_DLL) 1003 | $resultObject.Add('gac', $gac) 1004 | 1005 | $webServer = Select-Xml -Path $APP_HOST_CFG_FILE_PATH -XPath $WEB_SERVER_XML_PATH | Select-Object -ExpandProperty Node 1006 | $resultObject.Add('webServer', $webServer) 1007 | 1008 | $ftpServer = Select-Xml -Path $APP_HOST_CFG_FILE_PATH -XPath $FTP_SERVER_XML_PATH | Select-Object -ExpandProperty Node 1009 | $resultObject.Add('ftpServer', $ftpServer) 1010 | 1011 | $iisVersions = get-itemproperty HKLM:\SOFTWARE\Microsoft\InetStp\ | select setupstring,versionstring 1012 | $resultObject.Add('iisVersion', $iisVersions) 1013 | 1014 | $location = Select-Xml -Path $APP_HOST_CFG_FILE_PATH -XPath $LOCATION_XML_PATH | Select-Object -ExpandProperty Node 1015 | $resultObject.Add('location', $location) 1016 | 1017 | $admWebServer = Select-Xml -Path $ADM_CFG_FILE_PATH -XPath $ADM_WEBSERVER_XML_PATH | Select-Object -ExpandProperty Node 1018 | $resultObject.Add('admWebServer', $admWebServer) 1019 | 1020 | $admModuleProviders = Select-Xml -Path $ADM_CFG_FILE_PATH -XPath $ADM_MODULE_PROVIDERS_XML_PATH | Select-Object -ExpandProperty Node 1021 | $resultObject.Add('admModuleProviders', $admModuleProviders) 1022 | 1023 | return $resultObject 1024 | } 1025 | 1026 | function Global:Write-IISServerInfo() 1027 | { 1028 | <# 1029 | .SYNOPSIS 1030 | Write IIS server info to log file 1031 | .INPUTS 1032 | Output file name, website object 1033 | .OUTPUTS 1034 | None 1035 | #> 1036 | [CmdletBinding()] 1037 | [OutputType([psobject])] 1038 | param( 1039 | [Parameter(Mandatory = $true)] 1040 | [ValidateNotNullOrEmpty()] 1041 | [String] 1042 | $outputFileName 1043 | ) 1044 | 1045 | $output = "" 1046 | 1047 | $serverObj = Get-IISServerInfoObject 1048 | 1049 | $output += "Computer Name: $($serverObj["computerName"])`r`n`r`n" 1050 | $output += "OS Version: $($serverObj["osVersion"])`r`n`r`n" 1051 | 1052 | if (Test-Path -Path $outputFileName) { 1053 | throw "ERROR: $outputFileName exists" 1054 | } 1055 | 1056 | foreach ($site in $serverObj['appHost'].sites.site) { 1057 | $output += "Website name : $($site.name), id : $($site.id), serverAutoStart status : $($site.serverAutoStart)`r`n" 1058 | foreach ($node in $site.bindings.ChildNodes) { 1059 | if ($node.protocol -ne $null) { 1060 | $output += "`tProtocol : $($node.protocol), Binding info : $($node.bindingInformation)`r`n" 1061 | } 1062 | } 1063 | $output += "`tApplication virtualDirectory path = $($site.application.virtualDirectory.path), physicalPath = $($site.application.virtualDirectory.physicalPath)" 1064 | $exists = $site.application.applicationPool 1065 | if ($exists) { 1066 | $output += ", applicationPool = $($site.application.applicationPool)" 1067 | } 1068 | $output += "`r`n" 1069 | } 1070 | 1071 | $security = $serverObj['webServer'].security 1072 | if ($security.authentication) { 1073 | $output += "Anonymous Authentication : $($security.authentication.anonymousAuthentication.enabled)`r`n" 1074 | $output += "Basic Authentication : $($security.authentication.basicAuthentication.enabled)`r`n" 1075 | $output += "Digest Authentication : $($security.authentication.digestAuthentication.enabled)`r`n" 1076 | $output += "IIS Client Certificate Mapping Authentication : $($security.authentication.iisClientCertificateMappingAuthentication.enabled)`r`n" 1077 | $output += "Windows Authentication : $($security.authentication.windowsAuthentication.enabled)`r`n" 1078 | } 1079 | $gac = $serverObj['gac'] 1080 | $output += "GAC enable status : $($gac.GlobalAssemblyCache)`r`n" 1081 | $iisVersions = $serverObj['iisVersion'] 1082 | $output += "IIS version : $($iisVersions.SetupString)`r`n" 1083 | 1084 | $appPools = $serverObj['appHost'].applicationPools 1085 | foreach ($item in $appPools.childnodes ) { 1086 | $printStr = "" 1087 | if ($item.name -ne "#whitespace") { 1088 | $printStr = "App Pool Name : $($item.name)" 1089 | $exists = $item.managedPipelineMode 1090 | if ($exists) { 1091 | $printstr += ", Managed pipeline mode : $($item.managedPipelineMode)" 1092 | } 1093 | $exists = $item.managedRuntimeVersion 1094 | if ($exists) { 1095 | $printstr += ", Managed runtime version : $($item.managedRuntimeVersion)" 1096 | } 1097 | $printstr += "`r`n" 1098 | } 1099 | if ($printStr -ne "") { 1100 | $output += $printStr 1101 | } 1102 | } 1103 | 1104 | $location = $serverObj['location'] 1105 | foreach ($item in $location.childNodes ) { 1106 | if ($item.name -ne "#whitespace" ) { 1107 | foreach ($module in $item.modules.add) { 1108 | $output += "Module name: $($module.name)`r`n" 1109 | } 1110 | } 1111 | } 1112 | 1113 | $admWebServer = $serverObj['admWebServer'] 1114 | foreach ($providers in $admWebServer.management.authentication) { 1115 | foreach ($node in $providers.ChildNodes) { 1116 | if ($node.add.type) { 1117 | $output += "Authentication provider name : $($node.add.name), type : $($node.add.type)`r`n" 1118 | } 1119 | } 1120 | } 1121 | foreach ($providers in $admWebServer.management.authorization) { 1122 | foreach ($node in $providers.ChildNodes) { 1123 | if ($node.add.type) { 1124 | $output += "Adm Authorization provider name : $($node.add.name), type : $($node.add.type)`r`n" 1125 | } 1126 | } 1127 | } 1128 | 1129 | $admModuleProviders = $serverObj['admModuleProviders'] 1130 | foreach ($module in $admModuleProviders.add) { 1131 | $output += "Adm module provider name : $($module.name), type = $($module.type)`r`n" 1132 | } 1133 | 1134 | $output | out-file $outputFileName 1135 | } 1136 | 1137 | function Global:Get-MissingDependencies { 1138 | <# 1139 | .SYNOPSIS 1140 | This function checks for missing depencencies of the migration assistant on the local server 1141 | .INPUTS 1142 | None 1143 | .OUTPUTS 1144 | A string of the names of the missing dependencies 1145 | #> 1146 | [CmdletBinding()] 1147 | param() 1148 | 1149 | $missingDependencies = "" 1150 | 1151 | # check for PowerShell 3.0+ 1152 | if (!($PSVersionTable.PSVersion.Major -ge 3)) { 1153 | $missingDependencies += "PowerShell 3.0 or above, " 1154 | } 1155 | 1156 | # check for IIS 1157 | if (!(Get-Service w3svc -ErrorAction SilentlyContinue)) { 1158 | $missingDependencies += "Internet Information Services (IIS), " 1159 | } 1160 | 1161 | # check for Web Deploy (v3+) 1162 | if(!(Test-Path "HKLM:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\3")) { 1163 | $missingDependencies += "Microsoft Web Deploy v3, " 1164 | } 1165 | 1166 | # check for AWSPowerShell 1167 | if (!(Get-Module -ListAvailable -Name "AWSPowerShell")) { 1168 | $missingDependencies += "AWSPowerShell, " 1169 | } 1170 | 1171 | # check for WebAdministration 1172 | if (!(Get-Module -ListAvailable -Name "WebAdministration")) { 1173 | $missingDependencies += "WebAdministration, " 1174 | } 1175 | 1176 | if ($missingDependencies) { 1177 | return $missingDependencies.Substring(0, $missingDependencies.Length - 2) 1178 | } 1179 | return $Null 1180 | } 1181 | 1182 | function Global:Get-AppHostSchemaPackage ($DestinationFilePath) { 1183 | $msDeploy = Get-WebDeployV3Exe 1184 | $argVerb = "-verb:sync" 1185 | $argSource = "-source:appHostSchema" 1186 | $argDest = "-dest:package='" + $DestinationFilePath + "'" 1187 | 1188 | [String[]] $argList = @( 1189 | $argVerb, 1190 | $argSource, 1191 | $argDest 1192 | ) 1193 | $process = Start-Process $msDeploy -ArgumentList $argList -NoNewWindow -Wait -PassThru 1194 | if ($process.ExitCode -ne 0) { 1195 | throw "ERROR: Failed to package appHostSchema using Web Deploy" 1196 | } 1197 | } 1198 | 1199 | function Global:Get-AppHostConfigXML { 1200 | $appHostConfigFile = Get-WebConfigFile 1201 | [XML] $appHostConfigXML = Get-Content $appHostConfigFile 1202 | $appHostConfigXML 1203 | } 1204 | 1205 | function Global:Get-WebServerConfigXMLString { 1206 | $appHostConfigXML = Get-AppHostConfigXML 1207 | $webServerXML = $appHostConfigXML.configuration."system.webServer" 1208 | if ($webServerXML -eq $null) { 1209 | throw "ERROR: Cannot find in applicationHost.config file" 1210 | } 1211 | $webServerXML.OuterXML 1212 | } 1213 | 1214 | function Global:Get-SitesConfigXMLString ($SiteName) { 1215 | $appHostConfigXML = Get-AppHostConfigXML 1216 | $sitesXML = $appHostConfigXML.configuration."system.applicationHost".sites 1217 | $nameMatchingString = "site name=`"" + $SiteName + "`"" 1218 | 1219 | foreach ($siteXML in $sitesXML.ChildNodes) { 1220 | $siteXMLString = $siteXML.OuterXML 1221 | if ($siteXMLString -match $nameMatchingString) { 1222 | return $siteXMLString 1223 | } 1224 | } 1225 | throw "ERROR: Cannot find site $SiteName" 1226 | } 1227 | 1228 | function Global:Collect-IISConfigs { 1229 | <# 1230 | .SYNOPSIS 1231 | This function collects IIS configurations of the server and the site that Web Deploy's iisApp provider does not package 1232 | .INPUTS 1233 | 1. SiteName: string, name of the site 1234 | 2. DestinationFolderPath: string, path to an existing & empty folder 1235 | .OUTPUTS 1236 | 3 files will be generated in the destination folder: 1237 | 1. appHostSchema.zip: any custom iis schema the user has - as a Web Deploy package 1238 | 2. siteConfig.xml: the section of the applicationHostConfig.xml 1239 | 3. webServerConfig.xml: the section of the applicationHostConfig.xml 1240 | .EXAMPLE 1241 | Collect-IISConfigs "NopCommerce380" "C:\dest" 1242 | #> 1243 | [CmdletBinding()] 1244 | param ( 1245 | [Parameter(Mandatory = $true)] 1246 | [ValidateNotNullOrEmpty()] 1247 | [String] 1248 | $SiteName, 1249 | 1250 | [Parameter(Mandatory = $true)] 1251 | [ValidateNotNullOrEmpty()] 1252 | [String] 1253 | $DestinationFolderPath 1254 | ) 1255 | 1256 | Verify-FolderExistsAndEmpty $DestinationFolderPath 1257 | 1258 | $appHostSchemaPackagePath = $DestinationFolderPath + "\appHostSchema.zip" 1259 | $webServerConfigOutputPath = $DestinationFolderPath + "\webServerConfig.xml" 1260 | $sitesConfigOutputPath = $DestinationFolderPath + "\siteConfig.xml" 1261 | 1262 | Get-AppHostSchemaPackage $appHostSchemaPackagePath 1263 | Get-WebServerConfigXMLString | Set-Content -Path $webServerConfigOutputPath 1264 | Get-SitesConfigXMLString $SiteName | Set-Content -Path $sitesConfigOutputPath 1265 | } 1266 | 1267 | function Global:Get-CheckAppPool { 1268 | <# 1269 | .SYNOPSIS 1270 | This function checks if a site uses multiple application pools. 1271 | .INPUTS 1272 | Site Name 1273 | .OUTPUTS 1274 | Return readiness object 1275 | #> 1276 | [CmdletBinding()] 1277 | param ( 1278 | [Parameter(Mandatory = $true)] 1279 | [ValidateNotNullOrEmpty()] 1280 | [String] 1281 | $SiteName 1282 | ) 1283 | $NumAppsPerPool = 0 1284 | $Apps = Get-WebApplication -Site $SiteName | sort -property ApplicationPool 1285 | $Count = 0 1286 | $MaxCount = 0 1287 | $CurrApplicationPool = "" 1288 | foreach ($App in $Apps) { 1289 | if ($CurrApplicationPool -ne $App.applicationPool) { 1290 | $Count = 1 1291 | $CurrApplicationPool = $App.applicationPool 1292 | } else { 1293 | $Count = $Count + 1 1294 | } 1295 | if ($Count -gt $MaxCount) { 1296 | $MaxCount = $Count 1297 | } 1298 | } 1299 | 1300 | $AppPoolCheck = [ordered]@{ 1301 | "Description"= "Checks if any site has multiple application pools." 1302 | "Log" = "Found maximum number of applications per pool = $MaxCount, no issues found" 1303 | "Result" = $true 1304 | } 1305 | 1306 | return $AppPoolCheck 1307 | } 1308 | 1309 | function Global:Get-CheckAppRuntimes { 1310 | <# 1311 | .SYNOPSIS 1312 | This function checks if a site uses multiple application pools. 1313 | .INPUTS 1314 | Site Name 1315 | .OUTPUTS 1316 | Return readiness object 1317 | #> 1318 | [CmdletBinding()] 1319 | param ( 1320 | [Parameter(Mandatory = $true)] 1321 | [ValidateNotNullOrEmpty()] 1322 | [String] 1323 | $SiteName 1324 | ) 1325 | 1326 | $RuntimeVersions = "Runtimes used by Applications = " 1327 | $AppPools = Get-ChildItem -Path IIS:\AppPools 1328 | foreach ($AppPool in $AppPools) { 1329 | $Apps = Get-WebApplication -Site $SiteName | sort -property ApplicationPool 1330 | foreach ($App in $Apps) { 1331 | if ($App.applicationPool -eq $AppPool.name) { 1332 | $AppPoolName = $AppPool.name 1333 | $RunTimeVersions = $RuntimeVersions + ($AppPool | Get-ItemProperty -Name managedRunTimeVersion).value + ";" 1334 | } 1335 | } 1336 | } 1337 | 1338 | $CurrentCLR = [System.Reflection.Assembly]::GetExecutingAssembly().ImageRuntimeVersion 1339 | $CurrentFramework = [System.Reflection.Assembly]::Load("mscorlib").GetName().Version.ToString() 1340 | $RunTimeVersions = $RunTimeVersions + " Current framework version is $CurrentFramework, current CLR version is $CurrentCLR" 1341 | 1342 | $RuntimesCheck = [ordered]@{ 1343 | "Result" = $true 1344 | "Description"= "Information on .NET Runtimes used by IIS applications" 1345 | "Log" = "Found $RuntimeVersions, no issues found" 1346 | } 1347 | $RuntimesCheck 1348 | } 1349 | 1350 | function Global:Get-CheckISAPIFilters { 1351 | <# 1352 | .SYNOPSIS 1353 | This function checks if a site uses ISAPI filters. 1354 | .INPUTS 1355 | None 1356 | .OUTPUTS 1357 | Return readiness object 1358 | #> 1359 | 1360 | $ISAPIFilter = "/system.WebServer/isapiFilters/filter" 1361 | $ISAPIFiltersUsed = Get-WebConfigurationProperty -filter $ISAPIFilter -name Enabled 1362 | 1363 | $Result = $true 1364 | $Log = "ISAPI filters found: " 1365 | 1366 | foreach ($Filter in $ISAPIFiltersUsed) { 1367 | $FilterName = $Filter.ItemXPath | Select-String "name=" 1368 | $NonASPFilter = $Filter | Select-String "ASP.Net" 1369 | if ($NonASPFilter) { 1370 | $NonASPFilterLog = $NonASPFilterLog + $NonASPFilter + " " 1371 | } 1372 | $Log = $Log + $FilterName + " " 1373 | } 1374 | if ($NonASPFilterLog) { 1375 | $Log = $Log + " Detected Non ASP filters : " + $NonASPFilterLog 1376 | } 1377 | 1378 | $IsapiFiltersCheck = [ordered]@{ 1379 | "Result" = $Result 1380 | "Description"= "Show detected ISAPI Filters" 1381 | "Log" = $Log 1382 | } 1383 | $IsapiFiltersCheck 1384 | } 1385 | 1386 | function Global:Get-CheckWindowsAuthenticationInWebConfig { 1387 | <# 1388 | .SYNOPSIS 1389 | This function checks if a site uses Windows Authentication - by scanning the web.config 1390 | .INPUTS 1391 | Site name 1392 | .OUTPUTS 1393 | Returns readiness object 1394 | #> 1395 | [CmdletBinding()] 1396 | param ( 1397 | [Parameter(Mandatory = $true)] 1398 | [ValidateNotNullOrEmpty()] 1399 | [String] 1400 | $SiteName 1401 | ) 1402 | 1403 | $iisPath = "IIS:\Sites\" + $SiteName 1404 | $configFile = Get-WebConfigFile $iisPath 1405 | $match = [regex]::Escape("") 1406 | $usingWinAuth = (Get-Content $configFile | %{$_ -match ($match)}) -contains $true 1407 | 1408 | $Result = $true 1409 | $Log = "No authentication problem found." 1410 | 1411 | if ($usingWinAuth) { 1412 | $Result = $false 1413 | $Log = "Windows authentication is not supported at this time." 1414 | } 1415 | 1416 | $AuthBindingsCheck = [ordered]@{ 1417 | "Result" = $Result 1418 | "Description"= "Forms of authentication supported" 1419 | "Log" = $Log 1420 | } 1421 | $AuthBindingsCheck 1422 | } 1423 | 1424 | function Global:Get-CheckAuthentication { 1425 | <# 1426 | .SYNOPSIS 1427 | This function checks if a site uses Windows Authentication 1428 | .INPUTS 1429 | Site name 1430 | .OUTPUTS 1431 | Return readiness object 1432 | #> 1433 | [CmdletBinding()] 1434 | param ( 1435 | [Parameter(Mandatory = $true)] 1436 | [ValidateNotNullOrEmpty()] 1437 | [String] 1438 | $SiteName 1439 | ) 1440 | 1441 | $wc = Get-WebConfiguration -filter /system.webServer/security/authentication/windowsAuthentication "IIS:\sites\$SiteName" 1442 | $Result = $true 1443 | 1444 | if ($wc.enabled) { 1445 | $Result = $false 1446 | $Log = "Windows authentication is not supported at this time." 1447 | } else { 1448 | $Log = "No authentication problem found." 1449 | } 1450 | 1451 | $AuthBindingsCheck = [ordered]@{ 1452 | "Result" = $Result 1453 | "Description"= "Forms of authentication supported" 1454 | "Log" = $Log 1455 | } 1456 | $AuthBindingsCheck 1457 | } 1458 | 1459 | function Global:Get-CheckHTTPPortBindings { 1460 | <# 1461 | .SYNOPSIS 1462 | This function checks HTTP port bindings for a site 1463 | .INPUTS 1464 | Site Name 1465 | .OUTPUTS 1466 | Return readiness object 1467 | #> 1468 | [CmdletBinding()] 1469 | param ( 1470 | [Parameter(Mandatory = $true)] 1471 | [ValidateNotNullOrEmpty()] 1472 | [String] 1473 | $SiteName 1474 | ) 1475 | 1476 | $Result = $true 1477 | $HTTPPortBinding = $false 1478 | $HTTPSPortBinding = $false 1479 | $WebBindingInfo = Get-WebBinding $SiteName 1480 | 1481 | foreach ($PortBinding in $WebBindingInfo) { 1482 | if ($PortBinding.protocol -eq 'http') { 1483 | if ( $HTTPPortBinding ) { 1484 | $Result = $false 1485 | } 1486 | $HTTPPortBinding = $true 1487 | } 1488 | if ($portBinding.protocol -eq 'https') { 1489 | if ( $HTTPSPortBinding ) { 1490 | $Result = $false 1491 | } 1492 | $HTTPSPortBinding = $true 1493 | } 1494 | } 1495 | 1496 | if ($Result) { 1497 | $log = "No issues found" 1498 | } else { 1499 | $log = "Found more than one HTTP and/or HTTPS port, only one port per protocol is supported" 1500 | } 1501 | 1502 | $PortBindingsCheck = [ordered]@{ 1503 | "Result" = $Result 1504 | "Description"= "Check HTTP port bindings" 1505 | "Log" = $log 1506 | } 1507 | $PortBindingsCheck 1508 | } 1509 | 1510 | function Global:Get-CheckAppProtocols { 1511 | <# 1512 | .SYNOPSIS 1513 | This function checks app protocols for a site 1514 | .INPUTS 1515 | Site Name 1516 | .OUTPUTS 1517 | Return readiness object 1518 | #> 1519 | [CmdletBinding()] 1520 | param ( 1521 | [Parameter(Mandatory = $true)] 1522 | [ValidateNotNullOrEmpty()] 1523 | [String] 1524 | $SiteName 1525 | ) 1526 | 1527 | $DetectNonHTTPProt = $false 1528 | $NonHTTPProtStr = "" 1529 | $Result = $true 1530 | 1531 | $FoundWebSite = $false 1532 | foreach ($site in Get-WebSite) { 1533 | if ($site.name -eq $SiteName) { 1534 | $FoundWebSite = $true 1535 | break 1536 | } 1537 | } 1538 | 1539 | if ( $FoundWebSite -ne $true ) { 1540 | Write-Output "ERROR : Did not find $SiteName" 1541 | throw "ERROR: Did not find $SiteName" 1542 | } 1543 | 1544 | foreach ($Prot in $site.bindings.protocol) { 1545 | if ($Prot -ne "http" -and $Prot -ne "https") { 1546 | $DetectNonHTTPProt = $true 1547 | if ($NonHTTPProtStr) { 1548 | $NonHTTPProtStr = $NonHTTPProtStr + "," + $Prot 1549 | } else { 1550 | $NonHTTPProtStr = $Prot 1551 | } 1552 | } 1553 | } 1554 | if ($DetectNonHTTPProt) { 1555 | $log = "Detected non HTTP protocols: $NonHTTPProtStr, they will not be migrated to EB" 1556 | $Result = $false 1557 | } else { 1558 | $log = "Detected HTTP protocol" 1559 | } 1560 | 1561 | $ProtocolsCheck = [ordered]@{ 1562 | "Result" = $Result 1563 | "Description"= "Show detected network protocols, non HTTP protocols cannot be migrated" 1564 | "Log" = $log 1565 | } 1566 | $ProtocolsCheck 1567 | } 1568 | 1569 | function Global:Get-CheckAppProcessModelIdentity { 1570 | <# 1571 | .SYNOPSIS 1572 | This function checks permissions of application process. 1573 | .INPUTS 1574 | Site Name 1575 | .OUTPUTS 1576 | Return readiness object 1577 | #> 1578 | [CmdletBinding()] 1579 | param ( 1580 | [Parameter(Mandatory = $true)] 1581 | [ValidateNotNullOrEmpty()] 1582 | [String] 1583 | $SiteName 1584 | ) 1585 | 1586 | $NumAppsPerPool = 0 1587 | $Apps = Get-WebApplication -Site $SiteName | sort -property ApplicationPool 1588 | $Count = 0 1589 | $Result = $true 1590 | $Log = "" 1591 | $Apps = Get-WebApplication -Site $SiteName | sort -property ApplicationPool 1592 | foreach ( $AppPool in Get-ChildItem IIS:\AppPools) { 1593 | foreach ($App in $Apps) { 1594 | if ($App.applicationPool -ne $AppPool.name) { 1595 | continue 1596 | } 1597 | $DotNetPools = $AppPool.name | Select-String ".NET" 1598 | if ($DotNetPools) { 1599 | continue 1600 | } 1601 | $IdentityType = $AppPool.processModel.identityType 1602 | if (($IdentityType-eq "ApplicationPoolIdentity") -or ($IdentityType.processModel.identityType -eq "LocalService") -or ($IdentityType.processModel.identityType -eq "LocalSystem")) { 1603 | $Result = $true 1604 | if ($Log) { 1605 | $Log = $Log + " ; Application Pool $($AppPool.name) runs in increased privilege $IdentityType" 1606 | } else { 1607 | $Log = "Application Pool $($AppPool.name) runs in increased privilege $IdentityType" 1608 | } 1609 | } 1610 | } 1611 | } 1612 | 1613 | $AppPoolIdentityCheck = [ordered]@{ 1614 | "Description"= "Show detected IIS application pool identities, see https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel#configuration" 1615 | "Result" = $Result 1616 | "Log" = $log 1617 | } 1618 | $AppPoolIdentityCheck 1619 | } 1620 | 1621 | function Global:New-ReadinessReport { 1622 | <# 1623 | .SYNOPSIS 1624 | This function creates an readiness report to check for IIS migratability to EB. 1625 | .INPUTS 1626 | Site Name 1627 | .OUTPUTS 1628 | JSON output containing report 1629 | #> 1630 | [CmdletBinding()] 1631 | param ( 1632 | [Parameter(Mandatory = $true)] 1633 | [ValidateNotNullOrEmpty()] 1634 | [String] 1635 | $SiteName 1636 | ) 1637 | 1638 | $Uuid = $MigrationRunID 1639 | $ReportTime = Get-Date -format r 1640 | 1641 | $AppPoolCheck = Get-CheckAppPool $SiteName 1642 | $AppRuntimesCheck = Get-CheckAppRuntimes $SiteName 1643 | $PortBindingsCheck = Get-CheckHTTPPortBindings $SiteName 1644 | $AuthSettingCheck = Get-CheckAuthentication $SiteName 1645 | $ProtCheck = Get-CheckAppProtocols $SiteName 1646 | $ISAPICheck = Get-CheckISAPIFilters 1647 | $AppPoolIdentityCheck = Get-CheckAppProcessModelIdentity $SiteName 1648 | $ChecksList = $AppPoolCheck, $AppRuntimesCheck, $PortBindingsCheck, $AuthSettingCheck, $ProtCheck, $ISAPICheck, $AppPoolIdentityCheck 1649 | 1650 | $report = [ordered]@{ 1651 | "SiteName" = $SiteName 1652 | "SessionGUID" = $Uuid 1653 | "ReportTime" = $ReportTime 1654 | "Checks" = $ChecksList 1655 | } 1656 | $report 1657 | } 1658 | 1659 | function Global:Test-DBConnection { 1660 | <# 1661 | .SYNOPSIS 1662 | This function tests validity of the given SQL connection string. 1663 | This function can be invoked after DB migration has been done to AWS cloud instances. 1664 | .INPUTS 1665 | Connection string of SQL Server. 1666 | .OUTPUTS 1667 | True or throws an error otherwise 1668 | .EXAMPLE 1669 | Test-DBConnectionString 1670 | 1671 | #> 1672 | [CmdletBinding()] 1673 | param ( 1674 | [Parameter(Mandatory = $true)] 1675 | [ValidateNotNullOrEmpty()] 1676 | [String] 1677 | $ConnStr 1678 | ) 1679 | 1680 | $SqlConn = New-Object System.Data.SqlClient.SqlConnection 1681 | 1682 | try { 1683 | $SqlConn.ConnectionString = $ConnStr 1684 | if ($SqlConn.Open()) { 1685 | $sqlConn.Close() 1686 | } 1687 | } catch { 1688 | $lastExceptionMessage = $error[0].Exception.Message 1689 | New-Message $DebugMsg $lastExceptionMessage $MigrationRunLogFile 1690 | return $false 1691 | } 1692 | 1693 | return $true 1694 | } 1695 | 1696 | function Global:Update-DBConnectionString { 1697 | <# 1698 | .SYNOPSIS 1699 | This function replaces all occurrances of a string (exact match) within a file with the input replacement string 1700 | .INPUTS 1701 | 1. Physical path of the file 1702 | 2. Connection string to be replaced 1703 | 3. New sonnection string 1704 | .OUTPUTS 1705 | None 1706 | #> 1707 | [CmdletBinding()] 1708 | param ( 1709 | [Parameter(Mandatory = $true)] 1710 | [ValidateNotNullOrEmpty()] 1711 | [String] 1712 | $FilePath, 1713 | 1714 | [Parameter(Mandatory = $true)] 1715 | [ValidateNotNullOrEmpty()] 1716 | [String] 1717 | $OldString, 1718 | 1719 | [Parameter(Mandatory = $true)] 1720 | [ValidateNotNullOrEmpty()] 1721 | [String] 1722 | $NewString 1723 | ) 1724 | 1725 | Verify-PathExists $FilePath 1726 | 1727 | $file = Get-Item $FilePath 1728 | $pattern = [regex]::Escape($OldString) 1729 | 1730 | (Get-Content $file.FullName ) | Foreach-Object { $_ -replace $pattern, $NewString } | Set-Content $file.FullName 1731 | } 1732 | 1733 | function Global:Get-DBConnectionStrings { 1734 | <# 1735 | .SYNOPSIS 1736 | This function reads the database connection strings from the website's Web.config file 1737 | If there is no output, the website either doesn't have a database or stores the connection strings in some non-standard ways 1738 | .INPUTS 1739 | The name of the website 1740 | .OUTPUTS 1741 | An array of XML elements of {name, connectionString, providerName}, or a string if configSource is found. 1742 | .EXAMPLE 1743 | Get-DBConnectionStrings MyWebsiteName 1744 | 1745 | Sample output: 1746 | name connectionString 1747 | ---- ---------------- 1748 | myConnectionString server=localhost;database=myDb;uid=myUser;password=myPass; 1749 | SitefinityConn data source=ABC;Integrated Security=SSPI;initial catalog=xyz;Backend=mssql 1750 | #> 1751 | [CmdletBinding()] 1752 | param ( 1753 | [Parameter(Mandatory = $true)] 1754 | [ValidateNotNullOrEmpty()] 1755 | [String] 1756 | $Name 1757 | ) 1758 | 1759 | $iisPath = "IIS:\Sites\" + $Name 1760 | $configFile = Get-WebConfigFile $iisPath 1761 | [XML] $configXML = Get-Content $configFile 1762 | $configSource = $configXML.configuration.connectionStrings.configSource 1763 | if ($configSource) { 1764 | return "Connection string config file detected: [Site_Root]\$configSource" 1765 | } 1766 | [Array] $configXML.configuration.connectionStrings.add 1767 | } 1768 | 1769 | function Global:Get-PossibleDBConnStrings { 1770 | <# 1771 | .SYNOPSIS 1772 | This function searches database connection strings from the website's physical directory. 1773 | If there is no output, the website either doesn't have a database or stores the connection strings in some non-standard ways 1774 | .INPUTS 1775 | Physical path of website. 1776 | .OUTPUTS 1777 | Connection strings that were found. 1778 | #> 1779 | [CmdletBinding()] 1780 | param ( 1781 | [Parameter(Mandatory = $true)] 1782 | [ValidateNotNullOrEmpty()] 1783 | [String] 1784 | $PhysPathName 1785 | ) 1786 | 1787 | $connStrPatterns = "DataSource=.*UserId=.*Password=.*", 1788 | "Server=.*Database=.*", 1789 | "User ID=.*Password=.*Host=.*Port=.*Database=.*Pooling=.*Min Pool Size=.*Max Pool Size=.*Connection Lifetime=.*", 1790 | "Provider=.*Data Source=.*location=.*User ID=.*password=.*timeout=.*", 1791 | "Server=.*Database=.*Uid=.*Pwd=.*", 1792 | "Database=.*Protocol=.*User Id=.*Password=.*", 1793 | "Provider=.*User Id=.*Password=.*", 1794 | "Provider=.*Data Source=.*", 1795 | "Provider=.*OledbKey1=.*OledbKey2=.*", 1796 | "Data Source=.*User ID=.*Password=.*", 1797 | "Data Source=.*Version=.*", 1798 | "Data Source=.*Persist Security Info=.*", 1799 | "Server=.*User ID=.*Password=.*Trusted_Connection=.*Encrypt=.*", 1800 | "Data Source=.*Integrated Security=.*" 1801 | 1802 | $connStrings = Get-ChildItem -Path $PhysPathName -Recurse -exclude "*.exe","*.dll" | Select-String -Pattern $connStrPatterns 1803 | 1804 | return $connStrings 1805 | } 1806 | 1807 | function Global:New-CustomDeploymentFile ($TemplateFileName, $DestFolderPath, $WebsiteName, $Password) { 1808 | # replace all "{REPLACE_WITH_WEBSITE_NAME}"s in the template with website name, and generate a new file in the dest folder 1809 | 1810 | $templatesFolderPath = Join-Path $runDirectory "utils\templates" 1811 | $templateFilePath = Join-Path $templatesFolderPath $TemplateFileName 1812 | 1813 | Verify-PathExists $templateFilePath 1814 | Verify-PathExists $DestFolderPath 1815 | Verify-WebsiteExists $WebsiteName 1816 | 1817 | $templateFileContent = Get-Content $templateFilePath 1818 | $updatedFileContent = $templateFileContent -replace "{REPLACE_WITH_WEBSITE_NAME}", $WebsiteName 1819 | 1820 | if ($Password) { 1821 | $updatedFileContent = $updatedFileContent -replace "{REPLACE_WITH_PASSWORD}", $Password 1822 | } 1823 | 1824 | $newFile = New-File $DestFolderPath $TemplateFileName $True 1825 | $updatedFileContent | Out-File $newFile 1826 | } 1827 | 1828 | function Global:ConvertTo-EBApplicationFolder { 1829 | <# 1830 | .SYNOPSIS 1831 | This function takes the folder that contains (only) the msDeploy source bundle zip 1832 | and creates folders & scripts under it to make it EB deployment compatible 1833 | 1834 | File structure of the original folder: 1835 | 1836 | eb-app-bundle/ 1837 | source_bundle.zip 1838 | 1839 | The file structure after calling this function: 1840 | 1841 | eb-app-bundle/ 1842 | .ebextensions/ 1843 | (empty folder) 1844 | aws-windows-deployment-manifest.json 1845 | scripts/ 1846 | site_install.ps1 1847 | site_post_install.ps1 1848 | site_restart.ps1 1849 | site_uninstall.ps1 1850 | source_bundle.zip 1851 | 1852 | You can then add additional scripts in .ebextensions/ folder for custom configurations 1853 | 1854 | .INPUTS 1855 | 1. Full physical path of the target folder 1856 | 2. Name of the website 1857 | 3. Password for msDeploy usage 1858 | .OUTPUTS 1859 | None 1860 | #> 1861 | [CmdletBinding()] 1862 | param ( 1863 | [Parameter(Mandatory = $true)] 1864 | [ValidateNotNullOrEmpty()] 1865 | [String] 1866 | $FolderPath, 1867 | 1868 | [Parameter(Mandatory = $true)] 1869 | [ValidateNotNullOrEmpty()] 1870 | [String] 1871 | $WebsiteName, 1872 | 1873 | [Parameter(Mandatory = $true)] 1874 | [ValidateNotNullOrEmpty()] 1875 | [String] 1876 | $Password 1877 | ) 1878 | 1879 | Verify-PathExists $FolderPath 1880 | Verify-WebsiteExists $WebsiteName 1881 | 1882 | New-Folder $FolderPath ".ebextensions" $False 1883 | $scriptsPath = New-Folder $FolderPath "scripts" $True 1884 | 1885 | New-CustomDeploymentFile "aws-windows-deployment-manifest.json" $FolderPath $WebsiteName 1886 | New-CustomDeploymentFile "site_install.ps1" $scriptsPath $WebsiteName $null 1887 | New-CustomDeploymentFile "site_post_install.ps1" $scriptsPath $WebsiteName 1888 | New-CustomDeploymentFile "site_restart.ps1" $scriptsPath $WebsiteName 1889 | New-CustomDeploymentFile "site_uninstall.ps1" $scriptsPath $WebsiteName 1890 | } 1891 | 1892 | function Global:Generate-EBApplicationBundle { 1893 | <# 1894 | .SYNOPSIS 1895 | This function zips up the folder converted by ConvertTo-EBApplicationFolder to generate a app bundle zip file ready for EB deployment 1896 | .INPUTS 1897 | Full physical path of the folder to zip up 1898 | .OUTPUTS 1899 | Full physical path of the output zip file 1900 | #> 1901 | [CmdletBinding()] 1902 | param ( 1903 | [Parameter(Mandatory = $true)] 1904 | [ValidateNotNullOrEmpty()] 1905 | [String] 1906 | $EBAppFolder 1907 | ) 1908 | 1909 | Verify-PathExists $EBAppFolder 1910 | 1911 | $parentFolder = Split-Path $EBAppFolder -Parent 1912 | $ebAppFolderName = Split-Path $EBAppFolder -Leaf 1913 | $ebAppPackageName = $ebAppFolderName + ".zip" 1914 | $ebAppPackagePath = Join-Path $parentFolder $ebAppFolderName 1915 | 1916 | Get-ZippedFolder $EBAppFolder $parentFolder $ebAppPackageName 1917 | 1918 | $ebAppPackagePath 1919 | } 1920 | 1921 | function Global:Add-EBExtensionFileToFolder { 1922 | <# 1923 | .SYNOPSIS 1924 | This function copies the input file to the ".ebextensions" folder under an EB application folder 1925 | .INPUTS 1926 | 1. Full physical path of the input file 1927 | 2. Full physical path of the EB application folder 1928 | .OUTPUTS 1929 | None - will throw exception when execution fails 1930 | #> 1931 | [CmdletBinding()] 1932 | param( 1933 | [Parameter(Mandatory = $true)] 1934 | [ValidateNotNullOrEmpty()] 1935 | [String] 1936 | $FilePath, 1937 | 1938 | [Parameter(Mandatory = $true)] 1939 | [ValidateNotNullOrEmpty()] 1940 | [String] 1941 | $EBAppFolderPath 1942 | ) 1943 | 1944 | $ebExtensionsFolderPath = Join-Path $EBAppFolderPath ".ebextensions" 1945 | 1946 | Verify-PathExists $FilePath 1947 | Verify-PathExists $EBAppFolderPath 1948 | Verify-PathExists $ebExtensionsFolderPath 1949 | 1950 | Copy-Item $FilePath -Destination $ebExtensionsFolderPath 1951 | } 1952 | 1953 | function Global:Add-IISHardeningSettings { 1954 | <# 1955 | .SYNOPSIS 1956 | This function adds IIS hardening settings to the EB application folder 1957 | .INPUTS 1958 | Full physical path of the EB application folder 1959 | .OUTPUTS 1960 | None - will throw exception when execution fails 1961 | #> 1962 | [CmdletBinding()] 1963 | param( 1964 | [Parameter(Mandatory = $true)] 1965 | [ValidateNotNullOrEmpty()] 1966 | [String] 1967 | $EBAppFolderPath 1968 | ) 1969 | 1970 | Verify-PathExists $EBAppFolderPath 1971 | 1972 | $iisHardeningConfig = Join-Path $runDirectory "utils\templates\iis_hardening.config" 1973 | $iisHardeningScript = Join-Path $runDirectory "utils\templates\harden_iis.ps1" 1974 | 1975 | Add-EBExtensionFileToFolder $iisHardeningConfig $EBAppFolderPath 1976 | Add-EBExtensionFileToFolder $iisHardeningScript $EBAppFolderPath 1977 | } 1978 | 1979 | function Global:Generate-MSDeploySourceBundle { 1980 | <# 1981 | .SYNOPSIS 1982 | This function uses Web Deploy to package a website (using appHostConfig provider) into a zip file (source_bundle.zip) 1983 | .INPUTS 1984 | 1. Name of the website (as it appears in IIS) 1985 | 2. Full physical path of an empty folder to generate the zip file in 1986 | 3. EncryptPassword for SSL certificate migration 1987 | 4. Full physical path to an existing file that stores msDeploy run logs (stdout) 1988 | 5. Full physical path to an existing file that stores msDeploy run logs (stderr) 1989 | .OUTPUTS 1990 | The script returns the full physical path to the generated zip file 1991 | .EXAMPLE 1992 | Generate-MSDeploySourceBundle "NopCommerce" "C:\dest\out" $password "C:\dest\stdout.log" "C:\dest\stderr.log" 1993 | .NOTES 1994 | Please make 2 new & empty log files for this function's use ONLY. 1995 | #> 1996 | [CmdletBinding()] 1997 | param ( 1998 | [Parameter(Mandatory = $true)] 1999 | [ValidateNotNullOrEmpty()] 2000 | [String] 2001 | $WebsiteName, 2002 | 2003 | [Parameter(Mandatory = $true)] 2004 | [ValidateNotNullOrEmpty()] 2005 | [String] 2006 | $DestinationFolderPath, 2007 | 2008 | [Parameter(Mandatory = $true)] 2009 | [ValidateNotNullOrEmpty()] 2010 | [String] 2011 | $EncryptPassword, 2012 | 2013 | [Parameter(Mandatory = $true)] 2014 | [ValidateNotNullOrEmpty()] 2015 | [String] 2016 | $StdOutLogFilePath, 2017 | 2018 | [Parameter(Mandatory = $true)] 2019 | [ValidateNotNullOrEmpty()] 2020 | [String] 2021 | $StdErrLogFilePath 2022 | ) 2023 | 2024 | Verify-FolderExistsAndEmpty $DestinationFolderPath 2025 | Verify-WebsiteExists $WebsiteName 2026 | Verify-PathExists $StdOutLogFilePath 2027 | Verify-PathExists $StdErrLogFilePath 2028 | 2029 | $SourceBundleName = "source_bundle.zip" 2030 | $sourceBundlePath = Join-Path $DestinationFolderPath $SourceBundleName 2031 | 2032 | $msDeployVerb = "-verb:sync" 2033 | $msDeploySource = "-source:appHostConfig='$WebsiteName'" 2034 | $msDeployDest = "-dest:package='$sourceBundlePath',encryptPassword='$EncryptPassword'" 2035 | $msDeployEnableAppPoolExt = "-enableLink:AppPoolExtension" 2036 | $msDeployAppPool = "-declareParam:name='Application Pool',defaultValue='Default Web Site',description='Application pool for this site',kind=DeploymentObjectAttribute,scope=appHostConfig,match='application/@applicationPool'" 2037 | 2038 | [String[]] $msDeployArgs = @( 2039 | $msDeployVerb, 2040 | $msDeploySource, 2041 | $msDeployDest, 2042 | $msDeployEnableAppPoolExt, 2043 | $msDeployAppPool 2044 | ) 2045 | 2046 | $msDeployExe = Get-WebDeployV3Exe 2047 | $process = Start-Process $msDeployExe -ArgumentList $msDeployArgs -NoNewWindow -Wait -PassThru -RedirectStandardOutput $StdOutLogFilePath -RedirectStandardError $StdErrLogFilePath 2048 | 2049 | if ($process.ExitCode -ne 0) { 2050 | throw "ERROR: Failed to package source bundle for website $WebsiteName." 2051 | } 2052 | 2053 | $sourceBundlePath 2054 | } 2055 | 2056 | function Global:Remove-SSLCertificate($pathToUnzippedSourceBundle) { 2057 | $archiveXMLPath = Join-Path $pathToUnzippedSourceBundle "archive.xml" 2058 | Verify-PathExists $archiveXMLPath 2059 | 2060 | $nodeToRemove = "//httpCert" 2061 | [xml]$archiveXML = Get-Content $archiveXMLPath 2062 | 2063 | $node = $archiveXML.SelectSingleNode($nodeToRemove) 2064 | while ($node -ne $null) { 2065 | $node.ParentNode.RemoveChild($node) | Out-Null 2066 | $node = $archiveXML.SelectSingleNode($nodeToRemove) 2067 | } 2068 | $archiveXML.save($archiveXMLPath) 2069 | } 2070 | 2071 | function Global:Verify-UserHasRequiredAWSPolicies { 2072 | <# 2073 | .SYNOPSIS 2074 | This function verifies that the current AWS user has the following AWS managed policies: 2075 | 1. IAMReadOnlyAccess (only needed for Get-IAMAttachedUserPolicyList) 2076 | 2. AdministratorAccess-AWSElasticBeanstalk (needed for EB application deployment operations) 2077 | .INPUTS 2078 | None 2079 | .OUTPUTS 2080 | None 2081 | #> 2082 | 2083 | try { 2084 | $stsIdentity = Get-STSCallerIdentity 2085 | $userName = $stsIdentity.Arn.Split("/")[-1] 2086 | $policies = Get-IAMAttachedUserPolicyList -UserName $userName 2087 | } catch { 2088 | throw "ERROR: Please make sure that your AWS credentials are correct, and the AWS managed policy IAMReadOnlyAccess is attached to the current user" 2089 | } 2090 | 2091 | foreach ($policy in $policies) { 2092 | if ($policy.PolicyName -eq "AdministratorAccess-AWSElasticBeanstalk") { 2093 | return 2094 | } 2095 | } 2096 | throw "ERROR: Please make sure that the AWS managed policy AdministratorAccess-AWSElasticBeanstalk is attached to the current user" 2097 | } 2098 | 2099 | function Global:Verify-RequiredRolesExist { 2100 | <# 2101 | .SYNOPSIS 2102 | This function checks if the required IAM roles exists (aws-elasticbeanstalk-ec2-role, aws-elasticbeanstalk-service-role) 2103 | .INPUTS 2104 | None 2105 | .OUTPUTS 2106 | None 2107 | #> 2108 | try { 2109 | Get-IAMInstanceProfile -InstanceProfileName $DefaultElasticBeanstalkInstanceProfileName | Out-Null 2110 | } catch { 2111 | New-Message $InfoMsg "Default Elastic Beanstalk instance profile $DefaultElasticBeanstalkInstanceProfileName was not found." $MigrationRunLogFile 2112 | New-Message $InfoMsg "Creating IAM role $DefaultElasticBeanstalkInstanceProfileName." $MigrationRunLogFile 2113 | New-IAMRole -RoleName $DefaultElasticBeanstalkInstanceProfileName -AssumeRolePolicyDocument $(Get-Content -raw 'utils\iam_trust_relationship_ec2.json') 2114 | Register-IAMRolePolicy -RoleName $DefaultElasticBeanstalkInstanceProfileName -PolicyArn 'arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier' 2115 | Register-IAMRolePolicy -RoleName $DefaultElasticBeanstalkInstanceProfileName -PolicyArn 'arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier' 2116 | Register-IAMRolePolicy -RoleName $DefaultElasticBeanstalkInstanceProfileName -PolicyArn 'arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker' 2117 | New-Message $InfoMsg "Created IAM role $DefaultElasticBeanstalkInstanceProfileName." $MigrationRunLogFile 2118 | New-Message $InfoMsg "Creating instance profile $DefaultElasticBeanstalkInstanceProfileName." $MigrationRunLogFile 2119 | New-IAMInstanceProfile -InstanceProfileName $DefaultElasticBeanstalkInstanceProfileName 2120 | Add-IAMRoleToInstanceProfile -InstanceProfileName $DefaultElasticBeanstalkInstanceProfileName -RoleName $DefaultElasticBeanstalkInstanceProfileName 2121 | New-Message $InfoMsg "Created Elastic Beanstalk instance profile $DefaultElasticBeanstalkInstanceProfileName." $MigrationRunLogFile 2122 | } 2123 | try { 2124 | Get-IAMRole -RoleName $DefaultElasticBeanstalkServiceRoleName | Out-Null 2125 | } catch { 2126 | New-Message $InfoMsg "Default Elastic Beanstalk service role $DefaultElasticBeanstalkServiceRoleName was not found." $MigrationRunLogFile 2127 | New-Message $InfoMsg "Creating IAM role $DefaultElasticBeanstalkServiceRoleName." $MigrationRunLogFile 2128 | New-IAMRole -roleName $DefaultElasticBeanstalkServiceRoleName -AssumeRolePolicyDocument $(Get-Content -raw 'utils\iam_trust_relationship_eb.json') 2129 | Register-IAMRolePolicy -RoleName $DefaultElasticBeanstalkServiceRoleName -PolicyArn 'arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkService' 2130 | Register-IAMRolePolicy -RoleName $DefaultElasticBeanstalkServiceRoleName -PolicyArn 'arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth' 2131 | New-Message $InfoMsg "Created IAM role $DefaultElasticBeanstalkServiceRoleName." $MigrationRunLogFile 2132 | } 2133 | } 2134 | 2135 | function Global:Verify-CanCreateNewEIP { 2136 | <# 2137 | .SYNOPSIS 2138 | This function verifies if a new elastic IP can be created in the current AWS region. 2139 | This function will attempt to create a new EIP and delete it immidiately. 2140 | .INPUTS 2141 | None 2142 | .OUTPUTS 2143 | None 2144 | #> 2145 | 2146 | $deleteEIP = $False 2147 | try { 2148 | $tmpEIP = New-EC2Address 2149 | $deleteEIP = $True 2150 | } catch { 2151 | throw "ERROR: Unable to create new elastic IP. Please check the EIP reource limit in the selected AWS region." 2152 | } finally { 2153 | if ($deleteEIP) { 2154 | Remove-EC2Address -Force -AllocationId $tmpEIP.AllocationId 2155 | } 2156 | } 2157 | } 2158 | 2159 | function Global:Get-CurrentAWSAccountID { 2160 | <# 2161 | .SYNOPSIS 2162 | This function returns the ID of the current AWS account 2163 | .INPUTS 2164 | None 2165 | .OUTPUTS 2166 | AWS Account ID 2167 | #> 2168 | 2169 | $stsIdentity = Get-STSCallerIdentity 2170 | $stsIdentity.Account 2171 | } 2172 | 2173 | function Global:New-TempS3Bucket { 2174 | <# 2175 | .SYNOPSIS 2176 | This function creates a new S3 bucket 2177 | .INPUTS 2178 | 1. S3 bucket region 2179 | .OUTPUTS 2180 | Bucket name 2181 | #> 2182 | [CmdletBinding()] 2183 | param ( 2184 | [Parameter(Mandatory = $true)] 2185 | [ValidateNotNullOrEmpty()] 2186 | [String] 2187 | $BucketRegion 2188 | ) 2189 | 2190 | $awsID = Get-CurrentAWSAccountID 2191 | $bucketName = "elastic-beanstalk-migration-" + $awsID + "-" + $(Get-Date -f yyyyMMddHHmmss) 2192 | $null = New-S3Bucket -BucketName $bucketName -Region $BucketRegion -CannedACLName Private | Out-File -append $ItemCreationLogFile 2193 | $null = Set-S3BucketEncryption -Region $BucketRegion -BucketName $bucketName -ServerSideEncryptionConfiguration_ServerSideEncryptionRule @{ServerSideEncryptionByDefault = @{ServerSideEncryptionAlgorithm = "AES256" } } 2194 | Invoke-CommandsWithRetry 99 $MigrationRunLogFile { 2195 | $result = Get-S3ACL -Bucketname $bucketName 2196 | } 2197 | foreach ($grant in $result.Grants) 2198 | { 2199 | if ($grant.Grantee.URI -eq "http://acs.amazonaws.com/groups/global/AllUsers") 2200 | { 2201 | Write-Host "Error: S3 bucket '$bucketName' has public access." 2202 | Delete-TempS3Bucket $bucketName 2203 | Exit-WithError 2204 | } 2205 | } 2206 | $bucketName 2207 | } 2208 | 2209 | function Global:Delete-TempS3Bucket { 2210 | <# 2211 | .SYNOPSIS 2212 | This function deletes an S3 bucket 2213 | .INPUTS 2214 | 1. S3 bucket name 2215 | .OUTPUTS 2216 | None 2217 | #> 2218 | [CmdletBinding()] 2219 | param ( 2220 | [Parameter(Mandatory = $true)] 2221 | [ValidateNotNullOrEmpty()] 2222 | [String] 2223 | $BucketName 2224 | ) 2225 | 2226 | Remove-S3Bucket -BucketName $BucketName -DeleteBucketContent -Force 2227 | } 2228 | 2229 | function Global:Upload-FileToS3Bucket { 2230 | <# 2231 | .SYNOPSIS 2232 | This function puts a file into a S3 bucket, giving the bucket owner full control 2233 | .INPUTS 2234 | 1. Full physical path of the file 2235 | 2. S3 bucket name 2236 | 3. S3 bucket region 2237 | .OUTPUTS 2238 | None 2239 | #> 2240 | [CmdletBinding()] 2241 | param ( 2242 | [Parameter(Mandatory = $true)] 2243 | [ValidateNotNullOrEmpty()] 2244 | [String] 2245 | $FilePath, 2246 | 2247 | [Parameter(Mandatory = $true)] 2248 | [ValidateNotNullOrEmpty()] 2249 | [String] 2250 | $BucketName, 2251 | 2252 | [Parameter(Mandatory = $true)] 2253 | [ValidateNotNullOrEmpty()] 2254 | [String] 2255 | $BucketRegion 2256 | ) 2257 | 2258 | Verify-PathExists $FilePath 2259 | if (-Not (Test-S3Bucket -BucketName $BucketName)) { 2260 | throw "ERROR: S3 bucket $BucketName does not exist" 2261 | } 2262 | Write-S3Object -BucketName $BucketName -File $FilePath -CannedACLName "bucket-owner-full-control" -Region $BucketRegion 2263 | } 2264 | 2265 | function Global:Get-S3ObjectURL { 2266 | <# 2267 | .SYNOPSIS 2268 | This function gets 2269 | .INPUTS 2270 | 1. S3 bucket name 2271 | 1. S3 object name 2272 | 1. S3 bucket region 2273 | .OUTPUTS 2274 | URL of the object 2275 | #> 2276 | [CmdletBinding()] 2277 | param ( 2278 | [Parameter(Mandatory = $true)] 2279 | [ValidateNotNullOrEmpty()] 2280 | [String] 2281 | $BucketName, 2282 | 2283 | [Parameter(Mandatory = $true)] 2284 | [ValidateNotNullOrEmpty()] 2285 | [String] 2286 | $ObjectName, 2287 | 2288 | [Parameter(Mandatory = $true)] 2289 | [ValidateNotNullOrEmpty()] 2290 | [String] 2291 | $BucketRegion 2292 | ) 2293 | $obj = Get-S3Object -BucketName $BucketName -Key $ObjectName -Region $BucketRegion 2294 | if (-Not $obj) { 2295 | throw "ERROR: unable to find object $ObjectName" 2296 | } 2297 | 2298 | "https://s3-$BucketRegion.amazonaws.com/$BucketName/$ObjectName" 2299 | } 2300 | 2301 | function Global:Validate-NumberedListUserInput { 2302 | <# 2303 | .SYNOPSIS 2304 | This function validates that the user has entered a number in a valid range 2305 | .INPUTS 2306 | 1. User input 2307 | 2. Lower bound 2308 | 3. Upper bound 2309 | .OUTPUTS 2310 | None 2311 | #> 2312 | [CmdletBinding()] 2313 | param ( 2314 | [Parameter(Mandatory = $true)] 2315 | [ValidateNotNullOrEmpty()] 2316 | [Int] 2317 | $UserInput, 2318 | 2319 | [Parameter(Mandatory = $true)] 2320 | [ValidateNotNullOrEmpty()] 2321 | [Int] 2322 | $LowerBound, 2323 | 2324 | [Parameter(Mandatory = $true)] 2325 | [ValidateNotNullOrEmpty()] 2326 | [Int] 2327 | $UpperBound 2328 | ) 2329 | 2330 | if (($UserInput -lt $LowerBound) -or ($UserInput -gt $UpperBound)) { 2331 | throw "Please enter a number in the list range." 2332 | } 2333 | } 2334 | 2335 | function Global:Validate-PowerShellArchitecture { 2336 | <# 2337 | .SYNOPSIS 2338 | This function validates that the PowerShell process architecture matches the IIS and OS architecture 2339 | 2340 | .OUTPUTS 2341 | None 2342 | #> 2343 | 2344 | if ([System.Environment]::is64BitOperatingSystem -ne [System.Environment]::Is64BitProcess){ 2345 | if ([System.Environment]::is64BitOperatingSystem){ 2346 | Write-Host "Run the migration assistant using 64-bit PowerShell [Windows PowerShell] and not 32-bit PowerShell [Windows PowerShell (x86)]." 2347 | } 2348 | else { 2349 | Write-Host "Run the migration assistant using 32-bit PowerShell [Windows PowerShell (x86)]." 2350 | } 2351 | Exit-WithError 2352 | } 2353 | } 2354 | 2355 | Validate-PowerShellArchitecture 2356 | 2357 | $Global:Version = "0.2" # must be exactly 3 characters long otherwise it breaks title display 2358 | $Global:runDirectory = $PSScriptRoot 2359 | $Global:ebAppBundleFileSizeLimit = 512mb 2360 | $Global:MigrationRunId = "run-" + $(Get-Date -f yyyyMMddHHmmss) 2361 | 2362 | # load custom migration run settings 2363 | $settings = Get-Content -Raw -Path "$runDirectory\utils\settings.txt" | ConvertFrom-Json 2364 | 2365 | $Global:DisplayTimestampsInConsole = [boolean]::Parse($settings.displayTimestampsInConsole) 2366 | 2367 | $Global:DefaultAwsProfileFileLocation = $settings.defaultAwsProfileFileLocation 2368 | $Global:DefaultAwsProfileName = $settings.defaultAwsProfileName 2369 | $Global:IgnoreMigrationReadinessWarnings = [boolean]::Parse($settings.ignoreMigrationReadinessWarnings) 2370 | $Global:DeleteTempS3Buckets = [boolean]::Parse($settings.deleteTempS3Buckets) 2371 | 2372 | Import-Module AWSPowerShell 2373 | Import-Module WebAdministration 2374 | Add-Type -Assembly System.IO.Compression.FileSystem 2375 | Setup-Workspace # set up global variables 2376 | Setup-NewMigrationRun $MigrationRunId # new migration run every time 2377 | 2378 | Write-Host " " 2379 | New-Message $InfoMsg " -----------------------------------------" $MigrationRunLogFile 2380 | New-Message $InfoMsg "| |" $MigrationRunLogFile 2381 | New-Message $InfoMsg "| AWS Web Application Migration Assistant |" $MigrationRunLogFile 2382 | New-Message $InfoMsg "| v$Version |" $MigrationRunLogFile 2383 | New-Message $InfoMsg " -----------------------------------------`n" $MigrationRunLogFile 2384 | 2385 | New-Message $InfoMsg "Starting new migration run: $MigrationRunId" $MigrationRunLogFile 2386 | New-Message $InfoMsg "Logs for this migration run can be found at" $MigrationRunLogFile 2387 | New-Message $InfoMsg " $LogFolderPath" $MigrationRunLogFile 2388 | 2389 | # Dependency Check 2390 | 2391 | New-Message $InfoMsg "Checking for dependencies..." $MigrationRunLogFile 2392 | $missingDependencyString = Get-MissingDependencies 2393 | 2394 | if ($missingDependencyString) { 2395 | New-Message $FatalMsg "Missing dependencies found. Be sure the following items are installed on this server:" $MigrationRunLogFile 2396 | New-Message $FatalMsg $missingDependencyString $MigrationRunLogFile 2397 | Exit-WithError 2398 | } 2399 | 2400 | 2401 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2402 | 2403 | 2404 | # AWS profile collection 2405 | 2406 | if (-Not $ReportOnly) 2407 | { 2408 | 2409 | New-Message $InfoMsg "Provide an AWS profile that the migrated application should use." $MigrationRunLogFile 2410 | 2411 | $Global:glb_AwsProfileLocation = $Null 2412 | $Global:glb_AwsProfileName = $Null 2413 | $Global:DefaultElasticBeanstalkInstanceProfileName = "aws-elasticbeanstalk-ec2-role" 2414 | $Global:DefaultElasticBeanstalkServiceRoleName = "aws-elasticbeanstalk-service-role" 2415 | 2416 | if ($DefaultAwsProfileFileLocation) { 2417 | New-Message $InfoMsg "Default AWS profile file detected at '$DefaultAwsProfileFileLocation'." $MigrationRunLogFile 2418 | $glb_AwsProfileLocation = $DefaultAwsProfileFileLocation 2419 | } else { 2420 | if ($NonInteractiveMode) { 2421 | $glb_AwsProfileLocation = $mfarg_awsprofilelocation 2422 | } else { 2423 | $glb_AwsProfileLocation = Get-UserInputString $MigrationRunLogFile "Enter file location, for example 'c:\aws\credentials', or press ENTER" 2424 | } 2425 | } 2426 | if ($DefaultAwsProfileName) { 2427 | New-Message $InfoMsg "Default AWS profile name '$DefaultAwsProfileName' detected." $MigrationRunLogFile 2428 | $glb_AwsProfileName = $DefaultAwsProfileName 2429 | } else { 2430 | if ($NonInteractiveMode) { 2431 | $glb_AwsProfileName = $mfarg_awsprofilename 2432 | } else { 2433 | $glb_AwsProfileName = Get-UserInputString $MigrationRunLogFile "Enter the AWS profile name" 2434 | } 2435 | } 2436 | 2437 | $AwsCredsObj = Get-AWSCredentials -ProfileName $glb_AwsProfileName -ProfileLocation $glb_AwsProfileLocation 2438 | if ($AwsCredsObj) { 2439 | $AwsCreds = $AwsCredsObj.GetCredentials() 2440 | $accessKey = $AwsCreds.accesskey 2441 | $secretKey = $AwsCreds.secretkey 2442 | if (-Not $accessKey) { 2443 | New-Message $FatalMsg "Error: Invalid AWS access key. Be sure to provide the correct AWS profile." $MigrationRunLogFile 2444 | Exit-WithError 2445 | } 2446 | if (-Not $secretKey) { 2447 | New-Message $FatalMsg "Error: Invalid AWS secret key. Be sure to provide the correct AWS profile." $MigrationRunLogFile 2448 | Exit-WithError 2449 | } 2450 | Set-AWSCredential -AccessKey $accessKey -SecretKey $secretKey 2451 | } else { 2452 | New-Message $FatalMsg "Error: Invalid AWS credentials. Be sure to provide the correct AWS profile." $MigrationRunLogFile 2453 | Exit-WithError 2454 | } 2455 | 2456 | # Collect AWS region 2457 | 2458 | Invoke-CommandsWithRetry 99 $MigrationRunLogFile { 2459 | New-Message $InfoMsg " " $MigrationRunLogFile 2460 | New-Message $InfoMsg "Enter the AWS Region for your migrated application. For example: us-east-2." $MigrationRunLogFile 2461 | New-Message $InfoMsg "For a list of available AWS Regions, see:" $MigrationRunLogFile 2462 | New-Message $InfoMsg " https://docs.aws.amazon.com/general/latest/gr/rande.html" $MigrationRunLogFile 2463 | 2464 | if ($NonInteractiveMode) { 2465 | $regionInput = $mfarg_region 2466 | } else { 2467 | $regionInput = Get-UserInputString $MigrationRunLogFile "Enter the AWS Region [us-east-1]" 2468 | } 2469 | if (!$regionInput) { 2470 | $regionInput = "us-east-1" 2471 | } 2472 | $Global:glb_AwsRegion = Get-AWSRegion $regionInput 2473 | if ($glb_AwsRegion -is [system.array] -or $glb_AwsRegion.name -eq "unknown") { 2474 | Throw "Error: Invalid AWS Region" 2475 | } 2476 | } 2477 | 2478 | Set-DefaultAWSRegion $glb_AwsRegion 2479 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2480 | 2481 | } 2482 | 2483 | try { 2484 | # all other AWS verifications go here 2485 | Verify-UserHasRequiredAWSPolicies 2486 | Verify-RequiredRolesExist 2487 | } catch { 2488 | $lastExceptionMessage = $error[0].Exception.Message 2489 | New-Message $FatalMsg $lastExceptionMessage $MigrationRunLogFile 2490 | Exit-WithError 2491 | } 2492 | 2493 | # Determine the website to migrate 2494 | $serverObj = Get-IISServerInfoObject 2495 | Write-IISServerInfo $EnvironmentInfoLogFile 2496 | 2497 | $websites = Get-Website 2498 | if (-Not $websites) { 2499 | New-Message $FatalMsg "The migration assistant didn't find any website on this server." $MigrationRunLogFile 2500 | Exit-WithError 2501 | } 2502 | 2503 | New-Message $InfoMsg "The migration assistant found website(s) on the local server '$($serverObj.computerName)'." $MigrationRunLogFile 2504 | $webSiteNum = 1 2505 | $webSiteNameTable = @{} 2506 | foreach ($site in $websites) { 2507 | $siteEntry = "[$webSiteNum] " + "- " + $site.name 2508 | $webSiteNameTable[$webSiteNum.ToString()] = $site.name 2509 | $webSiteNum = $webSiteNum + 1 2510 | New-Message $InfoMsg $siteEntry $MigrationRunLogFile 2511 | } 2512 | 2513 | Invoke-CommandsWithRetry 99 $MigrationRunLogFile { 2514 | if ($NonInteractiveMode) { 2515 | $websiteNumStr = $mfarg_websitenumstr 2516 | } else { 2517 | $websiteNumStr = Get-UserInputString $MigrationRunLogFile "Enter the number of the website to migrate: [1]" 2518 | } 2519 | if (!$websiteNumStr) { 2520 | $Global:glb_websiteToMigrate = $webSiteNameTable["1"] 2521 | } else { 2522 | $Global:glb_websiteToMigrate = $webSiteNameTable[$websiteNumStr] 2523 | } 2524 | New-Message $InfoMsg "Selected the website '$Global:glb_websiteToMigrate' to migrate from the server '$($serverObj.computerName)'." $MigrationRunLogFile 2525 | Verify-WebsiteExists $glb_websiteToMigrate 2526 | } 2527 | 2528 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2529 | 2530 | # Generate migration readiness report 2531 | 2532 | New-Message $InfoMsg "Analyzing migration readiness..." $MigrationRunLogFile 2533 | 2534 | try { 2535 | 2536 | $readinessReportFileName = "migration_readiness_report.json" 2537 | $Global:readinessReportFilePath = New-File $CurrentMigrationRunPath $readinessReportFileName $True 2538 | $reportObject = New-ReadinessReport $glb_websiteToMigrate 2539 | $reportObject | ConvertTo-Json | Out-File $readinessReportFilePath 2540 | 2541 | New-Message $InfoMsg "The migration readiness report is at:" $MigrationRunLogFile 2542 | New-Message $InfoMsg $readinessReportFilePath $MigrationRunLogFile 2543 | New-Message $InfoMsg "Looking for incompatibilities..." $MigrationRunLogFile 2544 | 2545 | $incompatibilityFound = $False 2546 | 2547 | foreach ($checkItem in $reportObject.Checks) { 2548 | if (-Not $checkItem.Result) { 2549 | New-Message $ErrorMsg $checkItem.Log $MigrationRunLogFile 2550 | $incompatibilityFound = $True 2551 | } 2552 | } 2553 | 2554 | if ($incompatibilityFound) { 2555 | New-Message $FatalMsg "The migration assistant found incompatibilities for website '$glb_websiteToMigrate'." $MigrationRunLogFile 2556 | if ($IgnoreMigrationReadinessWarnings) { 2557 | New-Message $InfoMsg "The migration assistant found a custom setting directing it to ignore warnings. Continue the migration?" $MigrationRunLogFile 2558 | if ($NonInteractiveMode) { 2559 | $userConsent = $mfarg_userconsent 2560 | } else { 2561 | $userConsent = Get-UserInputString $MigrationRunLogFile "Press ENTER to continue" 2562 | } 2563 | } else { 2564 | New-Message $InfoMsg "Contact the AWS migration support team for help with preparing the website for migration." $MigrationRunLogFile 2565 | Exit-WithError 2566 | } 2567 | } 2568 | 2569 | } catch { 2570 | New-Message $FatalMsg $_ $MigrationRunLogFile 2571 | New-Message $FatalMsg "The migration assistant is unable to generate a migration readiness report. The website might be unsupported for Elastic Beanstalk migration." $MigrationRunLogFile 2572 | if ($NonInteractiveMode) { 2573 | $userInputI = $mfarg_userinputI 2574 | } else { 2575 | $userInputI = Get-UserInputString $MigrationRunLogFile "Enter 'I' to ignore this warning and continue migration" 2576 | } 2577 | 2578 | if ($userInputI -eq "I" -or $userInputI -eq "i") { 2579 | New-Message $InfoMsg "Starting the Elastic Beanstalk migration." $MigrationRunLogFile 2580 | } else { 2581 | New-Message $InfoMsg "Contact the AWS migration support team for help on migration readiness." $MigrationRunLogFile 2582 | Exit-WithError 2583 | } 2584 | } 2585 | 2586 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2587 | 2588 | 2589 | # Back up the website 2590 | 2591 | if (-Not $ReportOnly) 2592 | { 2593 | $appBundleFolderName = "EB-Application" 2594 | $appBundleFolderPath = New-Folder $CurrentMigrationRunPath $appBundleFolderName $True 2595 | $msDeployStdOutLog = New-File $LogFolderPath "msDeployStdOut.log" $True 2596 | $msDeployStdErrLog = New-File $LogFolderPath "msDeployStdErr.log" $True 2597 | 2598 | New-Message $InfoMsg "Taking a snapshot of the website... This might take a few minutes." $MigrationRunLogFile 2599 | $encryptPassword = Get-RandomPassword 2600 | 2601 | try { 2602 | $msDeployZipPath = Generate-MSDeploySourceBundle $glb_websiteToMigrate $appBundleFolderPath $encryptPassword $msDeployStdOutLog $msDeployStdErrLog 2603 | $Global:siteFolder = New-Folder $appBundleFolderPath "site_content" $True 2604 | Unzip-Folder $msDeployZipPath $siteFolder 2605 | Delete-Item $msDeployZipPath 2606 | Remove-SSLCertificate $siteFolder 2607 | } catch { 2608 | New-Message $ErrorMsg "Error generating a website snapshot. For troubleshooting instructions, see the Readme document of the migration assistant GitHub repository or contact AWS migration support team." $MigrationRunLogFile 2609 | New-Message $FatalMsg $error[0].Exception.Message $MigrationRunLogFile 2610 | Exit-WithError 2611 | } 2612 | 2613 | 2614 | # Collect & list possible connection strings 2615 | 2616 | New-Message $InfoMsg " " $MigrationRunLogFile 2617 | New-Message $InfoMsg "Automatically discovering possible connection strings... This might take a few minutes." $MigrationRunLogFile 2618 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2619 | New-Message $InfoMsg " " $MigrationRunLogFile 2620 | 2621 | $standardConnStrings = Get-DBConnectionStrings $glb_websiteToMigrate 2622 | $possibleConnStrings = Get-PossibleDBConnStrings $siteFolder 2623 | 2624 | $connStringsTable = @{ } 2625 | $connStringsNum = 1 2626 | if ($standardConnStrings) { 2627 | if ($standardConnStrings -is [system.array]) { 2628 | foreach ($connStr in $standardConnStrings) { 2629 | $LogMsg = "Discovered connection string in location : $($connStr.Path) :" 2630 | New-Message $InfoMsg $LogMsg $MigrationRunLogFile 2631 | $connStringsTable[$connStringsNum] = $connStr 2632 | $connStr= $connStr.Line 2633 | $connStr = $connStr.TrimStart() 2634 | $connStringsNum = $connStringsNum + 1 2635 | $LogMsg = "[" + $connStringsNum + "] : " + $string 2636 | New-Message $ConsoleOnlyMsg $LogMsg $MigrationRunLogFile 2637 | New-Message $InfoMsg " " $MigrationRunLogFile 2638 | } 2639 | } else { 2640 | New-Message $InfoMsg $standardConnStrings $MigrationRunLogFile 2641 | New-Message $InfoMsg " " $MigrationRunLogFile 2642 | } 2643 | } 2644 | 2645 | if ($possibleConnStrings) { 2646 | if ($possibleConnStrings -is [system.array]) { 2647 | foreach ($connStr in $possibleConnStrings) { 2648 | $connStringsTable[$connStringsNum] = $connStr 2649 | $LogMsg = "Discovered connection string in location : $($connStr.Path)" 2650 | $connStr = $connStr.Line 2651 | $connStr = $connStr.TrimStart() 2652 | New-Message $InfoMsg $LogMsg $MigrationRunLogFile 2653 | $LogMsg = "[" + $connStringsNum + "] : " + $connStr 2654 | New-Message $ConsoleOnlyMsg $LogMsg $MigrationRunLogFile 2655 | New-Message $InfoMsg " " $MigrationRunLogFile 2656 | $connStringsNum = $connStringsNum + 1 2657 | } 2658 | } else { 2659 | New-Message $InfoMsg $possibleConnStrings $MigrationRunLogFile 2660 | New-Message $InfoMsg " " $MigrationRunLogFile 2661 | } 2662 | } 2663 | 2664 | if ((-Not $standardConnStrings) -and (-Not $possibleConnStrings)) { 2665 | New-Message $InfoMsg "The migration assistant didn't find any connection strings." $MigrationRunLogFile 2666 | } 2667 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2668 | 2669 | 2670 | # Collect connection strings from user 2671 | 2672 | $canAutoUpdateConnectionStrings = $True 2673 | $connectionStringNumber = 1 2674 | $connectionStringMatches = @{} 2675 | while ($True) { 2676 | if ($NonInteractiveMode) { 2677 | $userInputConnectionStringNum = $mfarg_userInputConnectionStringNum 2678 | } else { 2679 | $userInputConnectionStringNum = Get-SensitiveUserInputString $MigrationRunLogFile "Enter the number of the connection string you would like to update, or press ENTER" 2680 | } 2681 | 2682 | if (!$userInputConnectionStringNum) { 2683 | break 2684 | } 2685 | $userInputString = $connStringsTable[[int]$userInputConnectionStringNum].Line 2686 | 2687 | if (!$userInputString) { 2688 | break 2689 | } else { 2690 | New-Message $InfoMsg "Looking for this connection string in the site directory..." $MigrationRunLogFile 2691 | $matchedStrings = Get-ChildItem -Path $siteFolder -Recurse -exclude "*.exe","*.dll" | Select-String -Pattern $userInputString -SimpleMatch 2692 | if (-Not $matchedStrings) { 2693 | New-Message $ErrorMsg "The migration assistant couldn't find this connection string in the project." $MigrationRunLogFile 2694 | if ($NonInteractiveMode) { 2695 | $userInputY = $mfarg_userinputY 2696 | } else { 2697 | $userInputY = Get-UserInputString $MigrationRunLogFile "Enter 'Y' to keep it on record (you will need to manually replace the connection strings), or anything else to re-enter the string" 2698 | } 2699 | if ($userInputY -eq "Y" -or $userInputY -eq "y") { 2700 | continue 2701 | } 2702 | $canAutoUpdateConnectionStrings = $False 2703 | } 2704 | $matchList = @() 2705 | foreach ($match in $matchedStrings) { 2706 | $matchList += $match 2707 | } 2708 | $connectionStringMatches.Add($userInputString, $matchList) 2709 | $connectionStringNumber += 1 2710 | } 2711 | } 2712 | $connectionStringNumber -= 1 2713 | New-Message $InfoMsg " " $MigrationRunLogFile 2714 | New-Message $InfoMsg "Done. Collected $connectionStringNumber connection strings to update." $MigrationRunLogFile 2715 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2716 | 2717 | 2718 | # DB migration script integration 2719 | 2720 | if ($connectionStringMatches.Count -ne 0) { 2721 | 2722 | New-Message $InfoMsg "Migrate your database separately, if needed" $MigrationRunLogFile 2723 | if ($NonInteractiveMode) { 2724 | continue 2725 | } else { 2726 | $userInputEnter = Get-UserInputString $MigrationRunLogFile "Press ENTER to continue to the next step" 2727 | } 2728 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2729 | $userInputEnter = New-Message $InfoMsg "Continuing with connection string update..." $MigrationRunLogFile 2730 | 2731 | # Update the website with user provided connection strings 2732 | 2733 | $manualReplacement = $False 2734 | if (-Not $canAutoUpdateConnectionStrings) { 2735 | New-Message $InfoMsg "The migration assistant found unidentifiable connection strings." $MigrationRunLogFile 2736 | $manualReplacement = $True 2737 | } else { 2738 | $userInputM = Get-UserInputString $MigrationRunLogFile "Enter 'M' to manually edit the file containing the connection string, or paste the replacement string [M]" 2739 | if (!$userInputM) { 2740 | $userInputM = "M" 2741 | } 2742 | if ($userInputM -eq "M" -or $userInputM -eq "m") { 2743 | $manualReplacement = $True 2744 | } 2745 | } 2746 | 2747 | if ($manualReplacement) { 2748 | New-Message $InfoMsg "Update the connection string manually in the following locations:" $MigrationRunLogFile 2749 | foreach ($connStrMatch in $connectionStringMatches.GetEnumerator()) { 2750 | New-Message $InfoMsg $($connStrMatch.Value) $MigrationRunLogFile 2751 | } 2752 | if ($NonInteractiveMode) { 2753 | continue 2754 | } else { 2755 | $userInputEnter = Get-UserInputString $MigrationRunLogFile "Press ENTER when you're done" 2756 | } 2757 | } else { 2758 | foreach ($key in $connectionStringMatches.Keys) { 2759 | New-Message $InfoMsg "Provide a replacement connection string for:" $MigrationRunLogFile 2760 | New-Message $ConsoleOnlyMsg " $key" $MigrationRunLogFile 2761 | Invoke-CommandsWithRetry 99 $MigrationRunLogFile { 2762 | if ($NonInteractiveMode) { 2763 | $userInputString = $mfarg_userInputConnectionString 2764 | } else { 2765 | $userInputString = Get-SensitiveUserInputString $MigrationRunLogFile "Enter a new connection string" 2766 | } 2767 | $verified = Test-DBConnection $userInputString 2768 | if (-Not $verified) { 2769 | New-Message $ErrorMsg "The migration assistant can't verify the connection string. Type `"K`" to keep the connection string anyway." $MigrationRunLogFile 2770 | if ($NonInteractiveMode) { 2771 | $userInputK = $mfarg_userinputK 2772 | } else { 2773 | $userInputK = Get-UserInputString $MigrationRunLogFile "Press ENTER to retry" 2774 | } 2775 | if ($userInputK -ne "K" -or $userInputK -ne "k") { 2776 | throw "Please re-enter the last connection string." 2777 | } 2778 | } 2779 | $Global:glb_newString = $userInputString 2780 | } 2781 | foreach ($match in $connectionStringMatches.$key) { 2782 | $filePath = $match.Path 2783 | Update-DBConnectionString $filePath $key $glb_newString 2784 | } 2785 | New-Message $InfoMsg "Replaced an old connection string." $MigrationRunLogFile 2786 | } 2787 | } 2788 | } else { 2789 | New-Message $InfoMsg "Your application doesn't have a database that needs to be migrated." $MigrationRunLogFile 2790 | } 2791 | New-Message $InfoMsg "Finished updating the connection string." $MigrationRunLogFile 2792 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2793 | 2794 | 2795 | # Convert to EB application bundle 2796 | 2797 | New-Message $InfoMsg "Converting the website to an Elastic Beanstalk deployment bundle... This might take a few minutes." $MigrationRunLogFile 2798 | $outputFolderName = "output" 2799 | $ebAppBundleFileName = "eb-application-bundle.zip" 2800 | 2801 | ConvertTo-EBApplicationFolder $appBundleFolderPath $glb_websiteToMigrate $encryptPassword 2802 | 2803 | $iisHardeningDetails = Join-Path $runDirectory "utils\templates\harden_iis.ps1" 2804 | 2805 | # TBD: Add IIS hardening later 2806 | #New-Message $InfoMsg "Would you like to perform optional IIS hardening on the Elastic Beanstalk application?" $MigrationRunLogFile 2807 | #New-Message $InfoMsg "Review the detailed settings before applying them. They might impact the website's functionality." $MigrationRunLogFile 2808 | #New-Message $InfoMsg "The settings can be viewed or modified at:" $MigrationRunLogFile 2809 | #New-Message $InfoMsg " $iisHardeningDetails" $MigrationRunLogFile 2810 | 2811 | #$userInputY = "No" 2812 | #$userInputY = Get-UserInputString $MigrationRunLogFile "Enter 'Y' to include IIS hardening settings [N]" 2813 | #if ($userInputY -eq "Y" -or $userInputY -eq "y") { 2814 | # Add-IISHardeningSettings $appBundleFolderPath 2815 | # New-Message $InfoMsg "Added IIS hardening settings to the deployment bundle." $MigrationRunLogFile 2816 | #} else { 2817 | # New-Message $InfoMsg "IIS hardening settings waren't added." $MigrationRunLogFile 2818 | #} 2819 | 2820 | # TBD: Add AD support later 2821 | #New-Message $InfoMsg "Would you like to join your Elastic Beanstalk application to an Active Directory?" $MigrationRunLogFile 2822 | #New-Message $InfoMsg "For instructions on extending your AD on AWS, see the [Active Directory] section in the Readme document of the migration assistant GitHub repository." $MigrationRunLogFile 2823 | 2824 | #$userInputY = "no" 2825 | #$userInputY = Get-UserInputString $MigrationRunLogFile "Enter 'Y' to join the application to Active Directory [N]" 2826 | #if ($userInputY -eq "Y" -or $userInputY -eq "y") { 2827 | # $ssmDocName = Get-UserInputString $MigrationRunLogFile "Enter the name of the AD-Joining SSM document" 2828 | # Add-ADJoiningSettings $appBundleFolderPath $ssmDocName 2829 | # New-Message $InfoMsg "Your application is configured to join an Active Directory. Use the advanced deployment mode." $MigrationRunLogFile 2830 | #} else { 2831 | # New-Message $InfoMsg "Skipped Active Directory configurations." $MigrationRunLogFile 2832 | #} 2833 | 2834 | $outputFolderPath = New-Folder $CurrentMigrationRunPath $outputFolderName $True 2835 | $ebAppBundleFile = Get-ZippedFolder $appBundleFolderPath $outputFolderPath $ebAppBundleFileName 2836 | 2837 | New-Message $InfoMsg "An application bundle was successfully generated." $MigrationRunLogFile 2838 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2839 | 2840 | $appBundleSize = (Get-Item $ebAppBundleFile).length 2841 | if ($appBundleSize -gt $ebAppBundleFileSizeLimit) { 2842 | New-Message $FatalMsg "The application bundle size is too large. Be sure to limit directory size to '$ebAppBundleFileSizeLimit'." $MigrationRunLogFile 2843 | New-Message $InfoMsg "Contact the AWS migration support team to migrate the application." $MigrationRunLogFile 2844 | Exit-WithError 2845 | } 2846 | 2847 | 2848 | # EB Migration 2849 | 2850 | New-Message $InfoMsg "AWS Elastic Beanstalk Deployment" $MigrationRunLogFile 2851 | New-Message $InfoMsg "------------------------------------------------------------------------------------------" $MigrationRunLogFile 2852 | $s3BucketToCleanUp = $Null 2853 | 2854 | # Basic deployment using AWS PowerShell Toolkit 2855 | 2856 | try { 2857 | Verify-CanCreateNewEIP 2858 | } catch { 2859 | New-Message $ErrorMsg "You've reached your Elastic IP address limit. Be sure the number of Elastic IP addresses in AWS Region '$glb_AwsRegion' is below your account's limit." $MigrationRunLogFile 2860 | New-Message $ErrorMsg "For a successful Elastic Beanstalk deployment, you must be able to allocate a new Elastic IP address to your account." $MigrationRunLogFile 2861 | $userConfirmation = Get-UserInputString $MigrationRunLogFile "Press ENTER after resolving this issue" 2862 | } 2863 | 2864 | Invoke-CommandsWithRetry 99 $MigrationRunLogFile { 2865 | if ($NonInteractiveMode) { 2866 | $Global:glb_ebAppName = $mfarg_glb_ebAppName 2867 | } else { 2868 | $Global:glb_ebAppName = Get-UserInputString $MigrationRunLogFile "Enter a unique name for your new Elastic Beanstalk application" 2869 | } 2870 | New-Message $InfoMsg "Creating a new Elastic Beanstalk application..." $MigrationRunLogFile 2871 | New-EBApplication -ApplicationName $glb_ebAppName 2872 | } 2873 | 2874 | New-Message $InfoMsg "Elastic Beanstalk supports the following Windows Server versions: " $MigrationRunLogFile 2875 | $windowsVersions = @("2012", "2012 R2", "Core 2012 R2", "2016", "Core 2016", "2019", "Core 2019") 2876 | $windowsVersionNumber = 1 2877 | foreach ($windowsVersion in $windowsVersions) { 2878 | $LogMsg = "[" + $windowsVersionNumber + "] : Windows Server " + $windowsVersion 2879 | New-Message $ConsoleOnlyMsg $LogMsg $MigrationRunLogFile 2880 | $windowsVersionNumber = $windowsVersionNumber + 1 2881 | } 2882 | 2883 | $platformNameFilter = New-Object Amazon.ElasticBeanstalk.Model.PlatformFilter -Property @{Operator='contains';Type='PlatformName';Values='Windows Server'} 2884 | $platformOwnerFilter = New-Object Amazon.ElasticBeanstalk.Model.PlatformFilter -Property @{Operator='=';Type='PlatformOwner';Values='AWSElasticBeanstalk'} 2885 | $platformStatusFilter = New-Object Amazon.ElasticBeanstalk.Model.PlatformFilter -Property @{Operator='=';Type='PlatformStatus';Values='Ready'} 2886 | $ebPlatformVersions = Get-EBPlatformVersion -Filter $platformNameFilter,$platformOwnerFilter,$platformStatusFilter 2887 | 2888 | $EBtag = New-Object Amazon.ElasticBeanstalk.Model.Tag 2889 | $EBtag.Key = "createdBy" 2890 | $EBtag.Value = "MigrateIISWebsiteToElasticBeanstalk.ps1" 2891 | $environmentName = $glb_ebAppName + "-env" 2892 | 2893 | Invoke-CommandsWithRetry 99 $MigrationRunLogFile { 2894 | $userInputWindowsVersion = $windowsVersions[0] 2895 | 2896 | if ($NonInteractiveMode) { 2897 | $userInputWindowsStringNum = $mfarg_userInputWindowsStringNum 2898 | } else { 2899 | $userInputWindowsStringNum = Get-UserInputString $MigrationRunLogFile "Enter the number of the Windows version for your Elastic Beanstalk environment [1]" 2900 | } 2901 | if (!$userInputWindowsStringNum){ 2902 | $userInputWindowsStringNum = 1 2903 | } 2904 | Validate-NumberedListUserInput $([int]$userInputWindowsStringNum) 1 $windowsVersions.Count 2905 | $userInputWindowsVersion = $windowsVersions[([int]$userInputWindowsStringNum)-1] 2906 | New-Message $InfoMsg " " $MigrationRunLogFile 2907 | 2908 | foreach ($ebPlatformVersion in $ebPlatformVersions) { 2909 | if ($userInputWindowsVersion -and $($ebPlatformVersion.PlatformArn).contains($userInputWindowsVersion+"/")){ 2910 | $platformArn = $ebPlatformVersion.PlatformArn 2911 | break 2912 | } 2913 | } 2914 | 2915 | $platformArnPrefix = "platform/" 2916 | $userFriendlyEbPlatformVersion = $platformArn.substring($platformArn.IndexOf($platformArnPrefix)+$platformArnPrefix.length) 2917 | New-Message $InfoMsg "The latest Elastic Beanstalk platform for Windows Server $userInputWindowsVersion is: $userFriendlyEbPlatformVersion" $MigrationRunLogFile 2918 | New-Message $InfoMsg "To learn more about Elastic Beanstalk platforms, see:" $MigrationRunLogFile 2919 | New-Message $InfoMsg " https://docs.aws.amazon.com/elasticbeanstalk/latest/platforms/platforms-supported.html#platforms-supported.net" $MigrationRunLogFile 2920 | New-Message $InfoMsg " " $MigrationRunLogFile 2921 | 2922 | if ($NonInteractiveMode) { 2923 | $instanceType = $mfarg_instanceType 2924 | } else { 2925 | $instanceType = Get-UserInputString $MigrationRunLogFile "Enter the instance type [t3.medium]" 2926 | 2927 | } 2928 | if (!$instanceType) { 2929 | $instanceType = "t3.medium" 2930 | } 2931 | 2932 | New-Message $InfoMsg " " $MigrationRunLogFile 2933 | $environmentTypeOptions = @("SingleInstance", "LoadBalanced") 2934 | $optionNumber = 1 2935 | New-Message $InfoMsg "Elastic Beanstalk supports the following environment types:" $MigrationRunLogFile 2936 | foreach ($environmentTypeOption in $environmentTypeOptions) { 2937 | $LogMsg = "[" + $optionNumber + "] : " + $environmentTypeOption 2938 | New-Message $ConsoleOnlyMsg $LogMsg $MigrationRunLogFile 2939 | $optionNumber = $optionNumber + 1 2940 | } 2941 | New-Message $InfoMsg " " $MigrationRunLogFile 2942 | if ($NonInteractiveMode) { 2943 | $userInputEnvironmentTypeNum = $mfarg_userInputEnvironmentTypeNum 2944 | } else { 2945 | $userInputEnvironmentTypeNum = Get-UserInputString $MigrationRunLogFile "Enter the environment type [1]" 2946 | } 2947 | 2948 | if (!$userInputEnvironmentTypeNum){ 2949 | $userInputEnvironmentTypeNum = 1 2950 | } 2951 | Validate-NumberedListUserInput $([int]$userInputEnvironmentTypeNum) 1 $environmentTypeOptions.Count 2952 | $environmentType = $environmentTypeOptions[([int]$userInputEnvironmentTypeNum)-1] 2953 | 2954 | New-Message $InfoMsg "Creating a new Elastic Beanstalk environment using platform arn '$platformArn', environment type '$environmentType', and instance type '$instanceType'" $MigrationRunLogFile 2955 | $instanceProfileOptionSetting = New-Object Amazon.ElasticBeanstalk.Model.ConfigurationOptionSetting -ArgumentList aws:autoscaling:launchconfiguration,IamInstanceProfile,$DefaultElasticBeanstalkInstanceProfileName 2956 | $instanceTypeOptionSetting = New-Object Amazon.ElasticBeanstalk.Model.ConfigurationOptionSetting -ArgumentList aws:autoscaling:launchconfiguration,InstanceType,$instanceType 2957 | $serviceRoleOptionSetting = New-Object Amazon.ElasticBeanstalk.Model.ConfigurationOptionSetting -ArgumentList aws:elasticbeanstalk:environment,ServiceRole,$DefaultElasticBeanstalkServiceRoleName 2958 | $environmentTypeOptionSetting = New-Object Amazon.ElasticBeanstalk.Model.ConfigurationOptionSetting -ArgumentList aws:elasticbeanstalk:environment,EnvironmentType,$environmentType 2959 | $enhancedHealthReportingOptionSetting = New-Object Amazon.ElasticBeanstalk.Model.ConfigurationOptionSetting -ArgumentList aws:elasticbeanstalk:healthreporting:system,SystemType,enhanced 2960 | $disableIMDSv1 = New-Object Amazon.ElasticBeanstalk.Model.ConfigurationOptionSetting -ArgumentList aws:autoscaling:launchconfiguration,DisableIMDSv1,$true 2961 | 2962 | $optionSettings = $instanceProfileOptionSetting,$instanceTypeOptionSetting,$serviceRoleOptionSetting,$environmentTypeOptionSetting,$enhancedHealthReportingOptionSetting,$disableIMDSv1 2963 | 2964 | New-EBEnvironment -ApplicationName $glb_ebAppName -EnvironmentName $environmentName -PlatformArn $platformArn -OptionSetting $optionSettings -Tag $EBTag 2965 | } 2966 | 2967 | $versionLabel = $MigrationRunId + "-vl" 2968 | $ebS3Bucket = New-TempS3Bucket $glb_AwsRegion 2969 | 2970 | $ebS3Key = Split-Path $ebAppBundleFile -Leaf 2971 | Upload-FileToS3Bucket $ebAppBundleFile $ebS3Bucket $glb_AwsRegion 2972 | New-Message $InfoMsg "Creating a new Elastic Beanstalk application version in application '$glb_ebAppName' with version label '$versionLabel' and S3 bucket '$ebS3Bucket'." $MigrationRunLogFile 2973 | New-EBApplicationVersion -ApplicationName $glb_ebAppName -VersionLabel $versionLabel -SourceBundle_S3Bucket $ebS3Bucket -SourceBundle_S3Key $ebS3Key -Tag $EBtag 2974 | 2975 | New-Message $InfoMsg "Updating the Elastic Beanstalk environment... This might take a few minutes." $MigrationRunLogFile 2976 | $environmentReady = $False 2977 | $waitTime = (Date).AddMinutes(10) 2978 | while ((Date) -lt $waitTime) { 2979 | try{ 2980 | Update-EBEnvironment -ApplicationName $glb_ebAppName -EnvironmentName $environmentName -VersionLabel $versionLabel 2981 | $environmentReady = $true 2982 | break 2983 | } catch { 2984 | Start-Sleep -Milliseconds 30000 # sleep for 30 seconds 2985 | Append-DotsToLatestMessage 1 2986 | } 2987 | } 2988 | $env = Get-EBEnvironment -ApplicationName $glb_ebAppName -EnvironmentName $environmentName -Region $glb_AwsRegion 2989 | $Global:glb_EBEnvID = $env.EnvironmentId 2990 | $s3BucketToCleanUp = $ebS3Bucket 2991 | 2992 | # Post deployment operations 2993 | 2994 | New-Message $InfoMsg "Waiting for the Elastic Beanstalk application to launch... This might take a few minutes." $MigrationRunLogFile 2995 | $waitTime = (Date).AddMinutes(30) 2996 | $deploymentSucceeded = $False 2997 | $firstTimeStatusGreen = $True 2998 | while ((Date) -lt $waitTime) { 2999 | $ebEnvironment = Get-EBEnvironment -EnvironmentId $glb_EBEnvID 3000 | $health = $ebEnvironment.Health 3001 | if ($health -eq "Green") { 3002 | if ($firstTimeStatusGreen) { 3003 | # wait one more round when status turns green for the first time 3004 | $firstTimeStatusGreen = $False 3005 | } else { 3006 | $deploymentSucceeded = $True 3007 | break 3008 | } 3009 | } 3010 | # else health = Grey: deployment in process. Red: error can happen, but it can still recuperate 3011 | Start-Sleep -Milliseconds 30000 # sleep for 30 seconds 3012 | Append-DotsToLatestMessage 1 3013 | } 3014 | 3015 | # if it's still Red or Yellow after 30 minutes, deployment failed 3016 | $ebEnvironment = Get-EBEnvironment -EnvironmentId $glb_EBEnvID 3017 | $health = $ebEnvironment.Health 3018 | if ($health -eq "Red" -or $health -eq "Yellow") { 3019 | $deploymentSucceeded = $False 3020 | break 3021 | } 3022 | 3023 | if ($s3BucketToCleanUp -and $DeleteTempS3Buckets) { 3024 | Delete-TempS3Bucket $s3BucketToCleanUp 3025 | } 3026 | 3027 | if ($deploymentSucceeded) { 3028 | $ebEnvironment = Get-EBEnvironment -EnvironmentId $glb_EBEnvID 3029 | $applicationURL = $ebEnvironment.CNAME 3030 | Write-Host " " 3031 | New-Message $InfoMsg "Note that it might take a few minutes for the application to be ready." $MigrationRunLogFile 3032 | New-Message $InfoMsg "Application URL:" $MigrationRunLogFile 3033 | New-Message $InfoMsg " $applicationURL" $MigrationRunLogFile 3034 | New-Message $InfoMsg " " $MigrationRunLogFile 3035 | New-Message $InfoMsg "The Elastic Beanstalk deployment succeeded. Your web application is now hosted in AWS at '$applicationURL'." $MigrationRunLogFile 3036 | } else { 3037 | New-Message $ErrorMsg "The Elastic Beanstalk deployment failed. You can check the deployment log in the Elastic Beanstalk console." $MigrationRunLogFile 3038 | } 3039 | } 3040 | Exit-WithoutError 3041 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Windows Web Application Migration Assistant for AWS Elastic Beanstalk 2 | 3 | ### Overview 4 | The Windows Web Application Migration Assistant for AWS Elastic Beanstalk is an interactive PowerShell utility that migrates [ASP.NET](https://dotnet.microsoft.com/apps/aspnet) and [ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-3.1) applications from on-premises IIS Windows servers to Elastic Beanstalk. The migration assistant is able to migrate an entire website and its configuration to Elastic Beanstalk with minimal or no changes to the application. After the assistant migrates the application, Elastic Beanstalk automatically handles the ongoing details of capacity provisioning, load balancing, auto-scaling, application health monitoring, and applying patches and updates to the underlying platform. If you need to also migrate a database associated with your web application, you can separately use [AWS Database Migration Service](https://aws.amazon.com/dms/), [CloudEndure Migration](https://aws.amazon.com/cloudendure-migration/), or the [Windows to Linux Replatforming Assistant for Microsoft SQL Server Databases](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/replatform-sql-server.html). 5 | 6 | You can watch a demo video of the migration assistant [here](https://www.youtube.com/watch?v=Q-YnE5EA1-0&feature=youtu.be). 7 | 8 | To try out the migration assistant, run the [Migration Tutorial](https://aws.amazon.com/getting-started/hands-on/migrate-aspnet-web-application-elastic-beanstalk/) to migrate a sample ASP.NET website to Elastic Beanstalk. 9 | 10 | ### Migration Assistant Prerequisites 11 | The migration assistant runs under the Administrator role on the on-premises IIS Windows server. Below is a list of software dependencies for the assistant: 12 | 13 | 1. Internet Information Services (IIS) version 8.0 or above running on Windows Server 2012 or above 14 | 1. [MS PowerShell version 3.0](https://www.microsoft.com/en-us/download/details.aspx?id=34595) or above 15 | 1. [Microsoft Web Deploy version 3.6](https://www.iis.net/downloads/microsoft/web-deploy) or above 16 | 1. [AWSPowerShell module for MS PowerShell](https://www.powershellgallery.com/packages/AWSPowerShell/3.3.498.0) 17 | 1. .NET Framework 4.x, 2.0, 1.x or .NET Core 3.0.0, 2.2.8, 2.1.14 18 | 1. WebAdministration module for MS PowerShell. You can check for this dependency by invoking PowerShell command "Import-Module WebAdministration" 19 | 1. The server needs full internet access to AWS. 20 | 21 | ### Setting Up 22 | 1. Create a new IAM user (for example, `MigrationUser`) for the Elastic Beanstalk migration using the [AWS IAM console](https://console.aws.amazon.com/iam/home). 23 | 1. Attach the following AWS-managed policies to the IAM user: (1) `IAMReadOnlyAccess` (2) `AdministratorAccess-AWSElasticBeanstalk` (3) `AmazonS3FullAccess`. Assign both Programmatic access and AWS Management Console access to the IAM user. Before finishing the user creation, obtain the user's AccessKey and SecretKey from the console. For instructions on creating a new user, see [Creating an IAM User in Your AWS Account](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html) in the *AWS Identity and Access Management User Guide*. Open a PowerShell terminal on your on-premises Windows Server, where the website is hosted, and invoke the following two commands. 24 | ``` 25 | PS C:\> Import-Module AWSPowerShell 26 | PS C:\> Set-AWSCredential -AccessKey {access_key_of_the_user} -SecretKey {secret_key_of_the_user} -StoreAs {profile_name} -ProfileLocation {optional - path_to_the_new_profile_file} 27 | ``` 28 | The parameter `{profile_name}` refers to the IAM user, and the optional parameter `{path_to_the_new_profile_file}` refers to the full physical path of the new profile file. 29 | For CLI reference, see [AWS Tools for PowerShell](https://aws.amazon.com/powershell/). 30 | 1. On GitHub, use the **Clone or download** menu to either clone this repository or download a ZIP bundle of it and extract the ZIP file. Place the migration assistant on the local server, in a new folder on a disk that has more than 1 GB free space. 31 | 1. [Optional] Edit the `settings.txt` JSON file, and set the following 2 variables: (1) `defaultAwsProfileFileLocation : {path_to_the_new_profile_file}` (2) `defaultAwsProfileName : {profile_name}` 32 | 1. If you have a database associated with your application, you can migrate it before migrating the web application using [AWS Database Migration Service](https://aws.amazon.com/dms/), [CloudEndure Migration](https://aws.amazon.com/cloudendure-migration/), or the [Windows to Linux Replatforming Assistant for Microsoft SQL Server Databases](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/replatform-sql-server.html). 33 | 34 | ### Application Migration Workflow 35 | Here's an overview of the migration assistant's workflow: 36 | 37 | 1. Discover local websites. 38 | 1. Select site to migrate. 39 | 1. Discover database connection strings. 40 | 1. Update database connection strings. 41 | 1. Generate Elastic Beanstalk deployment bundle. 42 | 1. Deploy application to Elastic Beanstalk. 43 | 44 | ### Running the Migration Assistant 45 | Open a PowerShell terminal as Administrator and launch the `MigrateIISWebsiteToElasticBeanstalk.ps1` script. 46 | 47 | ``` 48 | PS C:\> .\MigrateIISWebsiteToElasticBeanstalk.ps1 49 | ``` 50 | 51 | The assistant prompts you for the location of your credentials file. Press ENTER to skip if you didn't enter a profile location when you ran `Set-AWSCredential` during setup, otherwise provide the path of your credentials. 52 | 53 | ``` 54 | Please provide your AWS profile to use for the migration 55 | AWS profile file location, like c:\aws\credentials (press ENTER for default value): 56 | ``` 57 | 58 | Enter the name of the profile you created when you ran `Set-AWSCredential` during setup. 59 | 60 | ``` 61 | Enter your AWS Profile Name: 62 | ``` 63 | 64 | Enter the AWS Region where you'd like your Elastic Beanstalk environment to run. For example: __us-west-1__. 65 | For a list of AWS Regions where Elastic Beanstalk is available, see [AWS Elastic Beanstalk Endpoints and Quotas](https://docs.aws.amazon.com/general/latest/gr/elasticbeanstalk.html) in the *AWS General Reference*. 66 | 67 | ``` 68 | Enter the AWS Region (default us-east-1) : 69 | ``` 70 | 71 | The assistant then discovers any websites running on your IIS server and lists them, as in the below example. 72 | 73 | ``` 74 | The migration assistant discovered website(s) on the local server EC2AMAZ-9VP6LPT 75 | [0] - Default Web Site 76 | [1] - nop4.2 77 | ``` 78 | 79 | Enter the number of the website you’d like to migrate. 80 | 81 | ``` 82 | Enter the number of the website to migrate: (default 0): 83 | ``` 84 | 85 | The assistant takes a snapshot of your environment and lists any connection strings used by your application. To update a connection string, enter its number, or press ENTER to skip. 86 | 87 | ``` 88 | Enter the number of the connection string you would like to update, or press ENTER: 89 | ``` 90 | 91 | The assistant then pauses and allows you to migrate your database, in case you want to do it now and interactively provide new connection strings. Press ENTER to continue. 92 | ``` 93 | Please separately migrate your database, if needed. 94 | ``` 95 | The assistant then prompts you to update any connection strings selected above. If you press `M`, you can update the string manually by editing it in the file path provided by the migration assistant. Otherwise, paste the contents of the new connection string and press ENTER. 96 | 97 | ``` 98 | Enter "M" to manually edit the file containing the connection string, or paste the replacement stri 99 | d press ENTER (default M) : 100 | ``` 101 | 102 | 103 | Next, name your new Elastic Beanstalk application. 104 | 105 | ``` 106 | Please enter the name of your new EB application. 107 | The name has to be unique: 108 | ``` 109 | 110 | Enter instance type that your application will run on. See [Amazon EC2 Instance Types](https://aws.amazon.com/ec2/instance-types/) for a complete list. 111 | 112 | ``` 113 | Enter the instance type (default t3.medium) : 114 | ``` 115 | 116 | Lastly, select the Elastic Beanstalk platform from the below list. This platform should match the version of Windows Server that is currently running on your host system. 117 | 118 | ``` 119 | Elastic Beanstalk supports the following Windows Server versions: 120 | [1] : Windows Server 2012 121 | [2] : Windows Server 2012 R2 122 | [3] : Windows Server Core 2012 R2 123 | [4] : Windows Server 2016 124 | [5] : Windows Server Core 2016 125 | [6] : Windows Server 2019 126 | [7] : Windows Server Core 2019 127 | Enter the number of the Windows version for your Elastic Beanstalk environment [1]: 128 | ``` 129 | 130 | 131 | The migration assistant then migrates your application to Elastic Beanstalk. 132 | 133 | 134 | ### Readiness Report Only Mode 135 | 136 | If you only want to get a readiness report, you can execute the following command: 137 | 138 | 139 | ``` 140 | PS C:\> .\MigrateIISWebsiteToElasticBeanstalk.ps1 -ReportOnly True 141 | ``` 142 | 143 | Upon completion, you will have a migration_readiness_report.json available. Readiness report only mode does not require AWS Credentials. 144 | 145 | #### Alternative Deployment Method 146 | Alternately, you can upload the deployment bundle manually to Elastic Beanstalk once it has been generated by the Migration Assistant. To do so, quit the Migration Assistant after the line "An application bundle was successfully generated." and follow the below steps: 147 | 148 | 1. Sign in to your AWS console and go to the Elastic Beanstalk app creation page: https://us-east-1.console.aws.amazon.com/elasticbeanstalk/home?region=us-east-1#/gettingStarted 149 | 2. Select the AWS region you want to migrate the application to. 150 | 3. Create the app by following the instructions on the page. Select "Upload your code" for "Application code" section, and upload the deployment bundle generated by the Migration Assistant. 151 | 4. The deployment bundle is a zip file which can be found under folder MigrationRun-xxxxxxx/output 152 | 153 | If you’re migrating an ASP.NET website that is actively maintained and updated, you can alternately publish your website to Elastic Beanstalk and update it using the [Elastic Beanstalk plugin for Visual Studio](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_NET.quickstart.html), APIs, SDKs, the AWS CLI, or the Elastic Beanstalk CLI(https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3.html). 154 | 155 | 156 | ### Migration Limitations 157 | 1. Software dependencies on the local server (outside of the website directory, for example GACs) aren’t detected or migrated. 158 | 1. There can be at most one HTTP port and at most one HTTPS port bound to the website. When the site is migrated to Elastic Beanstalk, the ports are bound to ports 80 and 443, respectively. 159 | 1. To migrate the existing SSL certificates, manually export them from the IIS server, import them to AWS Certificate Manager (ACM), and then configure them to the Elastic Beanstalk load balancer. For detailed instructions, see [Configuring HTTPS for Your Elastic Beanstalk Environment](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/configuring-https.html) in the AWS Elastic Beanstalk Developer Guide. 160 | 1. Applications with Active Directory aren’t currently supported. 161 | 162 | 163 | ### Troubleshooting 164 | 1. You see this error message: `Exception calling "ExtractToDirectory" with "2" argument(s): "Could not find a part of the path..."`: This means that the path to the file in generated Elastic Beanstalk deployment bundle is too long. In this case, move the migration assistant folder to the root directory of the hard drive and shorten the name of the folder (e.g. from `AWSWebAppMigrationAssistant` to `AMS`). 165 | 1. Your migrated application has trouble accessing its database: If the database is in AWS, please make sure that its security group allows traffic between the migrated Elastic Beanstalk instance (you can find it in EC2 in the same AWS region) and itself. If the database isn’t in AWS, configure the firewall to allow traffic from the Elastic Beanstalk instance. 166 | 1. The migration assistant shows the migration as complete, but the Elastic Beanstalk environment is disabled with the following error messages: `This environment is terminated and cannot be modified. It will remain visible for about an hour. ERROR Failed to launch environment. ERROR Environment must have instance profile associated with it.`: This can happen due to an issue on the Elastic Beanstalk side. To resolve the issue, deploy the generated source bundle to Elastic Beanstalk manually. 167 | 1. You receive an `InvalidAddress.NotFound` or `AddressLimitExceeded` error: Make sure that the Elastic IP limit isn’t exceeded in the AWS region(s) you intend to migrate the website to. For more information, see [How do I troubleshoot errors with Elastic IP addresses in Amazon VPC?](https://aws.amazon.com/premiumsupport/knowledge-center/unlock-move-recover-troubleshoot-eip/) 168 | 1. If the migration successfully completes, but you later see an error in the Elastic Beanstalk event console such as: 169 | ''' 170 | Error messages running the command: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy unrestricted -NonInteractive -NoProfile -Command "& { & \"C:\staging\scripts/site_post_install.ps1\"; exit $LastExitCode }" Get-WebFilePath : Cannot find path 'IIS:\Sites\EBSDemo' because it does not exist. At C:\staging\scripts\site_post_install.ps1:14 char:20 + $websiteFilePath = ...message truncated, view the environment logs for full error message details. 171 | ''' 172 | Confirm that you have migrated your application to an Elastic Beanstalk platform with the same version of Windows Server (e.g. migrate from Windows Server 2016 to Windows Server 2016). 173 | 1. If you see a default ASP.NET website presented when you navigate to your environment’s web page, and your website relies on a database, it may mean the migrated website is unable to connect to your database. 174 | 1. Confirm that you correctly made any changes to the connection string during migration. 175 | 1. If your database runs on an EC2 instance, make sure its security group allows inbound traffic (port 1433 for SQL Server) from your new environment’s security group. 176 | 177 | ### License 178 | This project is licensed under the [Apache-2.0 License](https://www.apache.org/licenses/LICENSE-2.0). 179 | -------------------------------------------------------------------------------- /cfn_stack/WWAMALab.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Ec2Instance: 3 | Type: AWS::EC2::Instance 4 | Properties: 5 | ImageId: ami-048fb16ca78a503fe 6 | InstanceType: t3.medium 7 | Tags: 8 | - Key: Name 9 | Value: "WWAMA Lab" 10 | SecurityGroups: 11 | - !Ref MySecurityGroup 12 | KeyName: 13 | Ref: "KeyPair" 14 | UserData: 15 | !Base64 | 16 | 17 | (new-object net.webclient).DownloadFile('https://github.com/awslabs/windows-web-app-migration-assistant/archive/master.zip','c:\wwama.zip') 18 | 19 | MySecurityGroup: 20 | Type: AWS::EC2::SecurityGroup 21 | Properties: 22 | GroupName: SimpleWinGroup 23 | GroupDescription: Enable RDP traffic via port 3389 24 | SecurityGroupIngress: 25 | - IpProtocol: tcp 26 | FromPort: 3389 27 | ToPort: 3389 28 | CidrIp: 0.0.0.0/0 29 | Parameters: 30 | KeyPair: 31 | Description: Key Pair for this Instance 32 | Type: "AWS::EC2::KeyPair::KeyName" 33 | Outputs: 34 | ServerDns: 35 | Value: !GetAtt 36 | - Ec2Instance 37 | - PublicDnsName 38 | -------------------------------------------------------------------------------- /utils/iam_trust_relationship_eb.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "Service": "elasticbeanstalk.amazonaws.com" 9 | }, 10 | "Action": "sts:AssumeRole", 11 | "Condition": { 12 | "StringEquals": { 13 | "sts:ExternalId": "elasticbeanstalk" 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /utils/iam_trust_relationship_ec2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2008-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "ec2.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /utils/settings.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Custom setting #1": "Set this flag to True to see exact timestamps for each console message.", 3 | "displayTimestampsInConsole": "False", 4 | 5 | "Custom setting #2": "Set this variable to use as the default AWS profile file location. Note: use double backslash (\\) instead of single", 6 | "defaultAwsProfileFileLocation": "", 7 | 8 | "Custom setting #3": "Set this variable to use as the default AWS profile name.", 9 | "defaultAwsProfileName": "", 10 | 11 | "Custom setting #4": "Set this flag to True to continue the migration even if there is an incompatibility detected", 12 | "ignoreMigrationReadinessWarnings": "False", 13 | 14 | "Custom setting #5": "Set this flag to True to delete all S3 buckets created during the migration after the deployment", 15 | "deleteTempS3Buckets": "False" 16 | } 17 | -------------------------------------------------------------------------------- /utils/templates/aws-windows-deployment-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "deployments": { 4 | "custom": [ 5 | { 6 | "name": "{REPLACE_WITH_WEBSITE_NAME}", 7 | "scripts": { 8 | "install": { 9 | "file": "scripts/site_install.ps1" 10 | }, 11 | "postInstall": { 12 | "file": "scripts/site_post_install.ps1" 13 | }, 14 | "restart": { 15 | "file": "scripts/site_restart.ps1" 16 | }, 17 | "uninstall": { 18 | "file": "scripts/site_uninstall.ps1" 19 | } 20 | } 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utils/templates/harden_iis.ps1: -------------------------------------------------------------------------------- 1 | # This script contains 18 IIS hardening settings that improve the Elastic Beanstalk server security 2 | # This is an experimental feature and it might decrease the performance of the ASP.NET application being migrated in some rare cases 3 | $ErrorActionPreference = "Continue" 4 | 5 | # (L1) Ensure 'directory browsing' is set to disabled 6 | Set-WebConfigurationProperty -Filter system.webserver/directorybrowse -PSPath iis:\ -Name Enabled -Value False 7 | 8 | # (L1) Ensure WebDav feature is disabled 9 | Remove-WindowsFeature Web-DAV-Publishing 10 | 11 | # (L1) Ensure 'global authorization rule' is set to restrict access 12 | Remove-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/authorization" -name "." -AtElement @{users='*';roles='';verbs=''} 13 | Add-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/authorization" -name "." -value @{accessType='Allow';roles='Administrators'} 14 | 15 | # (L1) Ensure IIS HTTP detailed errors are hidden from displaying remotely 16 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/httpErrors" -name "errorMode" -value "DetailedLocalOnly" 17 | 18 | # (L1) Ensure 'MachineKey validation method - .Net 4.5' is configured 19 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT' -filter "system.web/machineKey" -name "validation" -value "AES" 20 | 21 | # (L1) Ensure Double-Encoded requests will be rejected 22 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/requestFiltering" -name "allowDoubleEscaping" -value "False" 23 | 24 | # (L1) Ensure 'HTTP Trace Method' is disabled 25 | Add-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/requestFiltering/verbs" -name "." -value @{verb='TRACE';allowed='False'} 26 | 27 | # (L1) Ensure Handler is not granted Write and Script/Execute 28 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/handlers" -name "accessPolicy" -value "Read,Script" 29 | 30 | # (L1) Ensure 'notListedIsapisAllowed' is set to false 31 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/isapiCgiRestriction" -name "notListedIsapisAllowed" -value "False" 32 | 33 | # (L1) Ensure 'notListedCgisAllowed' is set to false 34 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/isapiCgiRestriction" -name "notListedCgisAllowed" -value "False" 35 | 36 | # (L2) Ensure 'debug' is turned off 37 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.web/compilation" -name "debug" -value "False" 38 | 39 | # (L2) Ensure ASP.NET stack tracing is not enabled 40 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.web/trace" -name "enabled" -value "False" 41 | 42 | # (L2) Ensure X-Powered-By Header is removed 43 | Remove-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webserver/httpProtocol/customHeaders" -name "." -AtElement @{name='X-Powered-By'} 44 | 45 | # (L2) Ensure Server Header is removed 46 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST/' -filter "system.webServer/security/requestFiltering" -name "removeServerHeader" -value "True" 47 | 48 | # (L2) Ensure 'maxAllowedContentLength' is configured 49 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/requestFiltering/requestLimits" -name "maxAllowedContentLength" -value 30000000 50 | 51 | # (L2) Ensure 'maxURL request filter' is configured 52 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/requestFiltering/requestLimits" -name "maxUrl" -value 4096 53 | 54 | # (L2) Ensure 'MaxQueryString request filter' is configured 55 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/requestFiltering/requestLimits" -name "maxQueryString" -value 2048 56 | 57 | # (L2) Ensure non-ASCII characters in URLs are not allowed 58 | Set-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.webServer/security/requestFiltering" -name "allowHighBitCharacters" -value "False" 59 | 60 | -------------------------------------------------------------------------------- /utils/templates/iis_hardening.config: -------------------------------------------------------------------------------- 1 | container_commands: 2 | 00-run-script: 3 | command: powershell.exe -ExecutionPolicy Bypass -Command ".\\.ebextensions\\harden_iis.ps1" 4 | waitAfterCompletion: 0 5 | 6 | -------------------------------------------------------------------------------- /utils/templates/site_install.ps1: -------------------------------------------------------------------------------- 1 | # This script installs website {REPLACE_WITH_WEBSITE_NAME} on the local IIS server using Web Deploy 2 | # (not used) encryptPassword={REPLACE_WITH_PASSWORD} 3 | 4 | if ($env:PROCESSOR_ARCHITECTURE -eq "x86") { 5 | # EB runs these scripts in 32-bit PowerShell - we need to force them to run in 64-bit 6 | & (Join-Path ($PSHOME -replace "syswow64", "sysnative") powershell.exe) -file ` 7 | (Join-Path $PSScriptRoot $MyInvocation.MyCommand) @args 8 | exit 9 | } 10 | 11 | $msDeployExe = "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe" # fixed for Elastic Beanstalk IIS instances 12 | $msDeployVerb = "-verb:sync" 13 | 14 | $msDeployStdOutPackagingTimeLog = "C:\Program Files\Amazon\ElasticBeanstalk\logs\eb_migration_msdeploy_p.stdout" 15 | $msDeployStdErrPackagingTimeLog = "C:\Program Files\Amazon\ElasticBeanstalk\logs\eb_migration_msdeploy_p.stderr" 16 | $msDeployStdOutDeploymentTimeLog = "C:\Program Files\Amazon\ElasticBeanstalk\logs\eb_migration_msdeploy_d.stdout" 17 | $msDeployStdErrDeploymentTimeLog = "C:\Program Files\Amazon\ElasticBeanstalk\logs\eb_migration_msdeploy_d.stderr" 18 | 19 | # generate deployment package 20 | 21 | $packagingSource = "-source:archiveDir='C:\staging\site_content'" 22 | $packagingDest = "-dest:package='C:\staging\source_bundle.zip'" 23 | $declareParam='-declareParamFile:"C:\staging\site_content\parameters.xml"' # do not modify the quotation marks 24 | 25 | [String[]] $msDeployPackagingArgs = @( 26 | $msDeployVerb, 27 | $packagingSource, 28 | $packagingDest, 29 | $declareParam 30 | ) 31 | 32 | $process = Start-Process $msDeployExe -ArgumentList $msDeployPackagingArgs -NoNewWindow -Wait -PassThru -RedirectStandardOutput $msDeployStdOutPackagingTimeLog -RedirectStandardError $msDeployStdErrPackagingTimeLog 33 | if ( 0 -ne $process.ExitCode ) 34 | { 35 | Write-Output "ERROR: msdeploy.exe exits with nonzero exitcode" 36 | exit $process.ExitCode 37 | } 38 | 39 | # deploy to local server 40 | 41 | $msDeploySource = "-source:package='C:\staging\source_bundle.zip'" 42 | $msDeployDest = "-dest:appHostConfig='{REPLACE_WITH_WEBSITE_NAME}'" 43 | $msDeployEnableAppPoolExt = "-enableLink:AppPoolExtension" 44 | $msDeployAppPool = "-setParam:'Application Pool'='.NET v4.5'" 45 | 46 | [String[]] $msDeployDeploymentArgs = @( 47 | $msDeployVerb, 48 | $msDeploySource, 49 | $msDeployDest, 50 | $msDeployEnableAppPoolExt, 51 | $msDeployAppPool 52 | ) 53 | 54 | $process = Start-Process $msDeployExe -ArgumentList $msDeployDeploymentArgs -NoNewWindow -Wait -PassThru -RedirectStandardOutput $msDeployStdOutDeploymentTimeLog -RedirectStandardError $msDeployStdErrDeploymentTimeLog 55 | 56 | exit $process.ExitCode 57 | -------------------------------------------------------------------------------- /utils/templates/site_post_install.ps1: -------------------------------------------------------------------------------- 1 | # This script performs post-installation operations for website {REPLACE_WITH_WEBSITE_NAME} on the local IIS server 2 | 3 | if ($env:PROCESSOR_ARCHITECTURE -eq "x86") { 4 | # EB runs these scripts in 32-bit PowerShell - we need to force them to run in 64-bit 5 | & (Join-Path ($PSHOME -replace "syswow64", "sysnative") powershell.exe) -file ` 6 | (Join-Path $PSScriptRoot $MyInvocation.MyCommand) @args 7 | exit 8 | } 9 | 10 | Import-Module WebAdministration 11 | $siteName = "{REPLACE_WITH_WEBSITE_NAME}" 12 | 13 | # update site directory ACL 14 | $websiteFilePath = Get-WebFilePath "IIS:\Sites\$siteName" 15 | $websiteFilePathString = $websiteFilePath.ToString() 16 | $accessControlList = Get-Acl $websiteFilePathString 17 | $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS_IUSRS", "FullControl", "ContainerInherit, ObjectInherit", "None", "Allow") 18 | $accessControlList.AddAccessRule($accessRule) 19 | Set-Acl $websiteFilePathString $accessControlList 20 | 21 | # update the site bindings to 80 & 443 22 | $siteBindings = Get-WebBinding -Name $siteName 23 | foreach ($binding in $siteBindings) { 24 | $protocol = $binding.protocol 25 | $bindingInfo = $binding.bindingInformation 26 | if ($protocol -eq "http") { 27 | Set-WebBinding -Name $siteName -BindingInformation $bindingInfo -PropertyName Port -Value 80 28 | } 29 | if ($protocol -eq "https") { 30 | # there is a bug on MS side for https binding - need to use a low level command 31 | Set-WebConfigurationProperty "/system.applicationHost/sites/site[@name='$siteName']/bindings/binding[@protocol='https']" -name bindingInformation -value "*:443:" 32 | } 33 | } 34 | 35 | # start website 36 | Stop-Website "Default Web Site" 37 | Start-Website $siteName 38 | -------------------------------------------------------------------------------- /utils/templates/site_restart.ps1: -------------------------------------------------------------------------------- 1 | # This script restarts website {REPLACE_WITH_WEBSITE_NAME} on the local IIS server 2 | 3 | if ($env:PROCESSOR_ARCHITECTURE -eq "x86") { 4 | # EB runs these scripts in 32-bit PowerShell - we need to force them to run in 64-bit 5 | & (Join-Path ($PSHOME -replace "syswow64", "sysnative") powershell.exe) -file ` 6 | (Join-Path $PSScriptRoot $MyInvocation.MyCommand) @args 7 | exit 8 | } 9 | 10 | Import-Module WebAdministration 11 | 12 | Stop-Website "Default Web Site" # just in case it's running 13 | 14 | Stop-Website "{REPLACE_WITH_WEBSITE_NAME}" 15 | 16 | Start-Website "{REPLACE_WITH_WEBSITE_NAME}" 17 | -------------------------------------------------------------------------------- /utils/templates/site_uninstall.ps1: -------------------------------------------------------------------------------- 1 | # This script uninstalls website {REPLACE_WITH_WEBSITE_NAME} on the local IIS server and deletes its files 2 | 3 | if ($env:PROCESSOR_ARCHITECTURE -eq "x86") { 4 | # EB runs these scripts in 32-bit PowerShell - we need to force them to run in 64-bit 5 | & (Join-Path ($PSHOME -replace "syswow64", "sysnative") powershell.exe) -file ` 6 | (Join-Path $PSScriptRoot $MyInvocation.MyCommand) @args 7 | exit 8 | } 9 | 10 | Import-Module WebAdministration 11 | 12 | $websiteFilePath = Get-WebFilePath "IIS:\Sites\{REPLACE_WITH_WEBSITE_NAME}" 13 | 14 | Remove-Website -Name {REPLACE_WITH_WEBSITE_NAME} 15 | 16 | Start-Sleep -s 30 17 | 18 | $websiteFilePathString = $websiteFilePath.ToString() 19 | 20 | Remove-Item $websiteFilePathString -Recurse -Force -ErrorAction Ignore 21 | --------------------------------------------------------------------------------