├── AppRolla.psm1 ├── Tests └── playground.ps1 ├── config.ps1 ├── deploy.ps1 ├── install.ps1 └── readme.md /AppRolla.psm1: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Appveyor Systems Inc. 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 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Version: 1.0 16 | 17 | # config 18 | $config = @{} 19 | $config.TaskExecutionTimeout = 300 # 5 min 20 | 21 | # default remote root for deployed applications; the value is in single quotes to be evaluated remotely 22 | $config.ApplicationsPath = '$($env:SystemDrive)\applications' 23 | 24 | # the maximum number of previous deployment versions to keep on remote hosts 25 | $config.KeepPreviousVersions = 5 26 | 27 | # if $true a connection to remote host will use SSL and port defaults to 5986 28 | # if $false port defaults to 5985 29 | $config.UseSSL = $true 30 | 31 | # suppress certificate Certificate Authority (CA) check when connecting via SSL 32 | $config.SkipCACheck = $true 33 | 34 | # suppress certificate Canonical Name (CN) check when connecting via SSL 35 | $config.SkipCNCheck = $true 36 | 37 | # Azure settings 38 | $config.UpdateAzureDeployment = $true 39 | 40 | # context 41 | $script:context = @{} 42 | $currentContext = $script:context 43 | $currentContext.applications = @{} 44 | $currentContext.environments = @{} 45 | $currentContext.tasks = @{} 46 | $currentContext.remoteSessions = @{} 47 | $currentContext.azureSubscription = $null 48 | 49 | #region Configuration cmdlets 50 | function Set-DeploymentConfiguration 51 | { 52 | [CmdletBinding()] 53 | param 54 | ( 55 | [Parameter(Position=0, Mandatory=$true)] 56 | $Name, 57 | 58 | [Parameter(Position=1, Mandatory=$false)] 59 | $Value = $null 60 | ) 61 | 62 | $config[$Name] = $Value 63 | } 64 | 65 | function Get-DeploymentConfiguration 66 | { 67 | [CmdletBinding()] 68 | param 69 | ( 70 | [Parameter(Position=0, Mandatory=$false)] 71 | $Name 72 | ) 73 | 74 | if($Name) 75 | { 76 | return $config[$Name] 77 | } 78 | else 79 | { 80 | return $config 81 | } 82 | } 83 | #endregion 84 | 85 | #region Application cmdlets 86 | function New-Application 87 | { 88 | [CmdletBinding()] 89 | param 90 | ( 91 | [Parameter(Position=0, Mandatory=$true)] 92 | [string]$Name, 93 | 94 | [Parameter(Mandatory=$false)] 95 | [string]$BasePath, 96 | 97 | [Parameter(Mandatory=$false)] 98 | $Configuration = @{} 99 | ) 100 | 101 | Write-Verbose "New-Application $Name" 102 | 103 | # verify if application already exists 104 | if($currentContext.applications[$Name] -ne $null) 105 | { 106 | throw "Application $Name already exists. Choose a different name." 107 | } 108 | 109 | # add new application 110 | $app = @{ 111 | Name = $Name 112 | BasePath = $BasePath 113 | Configuration = $Configuration 114 | Roles = @{} 115 | } 116 | 117 | $currentContext.applications[$Name] = $app 118 | } 119 | 120 | function Get-Application 121 | { 122 | [CmdletBinding()] 123 | param 124 | ( 125 | [Parameter(Position=0, Mandatory=$false)] 126 | $Name 127 | ) 128 | 129 | if($Name) 130 | { 131 | # return specific application 132 | $app = $currentContext.applications[$Name] 133 | if($app -eq $null) 134 | { 135 | throw "Application $Name not found." 136 | } 137 | return $app 138 | } 139 | else 140 | { 141 | # return all applications 142 | return $currentContext.applications.values 143 | } 144 | } 145 | 146 | function Set-Application 147 | { 148 | [CmdletBinding()] 149 | param 150 | ( 151 | [Parameter(Position=0, Mandatory=$true)] 152 | [string]$Name, 153 | 154 | [Parameter(Mandatory=$false)] 155 | [string]$BasePath, 156 | 157 | [Parameter(Mandatory=$false)] 158 | $Configuration = @{} 159 | ) 160 | 161 | Write-Verbose "Set-Application $Name" 162 | 163 | $app = Get-Application $Name 164 | 165 | # update app details 166 | if($BasePath) { $app.BasePath = $BasePath } 167 | if($Configuration) { $app.Configuration = $Configuration } 168 | } 169 | 170 | 171 | function Add-WebSiteRole 172 | { 173 | [CmdletBinding()] 174 | param 175 | ( 176 | [Parameter(Position=0, Mandatory=$true)] 177 | $ApplicationName, 178 | 179 | [Parameter(Position=1, Mandatory=$true)] 180 | [string]$Name, 181 | 182 | [Parameter(Mandatory=$false)] 183 | [string]$DeploymentGroup, 184 | 185 | [Parameter(Mandatory=$true)] 186 | [string]$PackageUrl, 187 | 188 | [Parameter(Mandatory=$false)] 189 | [string]$BasePath = $null, 190 | 191 | [Parameter(Mandatory=$false)] 192 | [string]$WebsiteName = $null, 193 | 194 | [Parameter(Mandatory=$false)] 195 | [string]$WebsiteProtocol = $null, 196 | 197 | [Parameter(Mandatory=$false)] 198 | [string]$WebsiteIP = $null, 199 | 200 | [Parameter(Mandatory=$false)] 201 | [int]$WebsitePort = $null, 202 | 203 | [Parameter(Mandatory=$false)] 204 | [string]$WebsiteHost = $null, 205 | 206 | [Parameter(Mandatory=$false)] 207 | $Configuration = @{} 208 | ) 209 | 210 | Write-Verbose "Add-WebsiteRole" 211 | 212 | # get application 213 | $application = Get-Application $ApplicationName 214 | 215 | # verify if the role with such name exists 216 | $role = $Application.Roles[$Name] 217 | if($role -ne $null) 218 | { 219 | throw "Application $($Application.Name) has already $Name role configured. Choose a different role name." 220 | } 221 | 222 | # add role info to the application config 223 | $role = @{ 224 | Type = "website" 225 | Name = $Name 226 | DeploymentGroup = ValueOrDefault $DeploymentGroup "web" 227 | PackageUrl = $PackageUrl 228 | BasePath = $BasePath 229 | WebsiteName = ValueOrDefault $WebsiteName "Default Web Site" 230 | WebsiteProtocol = ValueOrDefault $WebsiteProtocol "http" 231 | WebsiteIP = ValueOrDefault $WebsiteIP "*" 232 | WebsitePort = ValueOrDefault $WebsitePort 80 233 | WebsiteHost = $WebsiteHost 234 | Configuration = $Configuration 235 | } 236 | $Application.Roles[$Name] = $role 237 | } 238 | 239 | function Set-WebSiteRole 240 | { 241 | [CmdletBinding()] 242 | param 243 | ( 244 | [Parameter(Position=0, Mandatory=$true)] 245 | $ApplicationName, 246 | 247 | [Parameter(Position=1, Mandatory=$true)] 248 | [string]$Name, 249 | 250 | [Parameter(Mandatory=$false)] 251 | [string]$DeploymentGroup, 252 | 253 | [Parameter(Mandatory=$false)] 254 | [string]$PackageUrl, 255 | 256 | [Parameter(Mandatory=$false)] 257 | [string]$BasePath, 258 | 259 | [Parameter(Mandatory=$false)] 260 | [string]$WebsiteName, 261 | 262 | [Parameter(Mandatory=$false)] 263 | [string]$WebsiteProtocol = $null, 264 | 265 | [Parameter(Mandatory=$false)] 266 | [string]$WebsiteIP, 267 | 268 | [Parameter(Mandatory=$false)] 269 | [int]$WebsitePort, 270 | 271 | [Parameter(Mandatory=$false)] 272 | [string]$WebsiteHost, 273 | 274 | [Parameter(Mandatory=$false)] 275 | $Configuration 276 | ) 277 | 278 | Write-Verbose "Set-WebsiteRole" 279 | 280 | # get role 281 | $role = Get-ApplicationRole $ApplicationName $Name 282 | 283 | # update role details 284 | if($DeploymentGroup) { $role.DeploymentGroup = $DeploymentGroup } 285 | if($PackageUrl) { $role.PackageUrl = $PackageUrl } 286 | if($BasePath) { $role.BasePath = $BasePath } 287 | if($WebsiteName) { $role.WebsiteName = $WebsiteName } 288 | if($WebsiteProtocol) { $role.WebsiteProtocol = $WebsiteProtocol } 289 | if($WebsiteIP) { $role.WebsiteIP = $WebsiteIP } 290 | if($WebsitePort) { $role.WebsitePort = $WebsitePort } 291 | if($WebsiteHost) { $role.WebsiteHost = $WebsiteHost } 292 | if($Configuration) { $role.Configuration = $Configuration } 293 | } 294 | 295 | function Add-ServiceRole 296 | { 297 | [CmdletBinding()] 298 | param 299 | ( 300 | [Parameter(Position=0, Mandatory=$true)] 301 | $ApplicationName, 302 | 303 | [Parameter(Position=1, Mandatory=$true)] 304 | [string]$Name, 305 | 306 | [Parameter(Mandatory=$false)] 307 | [string]$DeploymentGroup, 308 | 309 | [Parameter(Mandatory=$true)] 310 | [string]$PackageUrl, 311 | 312 | [Parameter(Mandatory=$false)] 313 | [string]$BasePath, 314 | 315 | [Parameter(Mandatory=$false)] 316 | [string]$ServiceExecutable = $null, 317 | 318 | [Parameter(Mandatory=$false)] 319 | [string]$ServiceName = $null, 320 | 321 | [Parameter(Mandatory=$false)] 322 | [string]$ServiceDisplayName = $null, 323 | 324 | [Parameter(Mandatory=$false)] 325 | [string]$ServiceDescription = $null, 326 | 327 | [Parameter(Mandatory=$false)] 328 | $Configuration = @{} 329 | ) 330 | 331 | Write-Verbose "Add-ServiceRole" 332 | 333 | # get application 334 | $application = Get-Application $ApplicationName 335 | 336 | # verify if the role with such name exists 337 | $role = $Application.Roles[$Name] 338 | if($role -ne $null) 339 | { 340 | throw "Application $($Application.Name) has already $Name role configured. Choose a different role name." 341 | } 342 | 343 | # add role info to the application config 344 | $role = @{ 345 | Type = "service" 346 | Name = $Name 347 | DeploymentGroup = ValueOrDefault $DeploymentGroup "app" 348 | PackageUrl = $PackageUrl 349 | BasePath = $BasePath 350 | Configuration = $Configuration 351 | } 352 | 353 | $role.ServiceExecutable = $ServiceExecutable 354 | $role.ServiceName = ValueOrDefault $ServiceName $Name 355 | $role.ServiceDisplayName = ValueOrDefault $ServiceDisplayName $role.ServiceName 356 | $role.ServiceDescription = ValueOrDefault $ServiceDescription "Deployed by AppRoller" 357 | 358 | $Application.Roles[$Name] = $role 359 | } 360 | 361 | function Set-ServiceRole 362 | { 363 | [CmdletBinding()] 364 | param 365 | ( 366 | [Parameter(Position=0, Mandatory=$true)] 367 | $ApplicationName, 368 | 369 | [Parameter(Position=1, Mandatory=$true)] 370 | [string]$Name, 371 | 372 | [Parameter(Mandatory=$false)] 373 | [string]$DeploymentGroup, 374 | 375 | [Parameter(Mandatory=$false)] 376 | [string]$PackageUrl, 377 | 378 | [Parameter(Mandatory=$false)] 379 | [string]$BasePath, 380 | 381 | [Parameter(Mandatory=$false)] 382 | [string]$ServiceExecutable, 383 | 384 | [Parameter(Mandatory=$false)] 385 | [string]$ServiceName, 386 | 387 | [Parameter(Mandatory=$false)] 388 | [string]$ServiceDisplayName, 389 | 390 | [Parameter(Mandatory=$false)] 391 | [string]$ServiceDescription, 392 | 393 | [Parameter(Mandatory=$false)] 394 | $Configuration 395 | ) 396 | 397 | Write-Verbose "Set-ServiceRole" 398 | 399 | # get role 400 | $role = Get-ApplicationRole $ApplicationName $Name 401 | 402 | # update role details 403 | if($DeploymentGroup) { $role.DeploymentGroup = $DeploymentGroup } 404 | if($PackageUrl) { $role.PackageUrl = $PackageUrl } 405 | if($BasePath) { $role.BasePath = $BasePath } 406 | if($ServiceExecutable) { $role.ServiceExecutable = $ServiceExecutable } 407 | if($ServiceName) { $role.ServiceName = $ServiceName } 408 | if($ServiceDisplayName) { $role.ServiceDisplayName = $ServiceDisplayName } 409 | if($ServiceDescription) { $role.ServiceDescription = $ServiceDescription } 410 | if($Configuration) { $role.Configuration = $Configuration } 411 | } 412 | 413 | function Get-ApplicationRole 414 | { 415 | [CmdletBinding()] 416 | param 417 | ( 418 | [Parameter(Position=0, Mandatory=$true)] 419 | $ApplicationName, 420 | 421 | [Parameter(Position=1, Mandatory=$true)] 422 | $RoleName 423 | ) 424 | 425 | # get application 426 | $application = Get-Application $ApplicationName 427 | 428 | # verify if the role with such name exists 429 | $role = $Application.Roles[$RoleName] 430 | if($role -eq $null) 431 | { 432 | throw "Application role $RoleName does not exist." 433 | } 434 | 435 | return $role 436 | } 437 | #endregion 438 | 439 | #region Azure Application cmdlets 440 | function New-AzureApplication 441 | { 442 | [CmdletBinding()] 443 | param 444 | ( 445 | [Parameter(Position=0, Mandatory=$true)] 446 | [string]$Name, 447 | 448 | [Parameter(Mandatory=$true)] 449 | [string]$PackageUrl, 450 | 451 | [Parameter(Mandatory=$true)] 452 | [string]$ConfigUrl, 453 | 454 | [Parameter(Mandatory=$false)] 455 | $Configuration = @{} 456 | ) 457 | 458 | Write-Verbose "New-AzureApplication $Name" 459 | 460 | # add standard application 461 | New-Application $Name -Configuration $Configuration 462 | 463 | # get application 464 | $application = Get-Application $Name 465 | 466 | # add Azure role 467 | $role = @{ 468 | Type = "azure" 469 | Name = "Azure" 470 | PackageUrl = $PackageUrl 471 | ConfigUrl = $ConfigUrl 472 | } 473 | 474 | $application.Roles[$role.Name] = $role 475 | } 476 | 477 | function Set-AzureApplication 478 | { 479 | [CmdletBinding()] 480 | param 481 | ( 482 | [Parameter(Position=0, Mandatory=$true)] 483 | [string]$Name, 484 | 485 | [Parameter(Mandatory=$false)] 486 | [string]$PackageUrl, 487 | 488 | [Parameter(Mandatory=$false)] 489 | [string]$ConfigUrl, 490 | 491 | [Parameter(Mandatory=$false)] 492 | $Configuration = @{} 493 | ) 494 | 495 | Write-Verbose "Set-AzureApplication $Name" 496 | 497 | # get application details 498 | $app = Get-Application $Name 499 | 500 | # update details 501 | if($Configuration) { $app.Configuration = $Configuration } 502 | 503 | # update azure role 504 | $role = $app.Roles["Azure"] 505 | 506 | if($PackageUrl) { $role.PackageUrl = $PackageUrl } 507 | if($ConfigUrl) { $role.ConfigUrl = $ConfigUrl } 508 | } 509 | #endregion 510 | 511 | #region Environment cmdlets 512 | function New-Environment 513 | { 514 | [CmdletBinding()] 515 | param 516 | ( 517 | [Parameter(Position=0, Mandatory=$true)] 518 | $Name, 519 | 520 | [Parameter(Mandatory=$false)] 521 | [PSCredential]$Credential, 522 | 523 | [Parameter(Mandatory=$false)] 524 | $Configuration = @{} 525 | ) 526 | 527 | Write-Verbose "New-Environment $Name" 528 | 529 | # create new environment 530 | $environment = @{ 531 | Name = $Name 532 | Credential = $Credential 533 | Servers = @{} 534 | Configuration = $Configuration 535 | } 536 | 537 | # verify if environment already exists 538 | if($currentContext.environments[$environment.Name] -eq $null) 539 | { 540 | $currentContext.environments[$environment.Name] = $environment 541 | } 542 | else 543 | { 544 | throw "Environment $Name already exists. Choose a different name." 545 | } 546 | } 547 | 548 | function Get-Environment 549 | { 550 | [CmdletBinding()] 551 | param 552 | ( 553 | [Parameter(Position=0, Mandatory=$false)] 554 | $Name 555 | ) 556 | 557 | if($Name) 558 | { 559 | # get specific environment 560 | $environment = $currentContext.environments[$Name] 561 | if($environment -eq $null) 562 | { 563 | throw "Environment $Name not found." 564 | } 565 | return $environment 566 | } 567 | else 568 | { 569 | # return all environments 570 | return $currentContext.environments.values 571 | } 572 | } 573 | 574 | function Set-Environment 575 | { 576 | [CmdletBinding()] 577 | param 578 | ( 579 | [Parameter(Position=0, Mandatory=$true)] 580 | $Name, 581 | 582 | [Parameter(Mandatory=$false)] 583 | [PSCredential]$Credential, 584 | 585 | [Parameter(Mandatory=$false)] 586 | $Configuration 587 | ) 588 | 589 | Write-Verbose "Set-Environment $Name" 590 | 591 | # find environment 592 | $environment = Get-Environment $Name 593 | 594 | # update details 595 | if($Credential) { $environment.Credential = $Credential } 596 | if($Configuration) { $environment.Configuration = $Configuration } 597 | } 598 | 599 | function Add-EnvironmentServer 600 | { 601 | [CmdletBinding()] 602 | param 603 | ( 604 | [Parameter(Position=0, Mandatory=$true)] 605 | $EnvironmentName, 606 | 607 | [Parameter(Position=1, Mandatory=$true)] 608 | [string]$ServerAddress, 609 | 610 | [Parameter(Mandatory=$false)] 611 | [int]$Port = 0, 612 | 613 | [Parameter(Mandatory=$false)] 614 | [string[]]$DeploymentGroup = $null, 615 | 616 | [Parameter(Mandatory=$false)] 617 | [PSCredential]$Credential = $null 618 | ) 619 | 620 | Write-Verbose "Add-EnvironmentServer $ServerAddress" 621 | 622 | # find environment 623 | $environment = Get-Environment $EnvironmentName 624 | 625 | # verify if the server with specified address exists 626 | $server = $Environment.Servers[$ServerAddress] 627 | if($server -ne $null) 628 | { 629 | throw "Environment $($Environment.Name) has already $ServerAddress server added." 630 | } 631 | 632 | # add role info to the application config 633 | $server = @{ 634 | ServerAddress = $ServerAddress 635 | Port = $Port 636 | DeploymentGroup = $DeploymentGroup 637 | Credential = ValueOrDefault $Credential $Environment.Credential 638 | } 639 | $Environment.Servers[$ServerAddress] = $server 640 | } 641 | #endregion 642 | 643 | #region Azure Environment cmdlets 644 | function New-AzureEnvironment 645 | { 646 | [CmdletBinding()] 647 | param 648 | ( 649 | [Parameter(Position=0, Mandatory=$true)] 650 | $Name, 651 | 652 | [Parameter(Mandatory=$true)] 653 | [string]$CloudService, 654 | 655 | [Parameter(Mandatory=$true)] 656 | [string]$Slot 657 | ) 658 | 659 | Write-Verbose "New-AzureEnvironment $Name" 660 | 661 | # create new Azure environment 662 | New-Environment $Name -Configuration @{ 663 | CloudService = $CloudService 664 | Slot = $Slot 665 | } 666 | 667 | # add single localhost server 668 | Add-EnvironmentServer $Name localhost 669 | } 670 | 671 | function Set-AzureEnvironment 672 | { 673 | [CmdletBinding()] 674 | param 675 | ( 676 | [Parameter(Position=0, Mandatory=$true)] 677 | $Name, 678 | 679 | [Parameter(Mandatory=$false)] 680 | $Configuration 681 | ) 682 | 683 | Write-Verbose "Set-AzureEnvironment $Name" 684 | 685 | # find environment 686 | $environment = Get-Environment $Name 687 | 688 | # update details 689 | if($Configuration) 690 | { 691 | foreach($key in $Configuration.keys) 692 | { 693 | $environment.Configuration[$key] = $Configuration[$key] 694 | } 695 | } 696 | } 697 | #endregion 698 | 699 | #region Task cmdlets 700 | function Set-DeploymentTask 701 | { 702 | [CmdletBinding()] 703 | param 704 | ( 705 | [Parameter(Position=0, Mandatory=$true)] 706 | $Name, 707 | 708 | [Parameter(Position=1, Mandatory=$true)] 709 | [scriptblock]$Script, 710 | 711 | [Parameter(Mandatory=$false)] 712 | [string[]]$Before = @(), 713 | 714 | [Parameter(Mandatory=$false)][Alias("On")] 715 | [string[]]$After = @(), 716 | 717 | [Parameter(Mandatory=$false)] 718 | $Requires = @(), 719 | 720 | [Parameter(Mandatory=$false)] 721 | $Application = $null, 722 | 723 | [Parameter(Mandatory=$false)] 724 | $Version = $null, 725 | 726 | [Parameter(Mandatory=$false)] 727 | [string[]]$DeploymentGroup = $null, 728 | 729 | [Parameter(Mandatory=$false)] 730 | [switch]$PerGroup = $false 731 | ) 732 | 733 | Write-Verbose "New-DeploymentTask" 734 | 735 | # verify if task already exists 736 | if($currentContext.tasks[$Name] -ne $null) 737 | { 738 | throw "Deployment task $Name already exists. Choose a different name." 739 | } 740 | 741 | # create new deployment task object 742 | $task = @{ 743 | Name = $Name 744 | Script = $Script 745 | BeforeTasks = New-Object System.Collections.Generic.List[string] 746 | AfterTasks = New-Object System.Collections.Generic.List[string] 747 | RequiredTasks = $Requires 748 | Application = $Application 749 | Version = $Version 750 | DeploymentGroup = $DeploymentGroup 751 | PerGroup = $PerGroup 752 | } 753 | 754 | # add task 755 | $currentContext.tasks[$Name] = $task 756 | 757 | # bind task to others 758 | if($Before) 759 | { 760 | foreach($beforeTaskName in $Before) 761 | { 762 | $beforeTask = $currentContext.tasks[$beforeTaskName] 763 | if($beforeTask -ne $null) 764 | { 765 | $beforeTask.BeforeTasks.Add($task.Name) > $null 766 | } 767 | else 768 | { 769 | throw "Wrong before task: $Before" 770 | } 771 | } 772 | } 773 | 774 | if($After) 775 | { 776 | foreach($afterTaskName in $After) 777 | { 778 | $afterTask = $currentContext.tasks[$afterTaskName] 779 | if($afterTask -ne $null) 780 | { 781 | $afterTask.AfterTasks.Add($task.Name) > $null 782 | } 783 | else 784 | { 785 | throw "Wrong after task: $After" 786 | } 787 | } 788 | } 789 | } 790 | 791 | function Invoke-DeploymentTask 792 | { 793 | param 794 | ( 795 | [Parameter(Position=0, Mandatory=$true)] 796 | [string]$taskName, 797 | 798 | [Parameter(Position=1, Mandatory=$true)][alias("On")] 799 | $environment, 800 | 801 | [Parameter(Position=2, Mandatory=$false)] 802 | $application, 803 | 804 | [Parameter(Position=3, Mandatory=$false)] 805 | [string]$version, 806 | 807 | [Parameter(Mandatory=$false)] 808 | [switch]$Serial = $false 809 | ) 810 | 811 | if($application -ne $null -and $application -is [string]) 812 | { 813 | $application = Get-Application $Application 814 | } 815 | 816 | if($environment -is [string]) 817 | { 818 | $environment = Get-Environment $environment 819 | } 820 | 821 | if($application) 822 | { 823 | Write-Verbose "Running `"$taskName`" task on `"$($environment.Name)` environment for application $($application.Name) version $version" 824 | } 825 | else 826 | { 827 | Write-Verbose "Running `"$taskName`" task on `"$($environment.Name)` environment" 828 | } 829 | 830 | # build remote task context 831 | $taskContext = @{ 832 | TaskName = $taskName 833 | Application = $Application 834 | Version = $Version 835 | Environment = $Environment 836 | ServerTasks = @{} 837 | Serial = $Serial 838 | } 839 | 840 | # add main task 841 | $task = $currentContext.tasks[$taskName] 842 | if($task -eq $null) 843 | { 844 | throw "Task $taskName not found" 845 | } 846 | 847 | # change task deployment group to application roles union 848 | if($taskContext.Application) 849 | { 850 | $task.DeploymentGroup = @(0) * $taskContext.Application.Roles.count 851 | $i = 0 852 | foreach($role in $taskContext.Application.Roles.values) 853 | { 854 | $task.DeploymentGroup[$i++] = $role.DeploymentGroup 855 | } 856 | } 857 | 858 | # setup tasks for each server 859 | $perGroupTasks = @{} 860 | foreach($server in $taskContext.Environment.Servers.values) 861 | { 862 | $serverTasks = @{} 863 | 864 | $filter = @{ 865 | Application = $Application 866 | Version = $Version 867 | Server = $server 868 | PerGroupTasks = $perGroupTasks 869 | } 870 | 871 | # process main task 872 | Add-ApplicableTasks $serverTasks $task $filter 873 | 874 | # filter per-group tasks 875 | $keys = $serverTasks.keys | ToArray 876 | foreach($name in $keys) 877 | { 878 | $t = $serverTasks[$name] 879 | if($t.PerGroup -and $perGroupTasks[$name] -eq $null) 880 | { 881 | # add it to the group tasks 882 | $perGroupTasks[$name] = $true 883 | } 884 | elseif($t.PerGroup -and $perGroupTasks[$name] -ne $null) 885 | { 886 | # remove it from server tasks 887 | $serverTasks.Remove($name) 888 | } 889 | } 890 | 891 | if($serverTasks.Count -gt 0) 892 | { 893 | # insert "init" tasks 894 | $initTask = $currentContext.tasks["init"] 895 | Add-ApplicableTasks $serverTasks $initTask $filter 896 | 897 | # add required tasks 898 | Add-RequiredTasks $serverTasks $task.RequiredTasks $filter 899 | 900 | # add to context 901 | $taskContext.ServerTasks[$server.ServerAddress] = @{ 902 | Tasks = $serverTasks 903 | Script = @("init", $taskName) 904 | } 905 | } 906 | } 907 | 908 | # run scripts on each server 909 | $jobs = @(0) * $taskContext.ServerTasks.count 910 | $jobCount = 0 911 | foreach($serverAddress in $taskContext.ServerTasks.keys) 912 | { 913 | # server sequence to run 914 | $serverTasks = $taskContext.ServerTasks[$serverAddress] 915 | 916 | $server = $environment.Servers[$serverAddress] 917 | 918 | $scriptContext = @{ 919 | Configuration = $config 920 | TaskName = $taskContext.TaskName 921 | Application = $taskContext.Application 922 | Version = $taskContext.Version 923 | Server = @{ 924 | ServerAddress = $server.ServerAddress 925 | DeploymentGroup = $server.DeploymentGroup 926 | } 927 | Environment = @{ 928 | Name = $environment.Name 929 | Configuration = $environment.Configuration 930 | } 931 | Tasks = $serverTasks.Tasks 932 | } 933 | 934 | $script = $serverTasks.Script 935 | 936 | $deployScript = { 937 | param ( 938 | $context, 939 | $script 940 | ) 941 | 942 | $callStack = New-Object System.Collections.Generic.Stack[string] 943 | 944 | function Write-Log($message) 945 | { 946 | $stack = $callStack.ToArray() 947 | [array]::Reverse($stack) 948 | $taskName = $stack -join ":" 949 | Write-Host "[$($context.Server.ServerAddress)][$taskName] $(Get-Date -f g) - $message" 950 | } 951 | 952 | function Invoke-DeploymentTask($taskName) 953 | { 954 | Write-Verbose "Invoke task $taskName" 955 | $task = $context.Tasks[$taskName] 956 | if($task -ne $null) 957 | { 958 | # push task name to call stack 959 | $callStack.Push($taskName) > $null 960 | 961 | # run before tasks recursively 962 | foreach($beforeTask in $task.BeforeTasks) 963 | { 964 | Invoke-DeploymentTask $beforeTask 965 | } 966 | 967 | # run task 968 | .([scriptblock]::Create($task.Script)) 969 | 970 | # run after tasks recursively 971 | foreach($afterTask in $task.AfterTasks) 972 | { 973 | Invoke-DeploymentTask $afterTask 974 | } 975 | 976 | # pop 977 | $callStack.Pop() > $null 978 | } 979 | } 980 | 981 | # run script parts one-by-one 982 | foreach($taskName in $script) 983 | { 984 | Invoke-DeploymentTask $taskName 985 | } 986 | } 987 | 988 | if(IsLocalhost $server.ServerAddress) 989 | { 990 | # run script locally 991 | if($taskContext.Serial) 992 | { 993 | # run script synchronously 994 | Write-Verbose "Running script synchronously on localhost" 995 | Invoke-Command -ScriptBlock $deployScript -ArgumentList $scriptContext,$script 996 | } 997 | else 998 | { 999 | # run script as a job 1000 | Write-Verbose "Run script in parallel on localhost" 1001 | $jobs[$jobCount++] = Start-Job -ScriptBlock $deployScript -ArgumentList $scriptContext,$script 1002 | } 1003 | } 1004 | else 1005 | { 1006 | # get server remote session 1007 | $server = $taskContext.Environment.Servers[$serverAddress] 1008 | $credential = $server.Credential 1009 | if(-not $credential) 1010 | { 1011 | $credential = $taskContext.Environment.Credential 1012 | } 1013 | 1014 | $session = Get-RemoteSession -serverAddress $server.ServerAddress -port $server.Port -credential $credential 1015 | 1016 | # run script on remote machine 1017 | if($taskContext.Serial) 1018 | { 1019 | # run script on remote server synchronously 1020 | Write-Verbose "Running script synchronously on $($server.ServerAddress)" 1021 | Invoke-Command -Session $session -ScriptBlock $deployScript -ArgumentList $scriptContext,$script 1022 | } 1023 | else 1024 | { 1025 | # run script on remote server as a job 1026 | Write-Verbose "Run script in parallel on $($server.ServerAddress)" 1027 | $jobs[$jobCount++] = Invoke-Command -Session $session -ScriptBlock $deployScript -AsJob -ArgumentList $scriptContext,$script 1028 | } 1029 | } 1030 | } 1031 | 1032 | # wait jobs 1033 | if(-not $taskContext.Serial) 1034 | { 1035 | Wait-Job -Job $jobs -Timeout $config.taskExecutionTimeout > $null 1036 | 1037 | # get results 1038 | for($i = 0; $i -lt $jobCount; $i++) 1039 | { 1040 | Receive-Job -Job $jobs[$i] 1041 | } 1042 | } 1043 | 1044 | # close remote sessions 1045 | Remove-RemoteSessions 1046 | } 1047 | 1048 | function Get-RemoteSession 1049 | { 1050 | [CmdletBinding()] 1051 | param 1052 | ( 1053 | [string]$serverAddress, 1054 | [int]$port, 1055 | [PSCredential]$credential 1056 | ) 1057 | 1058 | $session = $currentContext.remoteSessions[$serverAddress] 1059 | if($session -eq $null) 1060 | { 1061 | $useSSL = $config.UseSSL 1062 | $skipCACheck = $config.SkipCACheck 1063 | $skipCNCheck = $config.SkipCNCheck 1064 | 1065 | if($port -eq 0 -and $useSSL) 1066 | { 1067 | $port = 5986 1068 | } 1069 | elseif($port -eq 0) 1070 | { 1071 | $port = 5985 1072 | } 1073 | 1074 | Write-Verbose "Connecting to $($serverAddress) port $port" 1075 | 1076 | $options = New-PSSessionOption -SkipCACheck:$skipCACheck -SkipCNCheck:$skipCNCheck 1077 | 1078 | # start new session 1079 | if($credential) 1080 | { 1081 | # connect with credentials 1082 | $session = New-PSSession -ComputerName $serverAddress -Port $port -Credential $credential ` 1083 | -UseSSL:$useSSL -SessionOption $options 1084 | } 1085 | else 1086 | { 1087 | # connect without credentials 1088 | $session = New-PSSession -ComputerName $serverAddress -Port $port ` 1089 | -UseSSL:$useSSL -SessionOption $options 1090 | } 1091 | 1092 | # store it in a cache 1093 | $currentContext.remoteSessions[$serverAddress] = $session 1094 | } 1095 | 1096 | return $session 1097 | } 1098 | 1099 | function Remove-RemoteSessions 1100 | { 1101 | foreach($session in $currentContext.remoteSessions.values) 1102 | { 1103 | Write-Verbose "Closing remote session to $($session.ComputerName)" 1104 | 1105 | if($session) 1106 | { 1107 | Remove-PSSession -Session $session 1108 | } 1109 | } 1110 | 1111 | $currentContext.remoteSessions.Clear() 1112 | } 1113 | 1114 | function Add-ApplicableTasks($tasks, $task, $filter) 1115 | { 1116 | if((IsTaskAppicable $task $filter)) 1117 | { 1118 | # before tasks recursively 1119 | Add-BeforeTasks $tasks $task.BeforeTasks $filter 1120 | 1121 | $tasks[$task.Name] = $task 1122 | 1123 | # after tasks recursively 1124 | Add-AfterTasks $tasks $task.AfterTasks $filter 1125 | } 1126 | } 1127 | 1128 | function Add-BeforeTasks($tasks, $beforeTasks, $filter) 1129 | { 1130 | foreach($taskName in $beforeTasks) 1131 | { 1132 | $task = $currentContext.tasks[$taskName] 1133 | if($task -ne $null -and (IsTaskAppicable $task $filter)) 1134 | { 1135 | # add task before tasks 1136 | Add-BeforeTasks $tasks $task.BeforeTasks $filter 1137 | 1138 | # add task itself 1139 | $tasks[$taskName] = $task 1140 | } 1141 | } 1142 | } 1143 | 1144 | function Add-AfterTasks($tasks, $afterTasks, $filter) 1145 | { 1146 | foreach($taskName in $afterTasks) 1147 | { 1148 | $task = $currentContext.tasks[$taskName] 1149 | if($task -ne $null -and (IsTaskAppicable $task $filter)) 1150 | { 1151 | # add task itself 1152 | $tasks[$taskName] = $task 1153 | 1154 | # add tasks after 1155 | Add-AfterTasks $tasks $task.AfterTasks $filter 1156 | } 1157 | } 1158 | } 1159 | 1160 | function Add-RequiredTasks($tasks, $requiredTasks, $filter) 1161 | { 1162 | foreach($taskName in $requiredTasks) 1163 | { 1164 | $task = $currentContext.tasks[$taskName] 1165 | 1166 | if($task -ne $null -and (IsTaskAppicable $task $filter)) 1167 | { 1168 | # add required tasks recursively 1169 | Add-RequiredTasks $tasks $task.RequiredTasks $filter 1170 | 1171 | Add-ApplicableTasks $tasks $task $filter 1172 | 1173 | # add task itself 1174 | $tasks[$taskName] = $task 1175 | } 1176 | } 1177 | } 1178 | 1179 | function IsTaskAppicable($task, $filter) 1180 | { 1181 | # filter by application name and version 1182 | if($task.Application -ne $null -and $task.Application -ne $filter.Application.Name) 1183 | { 1184 | # task application name is specified but does not match 1185 | return $false 1186 | } 1187 | else 1188 | { 1189 | # OK, application name does match, check version now if specified 1190 | if($task.Version -ne $null -and $task.Version -ne $filter.Version) 1191 | { 1192 | # version is specified but does not match 1193 | return $false 1194 | } 1195 | } 1196 | 1197 | if($task.DeploymentGroup -eq $null -or $filter.Server.DeploymentGroup -eq $null) 1198 | { 1199 | # task is applicable to all groups 1200 | return $true 1201 | } 1202 | else 1203 | { 1204 | # find intersection of two arrays of DeploymentGroup 1205 | $commonGroups = $task.DeploymentGroup | ?{$filter.Server.DeploymentGroup -contains $_} 1206 | 1207 | if($commonGroups.length -gt 0) 1208 | { 1209 | return $true 1210 | } 1211 | } 1212 | 1213 | return $false 1214 | } 1215 | #endregion 1216 | 1217 | #region Deployment cmdlets 1218 | function New-Deployment 1219 | { 1220 | [CmdletBinding()] 1221 | param 1222 | ( 1223 | [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 1224 | $Application, 1225 | 1226 | [Parameter(Position=1, Mandatory=$true)] 1227 | $Version, 1228 | 1229 | [Parameter(Position=2, Mandatory=$true)][alias("To")] 1230 | $Environment, 1231 | 1232 | [Parameter(Mandatory=$false)] 1233 | [switch]$Serial = $false 1234 | ) 1235 | 1236 | Invoke-DeploymentTask deploy $environment $application $version -Serial:$serial 1237 | } 1238 | 1239 | function Remove-Deployment 1240 | { 1241 | [CmdletBinding()] 1242 | param 1243 | ( 1244 | [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 1245 | $Application, 1246 | 1247 | [Parameter(Position=1, Mandatory=$false)] 1248 | $Version, 1249 | 1250 | [Parameter(Mandatory=$true)][alias("From")] 1251 | $Environment, 1252 | 1253 | [Parameter(Mandatory=$false)] 1254 | [switch]$Serial = $false 1255 | ) 1256 | 1257 | Invoke-DeploymentTask remove $environment $application $version -Serial:$serial 1258 | } 1259 | 1260 | function Restore-Deployment 1261 | { 1262 | [CmdletBinding()] 1263 | param 1264 | ( 1265 | [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 1266 | $Application, 1267 | 1268 | [Parameter(Position=1, Mandatory=$false)] 1269 | $Version, 1270 | 1271 | [Parameter(Mandatory=$true)][alias("On")] 1272 | $Environment, 1273 | 1274 | [Parameter(Mandatory=$false)] 1275 | [switch]$Serial = $false 1276 | ) 1277 | 1278 | Invoke-DeploymentTask rollback $environment $application $version -Serial:$serial 1279 | } 1280 | 1281 | function Restart-Deployment 1282 | { 1283 | [CmdletBinding()] 1284 | param 1285 | ( 1286 | [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 1287 | $Application, 1288 | 1289 | [Parameter(Position=1, Mandatory=$false)][alias("On")] 1290 | $Environment, 1291 | 1292 | [Parameter(Mandatory=$false)] 1293 | [switch]$Serial = $false 1294 | ) 1295 | 1296 | Invoke-DeploymentTask restart $environment $application -Serial:$serial 1297 | } 1298 | 1299 | function Stop-Deployment 1300 | { 1301 | [CmdletBinding()] 1302 | param 1303 | ( 1304 | [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 1305 | $Application, 1306 | 1307 | [Parameter(Position=1, Mandatory=$false)][alias("On")] 1308 | $Environment, 1309 | 1310 | [Parameter(Mandatory=$false)] 1311 | [switch]$Serial = $false 1312 | ) 1313 | 1314 | Invoke-DeploymentTask stop $environment $application -Serial:$serial 1315 | } 1316 | 1317 | function Start-Deployment 1318 | { 1319 | [CmdletBinding()] 1320 | param 1321 | ( 1322 | [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 1323 | $Application, 1324 | 1325 | [Parameter(Position=1, Mandatory=$false)][alias("On")] 1326 | $Environment, 1327 | 1328 | [Parameter(Mandatory=$false)] 1329 | [switch]$Serial = $false 1330 | ) 1331 | 1332 | Invoke-DeploymentTask start $environment $application -Serial:$serial 1333 | } 1334 | #endregion 1335 | 1336 | #region Helper functions 1337 | function IsLocalhost 1338 | { 1339 | [CmdletBinding()] 1340 | param 1341 | ( 1342 | [Parameter(Position=0, Mandatory=$true)] 1343 | [string]$serverAddress 1344 | ) 1345 | 1346 | return ($serverAddress -eq "localhost" -or $serverAddress -eq "127.0.0.1") 1347 | } 1348 | 1349 | function ValueOrDefault($value, $default) 1350 | { 1351 | If($value) 1352 | { 1353 | return $value 1354 | } 1355 | return $default 1356 | } 1357 | 1358 | function ToArray 1359 | { 1360 | begin 1361 | { 1362 | $output = @(); 1363 | } 1364 | process 1365 | { 1366 | $output += $_; 1367 | } 1368 | end 1369 | { 1370 | return ,$output; 1371 | } 1372 | } 1373 | #endregion 1374 | 1375 | #region AppRolla tasks 1376 | Set-DeploymentTask init { 1377 | 1378 | $m = New-Module -Name "CommonFunctions" -ScriptBlock { 1379 | 1380 | function Expand-Zip 1381 | { 1382 | param( 1383 | [Parameter(Position=0,Mandatory=1)]$zipFile, 1384 | [Parameter(Position=1,Mandatory=1)]$destination 1385 | ) 1386 | 1387 | $shellApp = New-Object -com Shell.Application 1388 | $objZip = $shellApp.Namespace($zipFile) 1389 | $objDestination = $shellApp.Namespace($destination) 1390 | $objDestination.CopyHere($objZip.Items(), 16) 1391 | } 1392 | 1393 | function Update-ApplicationConfig 1394 | { 1395 | param ( 1396 | $configPath, 1397 | $variables 1398 | ) 1399 | 1400 | [xml]$xml = New-Object XML 1401 | $xml.Load($configPath) 1402 | 1403 | # appSettings section 1404 | foreach($appSettings in $xml.selectnodes("//*[local-name() = 'appSettings']")) 1405 | { 1406 | foreach($setting in $appSettings.ChildNodes) 1407 | { 1408 | if($setting.key) 1409 | { 1410 | $value = $variables["appSettings.$($setting.key)"] 1411 | if($value -ne $null) 1412 | { 1413 | Write-Log "Updating entry `"$($setting.key)`" to `"$value`"" 1414 | $setting.value = $value 1415 | } 1416 | } 1417 | } 1418 | } 1419 | 1420 | # connectionStrings 1421 | foreach($connectionStrings in $xml.selectnodes("//*[local-name() = 'connectionStrings']")) 1422 | { 1423 | foreach($entry in $connectionStrings.ChildNodes) 1424 | { 1425 | if($entry.name) 1426 | { 1427 | $connectionString = $variables["connectionStrings.$($entry.name)"] 1428 | if($connectionString -ne $null) 1429 | { 1430 | Write-Log "Updating entry `"$($entry.name)`" to `"$connectionString`"" 1431 | $entry.connectionString = $connectionString 1432 | } 1433 | } 1434 | } 1435 | } 1436 | 1437 | $xml.Save($configPath) 1438 | } 1439 | 1440 | function Test-RoleApplicableToServer 1441 | { 1442 | param ( 1443 | $role 1444 | ) 1445 | 1446 | # find intersection of two arrays of DeploymentGroup 1447 | $commonGroups = $role.DeploymentGroup | ?{$context.Server.DeploymentGroup -contains $_} 1448 | return (-not $context.Server.DeploymentGroup -or $commonGroups.length -gt 0) 1449 | } 1450 | 1451 | function Get-TempFileName 1452 | { 1453 | param ( 1454 | [Parameter(Position=0,Mandatory=$false)] 1455 | $extension 1456 | ) 1457 | 1458 | $tempPath = [System.IO.Path]::GetTempPath() 1459 | $fileName = [System.IO.Path]::GetRandomFileName() 1460 | if($extension) 1461 | { 1462 | # change extension 1463 | $fileName = [System.IO.Path]::GetFileNameWithoutExtension($fileName) + $extension 1464 | } 1465 | return [System.IO.Path]::Combine($tempPath, $fileName) 1466 | } 1467 | 1468 | function Get-WindowsService 1469 | { 1470 | param ( 1471 | [Parameter(Position=0,Mandatory=$true)] 1472 | $serviceName 1473 | ) 1474 | 1475 | Get-WmiObject -Class Win32_Service -Filter "Name='$serviceName'" 1476 | } 1477 | 1478 | function Get-VersionFromFileName($fileName) 1479 | { 1480 | return Get-VersionFromDirectory (Split-Path $fileName) 1481 | } 1482 | 1483 | function Get-VersionFromDirectory($directory) 1484 | { 1485 | return $directory.Substring($directory.LastIndexOf("\") + 1) 1486 | } 1487 | 1488 | function ConvertFrom-StringTemplate 1489 | { 1490 | param ( 1491 | [Parameter(Position=0,Mandatory=$true)] 1492 | $str 1493 | ) 1494 | & ([scriptblock]::Create("`"$str`"")) 1495 | } 1496 | 1497 | Export-ModuleMember -Function Push-TaskCallStack, Pop-TaskCallStack, Write-Log, Expand-Zip, Test-RoleApplicableToServer, ` 1498 | Update-ApplicationConfig, Get-TempFileName, Get-WindowsService, Get-VersionFromFileName, Get-VersionFromDirectory, ` 1499 | ConvertFrom-StringTemplate 1500 | } 1501 | } 1502 | 1503 | Set-DeploymentTask download-package -Requires authenticate-download-client { 1504 | Write-Log "Downloading package $($role.PackageUrl)" 1505 | 1506 | # expects parameters: 1507 | # $role - application role 1508 | # $packageFile - local package file 1509 | 1510 | $webClient = New-Object System.Net.WebClient 1511 | 1512 | # call custom method to authenticate web client 1513 | Invoke-DeploymentTask authenticate-download-client 1514 | 1515 | # download 1516 | $webClient.DownloadFile($role.PackageUrl, $packageFile) 1517 | } 1518 | 1519 | Set-DeploymentTask authenticate-download-client { 1520 | # expects parameters: 1521 | # $webClient - WebClient instance to download package 1522 | # $context.Configuration["appveyorApiKey"] - AppVeyor API access key 1523 | # $context.Configuration["appveyorApiSecret"] - AppVeyor API secret key 1524 | # $context.Configuration["accountId"] - AppVeyor API account ID to login 1525 | 1526 | if(-not $role.PackageUrl.StartsWith("https://ci.appveyor.com/api/")) 1527 | { 1528 | return 1529 | } 1530 | 1531 | $apiAccessKey = $context.Configuration["appveyorApiKey"] 1532 | $apiSecretKey = $context.Configuration["appveyorApiSecret"] 1533 | $accountId = $context.Configuration["appveyorAccountId"] 1534 | 1535 | # verify parameters 1536 | if(-not $apiAccessKey -or -not $apiSecretKey) 1537 | { 1538 | $msg = @" 1539 | "Unable to download package from AppVeyor repository. 1540 | To authenticate request set AppVeyor API keys in global deployment configuration: 1541 | - Set-DeploymentConfiguration appveyorApiKey 1542 | - Set-DeploymentConfiguration appveyorApiSecret " 1543 | "@ 1544 | throw $msg 1545 | } 1546 | 1547 | $timestamp = [DateTime]::UtcNow.ToString("r") 1548 | 1549 | # generate signature 1550 | $stringToSign = $timestamp 1551 | $secretKeyBytes = [byte[]]([System.Text.Encoding]::ASCII.GetBytes($apiSecretKey)) 1552 | $stringToSignBytes = [byte[]]([System.Text.Encoding]::ASCII.GetBytes($stringToSign)) 1553 | 1554 | [System.Security.Cryptography.HMACSHA1] $signer = New-Object System.Security.Cryptography.HMACSHA1(,$secretKeyBytes) 1555 | $signatureHash = $signer.ComputeHash($stringToSignBytes) 1556 | $signature = [System.Convert]::ToBase64String($signatureHash) 1557 | 1558 | $headerValue = "HMAC-SHA1 accessKey=`"$apiAccessKey`", timestamp=`"$timestamp`", signature=`"$signature`"" 1559 | if($accountId) 1560 | { 1561 | $headerValue = $headerValue + ", accountId=`"$accountId`"" 1562 | } 1563 | 1564 | # set web client header 1565 | $webClient.Headers.Add("Authorization", $headerValue) 1566 | } 1567 | 1568 | Set-DeploymentTask setup-role-folder { 1569 | 1570 | $rootPath = $null 1571 | $basePath = $null 1572 | 1573 | if($role.BasePath) 1574 | { 1575 | $basePath = ConvertFrom-StringTemplate $role.BasePath 1576 | } 1577 | elseif($context.Application.BasePath) 1578 | { 1579 | $basePath = ConvertFrom-StringTemplate $context.Application.BasePath 1580 | $basePath = Join-Path $basePath $role.Name 1581 | } 1582 | elseif($context.Configuration.applicationsPath) 1583 | { 1584 | $basePath = ConvertFrom-StringTemplate $context.Configuration.applicationsPath 1585 | $basePath = Join-Path $basePath $context.Application.Name 1586 | $basePath = Join-Path $basePath $role.Name 1587 | } 1588 | 1589 | if(-not $basePath) 1590 | { 1591 | throw "Cannot determine role base path" 1592 | } 1593 | else 1594 | { 1595 | $role.BasePath = $basePath 1596 | } 1597 | 1598 | # append version 1599 | if($context.Version) 1600 | { 1601 | $role.RootPath = Join-Path $role.BasePath $context.Version 1602 | } 1603 | 1604 | Write-Log "Role base path: $($role.BasePath)" 1605 | Write-Log "Role deployment path: $($role.RootPath)" 1606 | 1607 | # read all installed role versions 1608 | if(Test-Path $role.BasePath) 1609 | { 1610 | $versionFolders = Get-ChildItem -Path $role.BasePath | Sort-Object -Property CreationTime -Descending 1611 | $role.Versions = @(0) * $versionFolders.Count 1612 | for($i = 0; $i -lt $role.Versions.Length; $i++) 1613 | { 1614 | $role.Versions[$i] = $versionFolders[$i].Name 1615 | } 1616 | 1617 | if($role.Versions.length -gt 0) 1618 | { 1619 | Write-Log "Role installed version: $($role.Versions[0])" 1620 | } 1621 | } 1622 | } 1623 | 1624 | Set-DeploymentTask deploy -Requires setup-role-folder,download-package,deploy-website,deploy-service,deploy-console,deploy-azure { 1625 | Write-Log "Deploying application" 1626 | 1627 | # deploy each role separately 1628 | foreach($role in $context.Application.Roles.values) 1629 | { 1630 | if(Test-RoleApplicableToServer $role) 1631 | { 1632 | # determine the location of application folder 1633 | Invoke-DeploymentTask setup-role-folder 1634 | 1635 | # $role.BasePath - base path for role versions 1636 | # $role.RootPath - role version installation root (application root) 1637 | # $role.Versions - the list of installed versions (latest first) 1638 | 1639 | # invoke role specific deployment code 1640 | Invoke-DeploymentTask "deploy-$($role.Type)" 1641 | 1642 | # delete previous versions 1643 | if($role.Versions.length -gt $context.Configuration.KeepPreviousVersions) 1644 | { 1645 | for($i = $context.Configuration.KeepPreviousVersions; $i -lt $role.Versions.length; $i++) 1646 | { 1647 | $version = $role.Versions[$i] 1648 | Write-Log "Deleting old version $version" 1649 | Remove-Item (Join-Path $role.BasePath $version) -Force -Recurse 1650 | } 1651 | } 1652 | } 1653 | } 1654 | } 1655 | 1656 | Set-DeploymentTask deploy-website { 1657 | Write-Log "Deploying website $($role.Name)" 1658 | 1659 | Import-Module WebAdministration 1660 | 1661 | # ... and make sure the folder does not exists 1662 | if(Test-Path $role.RootPath) 1663 | { 1664 | throw "$($context.Application.Name) $($context.Version) role $($role.Name) deployment already exists." 1665 | } 1666 | 1667 | try 1668 | { 1669 | # create application folder (with version info) 1670 | Write-Log "Create website folder: $($role.RootPath)" 1671 | New-Item -ItemType Directory -Force -Path $role.RootPath > $null 1672 | 1673 | # download service package to temp location 1674 | $packageFile = Get-TempFileName ".zip" 1675 | Invoke-DeploymentTask download-package 1676 | 1677 | # unzip service package to application folder 1678 | Expand-Zip $packageFile $role.RootPath 1679 | 1680 | # update web.config 1681 | $webConfigPath = Join-Path $role.RootPath "web.config" 1682 | if(Test-Path $webConfigPath) 1683 | { 1684 | Write-Log "Updating web.config in $appConfigPath" 1685 | Update-ApplicationConfig -configPath $webConfigPath -variables $role.Configuration 1686 | } 1687 | 1688 | $appPoolName = $role.WebsiteName 1689 | $appPoolIdentityName = "IIS APPPOOL\$appPoolName" 1690 | 1691 | $website = Get-Item "IIS:\Sites\$($role.WebsiteName)" -EA 0 1692 | if ($website -ne $null) 1693 | { 1694 | Write-Log "Website `"$($role.WebsiteName)`" already exists" 1695 | 1696 | $appPoolName = $website.applicationPool 1697 | 1698 | # get app pool details 1699 | $appPool = Get-Item "IIS:\AppPools\$appPoolName" -EA 0 1700 | 1701 | # determine pool identity 1702 | switch($appPool.processModel.identityType) 1703 | { 1704 | ApplicationPoolIdentity { $appPoolIdentityName = "IIS APPPOOL\$appPoolName" } 1705 | NetworkService { $appPoolIdentityName = "NETWORK SERVICE" } 1706 | SpecificUser { $appPoolIdentityName = $appPool.processModel.userName } 1707 | } 1708 | 1709 | # stop application pool 1710 | Write-Log "Stopping website application pool..." 1711 | Stop-WebAppPool $website.applicationPool 1712 | 1713 | # wait 2 sec before continue 1714 | Start-Sleep -s 2 1715 | } 1716 | 1717 | Write-Log "Application pool name: $appPoolName" 1718 | Write-Log "Application pool identity: $appPoolIdentityName" 1719 | 1720 | # create website if required 1721 | if(-not $website) 1722 | { 1723 | # create application pool 1724 | Write-Log "Creating IIS application pool `"$appPoolName`"" 1725 | $webAppPool = New-WebAppPool -Name $appPoolName -Force 1726 | $WebAppPool.processModel.identityType = "ApplicationPoolIdentity" 1727 | $WebAppPool | Set-Item 1728 | 1729 | Write-Log "Granting `"Read`" permissions to application pool identity on application folder" 1730 | icacls $role.RootPath /grant "IIS APPPOOL\$($appPoolName):(OI)(CI)(R)" > $null 1731 | 1732 | # create website 1733 | New-Item "IIS:\Sites\$($role.WebsiteName)" -Bindings @{protocol="$($role.WebsiteProtocol)";bindingInformation="$($role.WebsiteIP):$($role.WebsitePort):$($role.WebsiteHost)"} ` 1734 | -PhysicalPath $role.RootPath -ApplicationPool $appPoolName > $null 1735 | } 1736 | else 1737 | { 1738 | Write-Log "Granting `"Read`" permissions to application pool identity on application folder" 1739 | icacls $role.RootPath /grant "IIS APPPOOL\$($appPoolName):(OI)(CI)(R)" > $null 1740 | 1741 | # update website root folder 1742 | Set-ItemProperty "IIS:\Sites\$($role.WebsiteName)" -Name physicalPath -Value $role.RootPath 1743 | 1744 | # start application pool 1745 | Write-Log "Starting application pool..." 1746 | Start-WebAppPool $appPoolName 1747 | } 1748 | } 1749 | catch 1750 | { 1751 | # delete new application folder 1752 | if(Test-Path $role.RootPath) 1753 | { 1754 | Remove-Item $role.RootPath -Force -Recurse 1755 | } 1756 | throw 1757 | } 1758 | finally 1759 | { 1760 | # cleanup 1761 | Write-Log "Cleanup..." 1762 | Remove-Item $packageFile -Force 1763 | } 1764 | } 1765 | 1766 | Set-DeploymentTask deploy-service { 1767 | Write-Log "Deploying Windows service $($role.Name)" 1768 | 1769 | # ... and make sure the folder does not exists 1770 | if(Test-Path $role.RootPath) 1771 | { 1772 | throw "$($context.Application.Name) $($context.Version) role $($role.Name) deployment already exists." 1773 | } 1774 | 1775 | $currentServiceExecutablePath = $null 1776 | try 1777 | { 1778 | # create application folder (with version info) 1779 | Write-Log "Create application folder: $($role.RootPath)" 1780 | New-Item -ItemType Directory -Force -Path $role.RootPath > $null 1781 | 1782 | # download service package to temp location 1783 | $packageFile = Get-TempFileName ".zip" 1784 | Invoke-DeploymentTask download-package 1785 | 1786 | # unzip service package to application folder 1787 | Expand-Zip $packageFile $role.RootPath 1788 | 1789 | # find windows service executable 1790 | $serviceExecutablePath = $null 1791 | if($role.ServiceExecutable) 1792 | { 1793 | $serviceExecutablePath = Join-Path $role.RootPath $role.ServiceExecutable 1794 | } 1795 | else 1796 | { 1797 | $serviceExecutablePath = Get-ChildItem "$($role.RootPath)\*.exe" | Select-Object -First 1 1798 | } 1799 | Write-Log "Service executable path: $serviceExecutablePath" 1800 | 1801 | # update app.config 1802 | $appConfigPath = "$serviceExecutablePath.config" 1803 | if(Test-Path $appConfigPath) 1804 | { 1805 | Write-Log "Updating service configuration in $appConfigPath" 1806 | Update-ApplicationConfig -configPath $appConfigPath -variables $role.Configuration 1807 | } 1808 | 1809 | # check if the service already exists 1810 | $existingService = Get-WindowsService $role.ServiceName 1811 | if ($existingService -ne $null) 1812 | { 1813 | Write-Log "Service already exists, stopping..." 1814 | 1815 | # remember path to restore in case of disaster 1816 | $currentServiceExecutablePath = $existingService.PathName 1817 | 1818 | # stop the service 1819 | Stop-Service -Name $role.ServiceName -Force 1820 | 1821 | # wait 2 sec before continue 1822 | Start-Sleep -s 2 1823 | 1824 | # uninstall service 1825 | $existingService.Delete() > $null 1826 | } 1827 | else 1828 | { 1829 | Write-Log "Service does not exists." 1830 | } 1831 | 1832 | # install service 1833 | Write-Log "Installing service $($role.ServiceName)" 1834 | New-Service -Name $role.ServiceName -BinaryPathName $serviceExecutablePath ` 1835 | -DisplayName $role.ServiceDisplayName -StartupType Automatic -Description $role.ServiceDescription > $null 1836 | 1837 | # start service 1838 | Write-Log "Starting service..." 1839 | Start-Service -Name $role.ServiceName 1840 | } 1841 | catch 1842 | { 1843 | # delete new application folder 1844 | if(Test-Path $role.RootPath) 1845 | { 1846 | Remove-Item $role.RootPath -Force -Recurse 1847 | } 1848 | throw 1849 | } 1850 | finally 1851 | { 1852 | # cleanup 1853 | Write-Log "Cleanup..." 1854 | Remove-Item $packageFile -Force 1855 | } 1856 | } 1857 | 1858 | Set-DeploymentTask remove -Requires setup-role-folder,remove-website,remove-service,remove-azure { 1859 | Write-Log "Removing deployment" 1860 | 1861 | # remove role-by-role 1862 | foreach($role in $context.Application.Roles.values) 1863 | { 1864 | if(Test-RoleApplicableToServer $role) 1865 | { 1866 | Invoke-DeploymentTask "remove-$($role.Type)" 1867 | } 1868 | } 1869 | } 1870 | 1871 | Set-DeploymentTask remove-website { 1872 | Write-Log "Removing website $($role.Name)" 1873 | 1874 | Import-Module WebAdministration 1875 | 1876 | # determine the location of application folder 1877 | Invoke-DeploymentTask setup-role-folder 1878 | 1879 | # get website details 1880 | $website = Get-Item "IIS:\Sites\$($role.WebsiteName)" -EA 0 1881 | if ($website -ne $null) 1882 | { 1883 | Write-Log "Website `"$($role.WebsiteName)`" found" 1884 | 1885 | $siteRoot = $website.physicalPath 1886 | 1887 | # make sure we are not trying to delete active version 1888 | $currentVersion = Get-VersionFromDirectory $siteRoot 1889 | 1890 | if($currentVersion -eq $context.Version) 1891 | { 1892 | throw "Active version $version cannot be removed. Specify previous version to remove or ommit -Version parameter to completely delete application." 1893 | } 1894 | } 1895 | 1896 | if(-not $context.Version) 1897 | { 1898 | # delete entire deployment 1899 | Write-Log "Deleting all website deployments" 1900 | 1901 | if($website -ne $null) 1902 | { 1903 | $appPoolName = $website.applicationPool 1904 | 1905 | # stop application pool 1906 | Write-Log "Stopping application pool $appPoolName..." 1907 | Stop-WebAppPool $appPoolName 1908 | 1909 | # wait 2 sec before continue 1910 | Start-Sleep -s 2 1911 | 1912 | # delete website 1913 | Write-Log "Deleting website $($role.WebsiteName)" 1914 | Remove-Website $role.WebsiteName 1915 | 1916 | # delete application pool 1917 | Write-Log "Deleting application pool $appPoolName" 1918 | Remove-WebAppPool $appPoolName 1919 | } 1920 | 1921 | # delete role folder recursively 1922 | Write-Log "Deleting application directory $($role.BasePath)" 1923 | if(Test-Path $role.BasePath) 1924 | { 1925 | Remove-Item $role.BasePath -Force -Recurse 1926 | } 1927 | } 1928 | else 1929 | { 1930 | # delete specific version 1931 | if(Test-Path $role.RootPath) 1932 | { 1933 | Write-Log "Deleting deployment directory $($role.RootPath)" 1934 | Remove-Item $role.RootPath -Force -Recurse 1935 | } 1936 | } 1937 | 1938 | } 1939 | 1940 | Set-DeploymentTask remove-service { 1941 | Write-Log "Removing Windows service $($role.Name)" 1942 | 1943 | # determine the location of application folder 1944 | Invoke-DeploymentTask setup-role-folder 1945 | 1946 | # get service details 1947 | $service = Get-WindowsService $role.ServiceName 1948 | if ($service -ne $null) 1949 | { 1950 | $serviceExecutable = $service.PathName 1951 | 1952 | # make sure we are not trying to delete active version 1953 | $currentVersion = Get-VersionFromFileName $serviceExecutable 1954 | 1955 | if($currentVersion -eq $context.Version) 1956 | { 1957 | throw "Active version $version cannot be removed. Specify previous version to remove or ommit -Version parameter to completely delete application." 1958 | } 1959 | } 1960 | 1961 | if(-not $context.Version) 1962 | { 1963 | # delete entire deployment 1964 | Write-Log "Deleting all service deployments" 1965 | 1966 | # delete service 1967 | if ($service -ne $null) 1968 | { 1969 | # stop the service 1970 | Write-Log "Stopping service $($role.ServiceName)..." 1971 | Stop-Service -Name $role.ServiceName -Force 1972 | 1973 | # wait 2 sec before continue 1974 | Start-Sleep -s 2 1975 | 1976 | # uninstall service 1977 | Write-Log "Deleting service $($role.ServiceName)" 1978 | $service.Delete() > $null 1979 | } 1980 | 1981 | # delete role folder recursively 1982 | Write-Log "Deleting application directory $($role.BasePath)" 1983 | if(Test-Path $role.BasePath) 1984 | { 1985 | Remove-Item $role.BasePath -Force -Recurse 1986 | } 1987 | } 1988 | else 1989 | { 1990 | # delete specific version 1991 | if(Test-Path $role.RootPath) 1992 | { 1993 | Write-Log "Deleting deployment directory $($role.RootPath)" 1994 | Remove-Item $role.RootPath -Force -Recurse 1995 | } 1996 | } 1997 | } 1998 | 1999 | Set-DeploymentTask rollback -Requires setup-role-folder,rollback-website,rollback-service { 2000 | Write-Log "Rollback deployment" 2001 | 2002 | # rollback role-by-role 2003 | foreach($role in $context.Application.Roles.values) 2004 | { 2005 | if(Test-RoleApplicableToServer $role) 2006 | { 2007 | Invoke-DeploymentTask "rollback-$($role.Type)" 2008 | } 2009 | } 2010 | } 2011 | 2012 | Set-DeploymentTask rollback-website { 2013 | Import-Module WebAdministration 2014 | 2015 | # determine the location of application folder 2016 | Invoke-DeploymentTask setup-role-folder 2017 | 2018 | # check if rollback is possible 2019 | if($role.Versions.length -lt 2) 2020 | { 2021 | throw "There are no previous versions to rollback to." 2022 | } 2023 | 2024 | # current version 2025 | $currentVersion = $role.Versions[0] 2026 | $currentPath = Join-Path $role.BasePath $currentVersion 2027 | 2028 | # get website details to determine actual current version 2029 | $website = Get-Item "IIS:\Sites\$($role.WebsiteName)" -EA 0 2030 | if ($website -ne $null) 2031 | { 2032 | $currentPath = $website.physicalPath 2033 | $currentVersion = Get-VersionFromDirectory $currentPath 2034 | } 2035 | 2036 | # rollback version 2037 | $rollbackVersion = $null 2038 | $rollbackPath = $null 2039 | 2040 | # is that a specific version we want to rollback to? 2041 | if($context.Version) 2042 | { 2043 | # make sure we don't rollback to active version 2044 | if($context.Version -eq $currentVersion) 2045 | { 2046 | throw "Cannot rollback to the currently deployed version $currentVersion" 2047 | } 2048 | 2049 | if(Test-Path $role.RootPath) 2050 | { 2051 | $rollbackVersion = $context.Version 2052 | $rollbackPath = $role.RootPath 2053 | } 2054 | else 2055 | { 2056 | throw "Version $($context.Version) not found." 2057 | } 2058 | } 2059 | else 2060 | { 2061 | # determine rollback version 2062 | # rollback version must be next after the current one 2063 | for($i = 0; $i -lt $role.Versions.length; $i++) 2064 | { 2065 | # find current version and make sure it's not the last one in the list 2066 | if($role.Versions[$i] -eq $currentVersion -and $i -ne ($role.Versions.length - 1)) 2067 | { 2068 | $rollbackVersion = $role.Versions[$i+1] 2069 | $rollbackPath = Join-Path $role.BasePath $rollbackVersion 2070 | break 2071 | } 2072 | } 2073 | 2074 | if(-not $rollbackVersion) 2075 | { 2076 | throw "Cannot rollback to the previous version because the active $currentVersion is the last one." 2077 | } 2078 | } 2079 | 2080 | # start rollback 2081 | Write-Log "Rollback website $($role.Name) to version $rollbackVersion" 2082 | 2083 | # stop website if it exists 2084 | if ($website -ne $null) 2085 | { 2086 | # stop application pool 2087 | Write-Log "Stopping application pool..." 2088 | Stop-WebAppPool $website.applicationPool 2089 | 2090 | # wait 2 sec before continue 2091 | Start-Sleep -s 2 2092 | 2093 | # change website root folder 2094 | Write-Log "Update website root folder to $rollbackPath" 2095 | Set-ItemProperty "IIS:\Sites\$($role.WebsiteName)" -Name physicalPath -Value $rollbackPath 2096 | 2097 | # start application pool 2098 | Write-Log "Starting application pool..." 2099 | Start-WebAppPool $website.applicationPool 2100 | } 2101 | 2102 | # delete current version 2103 | Write-Log "Deleting current version $currentVersion at $currentPath" 2104 | Remove-Item (Join-Path $role.BasePath $currentVersion) -Force -Recurse 2105 | } 2106 | 2107 | Set-DeploymentTask rollback-service { 2108 | 2109 | # determine the location of application folder 2110 | Invoke-DeploymentTask setup-role-folder 2111 | 2112 | # check if rollback is possible 2113 | if($role.Versions.length -lt 2) 2114 | { 2115 | throw "There are no previous versions to rollback to." 2116 | } 2117 | 2118 | # current version 2119 | $currentVersion = $role.Versions[0] 2120 | $currentPath = Join-Path $role.BasePath $currentVersion 2121 | 2122 | # get service details to determine actual current version 2123 | $service = Get-WindowsService $role.ServiceName 2124 | if ($service -ne $null) 2125 | { 2126 | $currentPath = Split-Path $service.PathName 2127 | $currentVersion = Get-VersionFromDirectory $currentPath 2128 | } 2129 | 2130 | # rollback version 2131 | $rollbackVersion = $null 2132 | $rollbackPath = $null 2133 | 2134 | # is that a specific version we want to rollback to? 2135 | if($context.Version) 2136 | { 2137 | # make sure we don't rollback to active version 2138 | if($context.Version -eq $currentVersion) 2139 | { 2140 | throw "Cannot rollback to the currently deployed version $currentVersion" 2141 | } 2142 | 2143 | if(Test-Path $role.RootPath) 2144 | { 2145 | $rollbackVersion = $context.Version 2146 | $rollbackPath = $role.RootPath 2147 | } 2148 | else 2149 | { 2150 | throw "Version $($context.Version) not found." 2151 | } 2152 | } 2153 | else 2154 | { 2155 | # determine rollback version 2156 | # rollback version must be next after the current one 2157 | for($i = 0; $i -lt $role.Versions.length; $i++) 2158 | { 2159 | # find current version and make sure it's not the last one in the list 2160 | if($role.Versions[$i] -eq $currentVersion -and $i -ne ($role.Versions.length - 1)) 2161 | { 2162 | $rollbackVersion = $role.Versions[$i+1] 2163 | $rollbackPath = Join-Path $role.BasePath $rollbackVersion 2164 | break 2165 | } 2166 | } 2167 | 2168 | if(-not $rollbackVersion) 2169 | { 2170 | throw "Cannot rollback to the previous version because the active $currentVersion is the last one." 2171 | } 2172 | } 2173 | 2174 | # start rollback 2175 | Write-Log "Rollback Windows service $($role.Name) to version $rollbackVersion" 2176 | 2177 | # stop service if exists 2178 | if ($service -ne $null) 2179 | { 2180 | Write-Log "Service already exists, stopping..." 2181 | 2182 | # stop the service 2183 | Stop-Service -Name $role.ServiceName -Force 2184 | 2185 | # wait 2 sec before continue 2186 | Start-Sleep -s 2 2187 | 2188 | # uninstall service 2189 | $service.Delete() > $null 2190 | } 2191 | else 2192 | { 2193 | Write-Log "Service does not exists." 2194 | } 2195 | 2196 | # find windows service executable 2197 | $serviceExecutablePath = $null 2198 | if($role.ServiceExecutable) 2199 | { 2200 | $serviceExecutablePath = Join-Path $rollbackPath $role.ServiceExecutable 2201 | } 2202 | else 2203 | { 2204 | $serviceExecutablePath = Get-ChildItem "$rollbackPath\*.exe" | Select-Object -First 1 2205 | } 2206 | Write-Log "Service executable path: $serviceExecutablePath" 2207 | 2208 | # install service 2209 | Write-Log "Installing service $($role.ServiceName)" 2210 | New-Service -Name $role.ServiceName -BinaryPathName $serviceExecutablePath ` 2211 | -DisplayName $role.ServiceDisplayName -StartupType Automatic -Description $role.ServiceDescription > $null 2212 | 2213 | # start service 2214 | Write-Log "Starting service..." 2215 | Start-Service -Name $role.ServiceName 2216 | 2217 | # delete current version 2218 | Write-Log "Deleting current version $currentVersion at $currentPath" 2219 | Remove-Item (Join-Path $role.BasePath $currentVersion) -Force -Recurse 2220 | } 2221 | 2222 | Set-DeploymentTask start -Requires setup-role-folder,start-website,start-service { 2223 | Write-Log "Start deployment" 2224 | 2225 | # start role-by-role 2226 | foreach($role in $context.Application.Roles.values) 2227 | { 2228 | if(Test-RoleApplicableToServer $role) 2229 | { 2230 | Invoke-DeploymentTask "start-$($role.Type)" 2231 | } 2232 | } 2233 | } 2234 | 2235 | Set-DeploymentTask stop -Requires setup-role-folder,stop-website,stop-service { 2236 | Write-Log "Stop deployment" 2237 | 2238 | # stop role-by-role 2239 | foreach($role in $context.Application.Roles.values) 2240 | { 2241 | if(Test-RoleApplicableToServer $role) 2242 | { 2243 | Invoke-DeploymentTask "stop-$($role.Type)" 2244 | } 2245 | } 2246 | } 2247 | 2248 | Set-DeploymentTask start-website { 2249 | Write-Log "Start website $($role.Name)" 2250 | 2251 | Import-Module WebAdministration 2252 | 2253 | $website = Get-Item "IIS:\Sites\$($role.WebsiteName)" -EA 0 2254 | if ($website -ne $null) 2255 | { 2256 | $appPoolName = $website.applicationPool 2257 | Write-Log "Starting application pool $appPoolName..." 2258 | Start-WebAppPool $appPoolName 2259 | Write-Log "Application pool started" 2260 | } 2261 | } 2262 | 2263 | Set-DeploymentTask stop-website { 2264 | Write-Log "Stop website $($role.Name)" 2265 | 2266 | Import-Module WebAdministration 2267 | 2268 | $website = Get-Item "IIS:\Sites\$($role.WebsiteName)" -EA 0 2269 | if ($website -ne $null) 2270 | { 2271 | $appPoolName = $website.applicationPool 2272 | if((Get-WebAppPoolState $appPoolName).Value -ne "Stopped") 2273 | { 2274 | Write-Log "Stopping application pool $appPoolName..." 2275 | Stop-WebAppPool $appPoolName 2276 | Write-Log "Application pool stopped" 2277 | } 2278 | else 2279 | { 2280 | Write-Log "Application pool `"$appPoolName`" is already stopped" 2281 | } 2282 | } 2283 | } 2284 | 2285 | Set-DeploymentTask start-service { 2286 | # get service details 2287 | $service = Get-WindowsService $role.ServiceName 2288 | if ($service -ne $null) 2289 | { 2290 | Write-Log "Starting Windows service $($role.ServiceName)..." 2291 | Start-Service -Name $role.ServiceName 2292 | Write-Log "Service started" 2293 | } 2294 | } 2295 | 2296 | Set-DeploymentTask stop-service { 2297 | # get service details 2298 | $service = Get-WindowsService $role.ServiceName 2299 | if ($service -ne $null) 2300 | { 2301 | Write-Log "Stopping Windows service $($role.ServiceName)..." 2302 | Stop-Service -Name $role.ServiceName -Force 2303 | Write-Log "Service stopped" 2304 | } 2305 | } 2306 | 2307 | Set-DeploymentTask restart -Requires start,stop { 2308 | Write-Log "Restart deployment" 2309 | Invoke-DeploymentTask stop 2310 | Invoke-DeploymentTask start 2311 | } 2312 | #endregion 2313 | 2314 | #region Azure tasks 2315 | Set-DeploymentTask deploy-azure { 2316 | Write-Log "Deploying Azure Cloud Service $($context.Application.Name)" 2317 | 2318 | function UpdateAzureCloudServiceConfig($configPath, $configuration) 2319 | { 2320 | [xml]$xml = New-Object XML 2321 | $xml.Load($configPath) 2322 | 2323 | # iterate through Roles 2324 | foreach($role in $xml.selectnodes("//*[local-name() = 'Role']")) 2325 | { 2326 | $roleName = $role.Attributes["name"].Value 2327 | 2328 | # check if the number of instances configured 2329 | if($configuration[$roleName] -ne $null) 2330 | { 2331 | # update the number of role instances 2332 | $instances = $role.SelectSingleNode("*[local-name() = 'Instances']"); 2333 | $instances.Attributes["count"].Value = $configuration[$roleName] 2334 | } 2335 | 2336 | # update role settings 2337 | foreach($setting in $role.SelectSingleNode("*[local-name() = 'ConfigurationSettings']").SelectNodes("*[local-name() = 'Setting']")) 2338 | { 2339 | # common setting 2340 | $value = $configuration[$setting.name] 2341 | 2342 | # role-specific setting 2343 | $specificValue = $configuration["$($roleName).$($setting.name)"] 2344 | if($specificValue -ne $null) 2345 | { 2346 | $value = $specificValue 2347 | } 2348 | 2349 | if($value -ne $null) 2350 | { 2351 | Write-Log "Updating entry `"$($setting.name)`" to `"$value`"" 2352 | $setting.value = $value 2353 | } 2354 | } 2355 | } 2356 | 2357 | # save config 2358 | $xml.Save($configPath) 2359 | } 2360 | 2361 | function CreateAzureDeployment 2362 | { 2363 | param ( 2364 | $serviceName, 2365 | $slot, 2366 | $label, 2367 | $packageUrl, 2368 | $configPath 2369 | ) 2370 | 2371 | Write-Log "Creating new $slot deployment in $serviceName" 2372 | 2373 | # create and wait 2374 | $deployment = New-AzureDeployment -ServiceName $serviceName -Slot $slot -Label $label -Package $packageUrl -Configuration $configPath 2375 | WaitForAllCloudServiceInstancesRunning $serviceName $slot 2376 | 2377 | # get URL 2378 | $deployment = Get-AzureDeployment -ServiceName $serviceName -Slot $slot 2379 | Write-Log "Deployment created, URL: $($deployment.url)" 2380 | } 2381 | 2382 | function UpdateAzureDeployment 2383 | { 2384 | param ( 2385 | $serviceName, 2386 | $slot, 2387 | $label, 2388 | $packageUrl, 2389 | $configPath 2390 | ) 2391 | 2392 | Write-Log "Upgrading $slot deployment in $serviceName" 2393 | 2394 | $deployment = Set-AzureDeployment -Upgrade -ServiceName $serviceName -Slot $slot -Label $label -Package $packageUrl -Configuration $configPath -Force 2395 | WaitForAllCloudServiceInstancesRunning $serviceName $slot 2396 | 2397 | # get URL 2398 | $deployment = Get-AzureDeployment -ServiceName $serviceName -Slot $slot 2399 | Write-Log "Deployment upgraded, URL: $($deployment.url)" 2400 | } 2401 | 2402 | function DeleteAzureDeployment 2403 | { 2404 | param ( 2405 | $serviceName, 2406 | $slot 2407 | ) 2408 | 2409 | Write-Log "Deleting $slot deployment in $serviceName" 2410 | 2411 | $deployment = Remove-AzureDeployment -Slot $slot -ServiceName $serviceName -Force 2412 | 2413 | Write-Log "Deployment deleted" 2414 | } 2415 | 2416 | function WaitForAllCloudServiceInstancesRunning 2417 | { 2418 | param ( 2419 | $serviceName, 2420 | $slot 2421 | ) 2422 | 2423 | $deployment = Get-AzureDeployment -ServiceName $serviceName -Slot $slot 2424 | $instanceStatuses = @("") * $deployment.RoleInstanceList.Count 2425 | do 2426 | { 2427 | $deployment = Get-AzureDeployment -ServiceName $serviceName -Slot $slot 2428 | 2429 | for($i = 0; $i -lt $deployment.RoleInstanceList.Count; $i++) 2430 | { 2431 | $instanceName = $deployment.RoleInstanceList[$i].InstanceName 2432 | $instanceStatus = $deployment.RoleInstanceList[$i].InstanceStatus 2433 | if ($instanceStatuses[$i] -ne $instanceStatus) 2434 | { 2435 | $instanceStatuses[$i] = $instanceStatus 2436 | Write-Log "Starting Instance '$instanceName': $instanceStatus" 2437 | } 2438 | } 2439 | } 2440 | until(AllCloudServiceInstancesRunning($deployment.RoleInstanceList)) 2441 | } 2442 | 2443 | function AllCloudServiceInstancesRunning($roleInstanceList) 2444 | { 2445 | foreach ($roleInstance in $roleInstanceList) 2446 | { 2447 | if ($roleInstance.InstanceStatus -ne "ReadyRole") 2448 | { 2449 | return $false 2450 | } 2451 | } 2452 | 2453 | return $true 2454 | } 2455 | 2456 | function DownloadAzureApplicationConfiguration 2457 | { 2458 | param ( 2459 | $configUrl, 2460 | $configuration 2461 | ) 2462 | 2463 | $storageAccountName = $context.Configuration.AzureStorageAccountName 2464 | $storageAccountKey = $context.Configuration.AzureStorageAccountKey 2465 | 2466 | if(-not $storageAccountName -or -not $storageAccountKey) 2467 | { 2468 | $msg = @" 2469 | "Unable to download Azure application configuration file. 2470 | Set Azure cloud storage account details in the global deployment configuration: 2471 | - Set-DeploymentConfiguration AzureStorageAccountName 2472 | - Set-DeploymentConfiguration AzureStorageAccountKey " 2473 | "@ 2474 | throw $msg 2475 | } 2476 | 2477 | # download .cscfg file 2478 | $configPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()) 2479 | Write-Log "Downloading .cscfg from $configUrl to $configPath" 2480 | 2481 | # parse URL 2482 | $blobHost = ".blob.core.windows.net/" 2483 | $hostIdx = $configUrl.indexOf($blobHost) 2484 | 2485 | if($hostIdx -eq -1) 2486 | { 2487 | throw "Azure Cloud Service package must be uploaded to Azure storage blob and has URL of the form http://.blob.core.windows.net/../package.zip. If you use AppVeyor CI configure custom Azure storage for your account." 2488 | } 2489 | 2490 | $relativeUrl = $configUrl.substring($hostIdx + $blobHost.length) 2491 | 2492 | # get container and blob name 2493 | $idx = $relativeUrl.indexOf("/") 2494 | $containerName = $relativeUrl.substring(0, $idx) 2495 | $blobName = $relativeUrl.substring($idx + 1) 2496 | 2497 | # download config from blob 2498 | $blobContext = New-AzureStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey 2499 | Get-AzureStorageBlobContent -Container $containerName -Blob $blobName -Destination $configPath -Context $blobContext | Out-Null 2500 | 2501 | # update .cscfg file 2502 | if($configuration -ne $null) 2503 | { 2504 | UpdateAzureCloudServiceConfig $configPath $configuration 2505 | } 2506 | 2507 | return $configPath 2508 | } 2509 | 2510 | # get context parameters 2511 | $cloudService = $context.Environment.Configuration.CloudService 2512 | $slot = $context.Environment.Configuration.Slot 2513 | 2514 | # download configuration file 2515 | $configPath = (DownloadAzureApplicationConfiguration $role.ConfigUrl $context.Application.Configuration) 2516 | 2517 | # deploy 2518 | Write-Log "Check if $slot deployment already exists" 2519 | $deployment = Get-AzureDeployment -ServiceName $cloudService -Slot $slot -ErrorAction SilentlyContinue 2520 | if ($deployment.Name -ne $null) 2521 | { 2522 | Write-Log "$slot deployment exists" 2523 | 2524 | # should we delete or upgrade existing deployment? 2525 | if($context.Configuration.UpdateAzureDeployment) 2526 | { 2527 | UpdateAzureDeployment $cloudService $slot $context.Version $role.PackageUrl $configPath 2528 | } 2529 | else 2530 | { 2531 | Write-Log "Upgrade is not enabled. Re-creating $slot deployment." 2532 | 2533 | DeleteAzureDeployment $cloudService $slot 2534 | CreateAzureDeployment $cloudService $slot $context.Version $role.PackageUrl $configPath 2535 | } 2536 | } 2537 | else 2538 | { 2539 | Write-Log "$slot deployment does not exist" 2540 | 2541 | CreateAzureDeployment $cloudService $slot $context.Version $role.PackageUrl $configPath 2542 | } 2543 | } 2544 | 2545 | Set-DeploymentTask remove-azure { 2546 | Write-Log "Deleting Azure Cloud Service $($context.Application.Name)" 2547 | 2548 | # get context parameters 2549 | $cloudService = $context.Environment.Configuration.CloudService 2550 | $slot = $context.Environment.Configuration.Slot 2551 | 2552 | Write-Log "Deleting $slot deployment in $cloudService" 2553 | 2554 | $deployment = Remove-AzureDeployment -Slot $slot -ServiceName $cloudService -Force 2555 | 2556 | Write-Log "Deployment deleted" 2557 | } 2558 | 2559 | Set-DeploymentTask setup-azure-subscription -Before deploy-azure,remove-azure { 2560 | 2561 | Write-Log "Loading Azure module" 2562 | 2563 | # import modules 2564 | Import-Module Azure 2565 | 2566 | $subscriptionName = "DeploySubscription" 2567 | 2568 | # variables 2569 | $subscriptionId = $context.Configuration.AzureSubscriptionID 2570 | $subscriptionCertificate = $context.Configuration.AzureSubscriptionCertificate 2571 | 2572 | if($subscriptionId -and $subscriptionCertificate) 2573 | { 2574 | if(Get-AzureSubscription $subscriptionName) 2575 | { 2576 | Write-Log "Azure subscription is already set" 2577 | return 2578 | } 2579 | 2580 | Write-Log "Setup Azure subscription" 2581 | 2582 | # setup 2583 | $tempFolder = [IO.Path]::GetTempPath() 2584 | $publishSettingsFile = [System.IO.Path]::Combine($tempFolder, "azure-subscription.publishsettings") 2585 | $publishSettingsXml = @" 2586 | 2587 | 2588 | 2592 | 2595 | 2596 | 2597 | "@ 2598 | 2599 | # create publish settings file 2600 | Write-Log "Create Azure subscription settings file" 2601 | $sf = New-Item $publishSettingsFile -type file -force -value $publishSettingsXml 2602 | 2603 | # import subscription 2604 | Write-Log "Import publishing settings profile" 2605 | Import-AzurePublishSettingsFile $publishSettingsFile 2606 | Select-AzureSubscription -SubscriptionName $subscriptionName 2607 | } 2608 | } 2609 | #endregion 2610 | 2611 | # add local environment 2612 | New-Environment local 2613 | Add-EnvironmentServer local "localhost" 2614 | 2615 | # export module members 2616 | Export-ModuleMember -Function ` 2617 | Set-DeploymentConfiguration, Get-DeploymentConfiguration, ` 2618 | New-Application, Get-Application, Set-Application, Add-WebSiteRole, Add-ServiceRole, Set-WebSiteRole, Set-ServiceRole, ` 2619 | New-AzureApplication, Set-AzureApplication, ` 2620 | New-Environment, Get-Environment, Set-Environment, Add-EnvironmentServer, ` 2621 | New-AzureEnvironment, Set-AzureEnvironment, ` 2622 | Set-DeploymentTask, ` 2623 | Invoke-DeploymentTask, New-Deployment, Remove-Deployment, Restore-Deployment, Restart-Deployment, Stop-Deployment, Start-Deployment -------------------------------------------------------------------------------- /Tests/playground.ps1: -------------------------------------------------------------------------------- 1 | # import AppRoller 2 | Remove-Module AppRolla -ErrorAction SilentlyContinue 3 | $currentPath = Split-Path $myinvocation.mycommand.path 4 | Import-Module (Resolve-Path (Join-Path $currentPath ..\AppRolla.psm1)) 5 | 6 | $applicationName = "test-web" 7 | $applicationVersion = "1.0.7" 8 | 9 | $secureData = Get-ItemProperty -Path "HKCU:SOFTWARE\AppRolla\Tests" 10 | $adminUsername = $secureData.adminUsername 11 | $adminPassword = $secureData.adminPassword 12 | 13 | $securePassword = ConvertTo-SecureString $adminPassword -AsPlainText -Force 14 | $credential = New-Object System.Management.Automation.PSCredential $adminUsername, $securePassword 15 | 16 | # set global deployment configuration 17 | Set-DeploymentConfiguration TaskExecutionTimeout 60 # 1 min 18 | 19 | 20 | #Set-DeploymentConfiguration UseSSL $false 21 | #Set-DeploymentConfiguration SkipCACheck $false 22 | #Set-DeploymentConfiguration SkipCNCheck $false 23 | 24 | # AppVeyor API keys for downloading application artifacts 25 | Set-DeploymentConfiguration AppveyorApiKey $secureData.appveyorApiKey 26 | Set-DeploymentConfiguration AppveyorApiSecret $secureData.appveyorApiSecret 27 | 28 | function Get-AppVeyorPackageUrl 29 | { 30 | param ( 31 | $applicationName, 32 | $applicationVersion, 33 | $artifactName 34 | ) 35 | 36 | return "https://ci.appveyor.com/api/projects/artifact?projectName=$applicationName`&versionName=$applicationVersion`&artifactName=$artifactName" 37 | } 38 | 39 | # add application 40 | New-Application MyApp -Configuration @{ 41 | "key1" = "value1" 42 | } 43 | 44 | # add "Web site" role 45 | Add-WebSiteRole MyApp MyWebsite -DeploymentGroup web ` 46 | -WebsiteName "test deploy" ` 47 | -WebsitePort 8333 ` 48 | -PackageUrl (Get-AppVeyorPackageUrl $applicationName $applicationVersion "HelloAppVeyor.Web") ` 49 | -BasePath '$($env:SystemDrive)\websites\test-web' 50 | 51 | # add "Windows service" role 52 | Add-ServiceRole MyApp MyService -DeploymentGroup app ` 53 | -PackageUrl (Get-AppVeyorPackageUrl $applicationName $applicationVersion "HelloAppVeyor.Service") ` 54 | -Configuration @{ 55 | "ConnectionString.Default" = "server=locahost;" 56 | } 57 | 58 | # add Staging environment 59 | New-Environment Staging -Configuration @{ 60 | var1 = "value1" 61 | var2 = "value2" 62 | } 63 | 64 | # add environment servers 65 | Add-EnvironmentServer Staging test-ps2.cloudapp.net -Port 51281 -DeploymentGroup web,app 66 | Add-EnvironmentServer Staging test-ps1.cloudapp.net -DeploymentGroup app 67 | Add-EnvironmentServer Staging test-ps3.cloudapp.net -DeploymentGroup app 68 | 69 | # setup custom deployment tasks 70 | Set-DeploymentTask remove-from-lb -Before deploy,restart -Application $myApp.Name { 71 | Write-Log "CUSTOM TASK: Remove machine from load balancer" 72 | } 73 | 74 | Set-DeploymentTask add-to-lb -After deploy,restart -Application $myApp.Name { 75 | Write-Log "CUSTOM TASK: Add machine to load balancer" 76 | } 77 | 78 | Set-DeploymentTask task3 -After rollback -Application $applicationName -Version 1.2.0 -DeploymentGroup web { 79 | Write-Log "task3: do something on EACH node of web deployment group after successful rollback from 1.2.0" 80 | } 81 | 82 | 83 | # custom task to setup database 84 | Set-DeploymentTask setup-db -Before deploy,remove -DeploymentGroup app -PerGroup { 85 | # database setup code goes here 86 | Write-Log "Setup database on $($context.Environment.Name)!" 87 | } 88 | 89 | Set-DeploymentTask hello -DeploymentGroup web,app { 90 | Write-Output "Hello from $($env:COMPUTERNAME)!" 91 | } 92 | 93 | #Invoke-DeploymentTask hello -On staging -Serial -Verbose 94 | 95 | # perform deployment to staging 96 | #New-Deployment myapp 1.0.0 -To staging -Verbose #-Serial 97 | #New-Deployment myapp 1.0.1 -To staging -Verbose #-Serial 98 | New-Deployment myapp 1.0.2 -To staging -Verbose #-Serial 99 | #New-Deployment myapp 1.0.4 -To staging -Verbose #-Serial 100 | 101 | #New-Deployment myapp 1.0.0 -To local -Verbose -Serial 102 | 103 | #Remove-Deployment myapp -From staging -Verbose -Serial 104 | #Remove-Deployment myapp -From local -Verbose 105 | 106 | #Restore-Deployment myapp -On local 107 | #Restore-Deployment myapp -On staging 108 | 109 | #Remove-Deployment myapp -From staging 110 | 111 | 112 | #Restore-Deployment myapp -On staging -Verbose -Serial 113 | 114 | #Restart-Deployment myapp -On staging -Serial -Verbose 115 | 116 | #Get-DeploymentConfiguration UseSSL 117 | 118 | #Stop-Deployment myapp -On local 119 | #Stop-Deployment myapp -On staging 120 | #Start-Deployment myapp -On local 121 | 122 | #Start-Deployment myapp -On staging -Verbose 123 | 124 | # start deployment 125 | #Start-Deployment $myapp -On $staging 126 | 127 | Remove-Module AppRolla -------------------------------------------------------------------------------- /config.ps1: -------------------------------------------------------------------------------- 1 | # import AppRoller module 2 | Remove-Module AppRolla -ErrorAction SilentlyContinue 3 | $path = Split-Path -Parent $MyInvocation.MyCommand.Definition 4 | Import-Module (Join-Path $path AppRolla.psm1) 5 | 6 | # add application and roles 7 | New-Application MyApp 8 | 9 | Add-WebSiteRole MyApp MyWebsite -DeploymentGroup web ` 10 | -PackageUrl "http://my-storage.com/website-package.zip" 11 | 12 | Add-ServiceRole MyApp MyService -DeploymentGroup app ` 13 | -PackageUrl "http://my-storage.com/service-package.zip" 14 | 15 | # define Staging environment 16 | New-Environment Staging 17 | Add-EnvironmentServer Staging "staging.server.com" 18 | 19 | # define Production environment 20 | New-Environment Production 21 | Add-EnvironmentServer Production "web.server.com" -DeploymentGroup web 22 | Add-EnvironmentServer Production "app.server.com" -DeploymentGroup app -------------------------------------------------------------------------------- /deploy.ps1: -------------------------------------------------------------------------------- 1 | # load configuration script 2 | $path = Split-Path -Parent $MyInvocation.MyCommand.Definition 3 | . (Join-Path $path config.ps1) 4 | 5 | # deploy to Staging 6 | New-Deployment MyApp 1.0.0 -To Staging -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | $modules = "$home\Documents\WindowsPowerShell\Modules" 2 | Write-Host "Installing AppRolla module into your user profile: $modules" 3 | 4 | New-Item "$modules\AppRolla" -ItemType Directory -Force | Out-Null 5 | (New-Object Net.WebClient).DownloadFile("https://raw.github.com/AppVeyor/AppRolla/master/AppRolla.psm1", "$modules\AppRolla\AppRolla.psm1") -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AppRolla 2 | 3 | AppRolla is extensible framework for automating deployment of distributed .NET applications to multi-server environments. 4 | 5 | AppRolla can be used as utility for executing commands in parallel on multiple machines via remote PowerShell. Tasks can be implemented in PowerShell and then applied to machines in certain roles. 6 | 7 | AppRolla was inspired by Capistrano - a super popular deployment framework from Linux world. Though initial motivation to build AppRolla was having easy to use deployment framework as part of [AppVeyor Continuous Integration](http://www.appveyor.com) pipeline after starting the project it became clear AppRolla could be used in any build automation script or interactively from command line. 8 | 9 | 10 | ### Example 11 | The script below performs the deployment of sample project consisting of **ASP.NET web application** (front-end) and **Windows service** (back-end) to "staging" environment with one server and then to "production" environment with 2 web (front-end) servers and 2 application (back-end) servers: 12 | 13 | ```posh 14 | Import-Module AppRolla 15 | 16 | # describe application 17 | New-Application MyApp 18 | Add-WebsiteRole MyApp MyWebsite -PackageUrl "http://www.site.com/packages/myapp.web.zip" 19 | Add-ServiceRole MyApp MyService -PackageUrl "http://www.site.com/packages/myapp.service.zip" 20 | 21 | # Staging environment 22 | New-Environment Staging 23 | Add-EnvironmentServer Staging staging-server 24 | 25 | # Production environment 26 | New-Environment Production 27 | Add-EnvironmentServer Production web1.hostname.com -DeploymentGroup web 28 | Add-EnvironmentServer Production web2.hostname.com -DeploymentGroup web 29 | Add-EnvironmentServer Production app1.hostname.com -DeploymentGroup app 30 | Add-EnvironmentServer Production app2.hostname.com -DeploymentGroup app 31 | 32 | # custom task to setup database 33 | Set-DeploymentTask setup-db -Before deploy -DeploymentGroup app -PerGroup { 34 | # database setup code goes here 35 | } 36 | 37 | # deploy to Staging 38 | New-Deployment MyApp 1.0.0 -To Staging 39 | 40 | # deploy to Production 41 | New-Deployment MyApp 1.0.0 -To Production 42 | ``` 43 | 44 | ### Features and benefits 45 | 46 | - Deploys distributed applications to multi-server environments in parallel or server-by-server. 47 | - Compact module without external dependencies - just a single `AppRolla.psm1` file. 48 | - Provides natural PowerShell cmdlets with valid verbs, intuitive syntax and validation. We do not trying to mimic Capistrano, Chef or Puppet. 49 | - Can deploy to local machine for testing/development. 50 | - Easily extendable by writing your own custom tasks. 51 | - Open-source under Apache 2.0 license - easy to adopt and modify without the fear to be locked-in. 52 | 53 | 54 | ### Assumptions 55 | 56 | - AppRolla does not build application. It must be pre-built, pre-published (if it's web application project) and zipped. Use [AppVeyor](http://www.appveyor.com) to build your application and store artifacts in a cloud. 57 | - AppRolla does not upload or push application packages to remote machines. Packages must be uploaded to any external location accessible from remote servers. 58 | - AppRolla uses remote PowerShell. No agent installation required. We prepared [complete guide on how to setup PowerShell remoting](https://github.com/AppVeyor/AppRolla/wiki/Configuring-Windows-PowerShell-remoting). 59 | - AppRolla is a *deployment* solution, not a *release management* with deployment workflow and security. Though AppRolla can rollback, remove and restart deployments it is basically “point and shoot” tool. 60 | 61 | 62 | ### Installing AppRolla 63 | 64 | Download [`AppRolla.psm1`](https://raw.github.com/AppVeyor/AppRolla/master/AppRolla.psm1) file to `deployment` directory inside your project repository folder. 65 | 66 | To install AppRolla module under your user profile run this command in PowerShell console: 67 | 68 | ```posh 69 | (new-object Net.WebClient).DownloadString("https://raw.github.com/AppVeyor/AppRolla/master/install.ps1") | iex 70 | ``` 71 | If you want to install AppRolla globally for your account create new `AppRolla` folder inside `$Home\Documents\WindowsPowerShell\Modules` and put `AppRolla.psm1` into it, or just run this script in PowerShel console: 72 | 73 | Inside `deployment` directory create a new "configuration" script named [`config.ps1`](https://github.com/AppVeyor/AppRolla/blob/master/config.ps1) where you describe your application and environments. You can use this template for your own config: 74 | 75 | ```posh 76 | # import AppRolla module 77 | Remove-Module AppRolla -ErrorAction SilentlyContinue 78 | $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition 79 | Import-Module (Join-Path $scriptPath AppRolla.psm1) 80 | 81 | # use this snippet to import AppRolla module if it was installed globally 82 | # Import-Module AppRolla 83 | 84 | # add application and roles 85 | New-Application MyApp 86 | 87 | Add-WebSiteRole MyApp MyWebsite -DeploymentGroup web ` 88 | -PackageUrl "http://my-storage.com/website-package.zip" 89 | 90 | Add-ServiceRole MyApp MyService -DeploymentGroup app ` 91 | -PackageUrl "http://my-storage.com/service-package.zip" 92 | 93 | # define Staging environment 94 | New-Environment Staging 95 | Add-EnvironmentServer Staging "staging.server.com" 96 | 97 | # define Production environment 98 | New-Environment Production 99 | Add-EnvironmentServer Production "web.server.com" -DeploymentGroup web 100 | Add-EnvironmentServer Production "app.server.com" -DeploymentGroup app 101 | ``` 102 | 103 | Now, if want to use AppRolla interactively just open PowerShell console in `deployment` directory and load configuration: 104 | 105 | ```posh 106 | PS> .\config.ps1 107 | ``` 108 | 109 | `config.ps1` script will load AppRolla module and add application and environments to the current session. 110 | 111 | Now you can just run AppRolla cmdlets from PowerShell command line, for example: 112 | 113 | ```posh 114 | PS> New-Deployment MyApp 1.0.0 -To Staging 115 | ``` 116 | 117 | If you are going to use AppRolla in your continuous integration environment add [`deploy.ps1`](https://github.com/AppVeyor/AppRolla/blob/master/deploy.ps1) script performing application deployment and using configuration from `config.ps1`: 118 | 119 | ```posh 120 | # load configuration script 121 | $path = Split-Path -Parent $MyInvocation.MyCommand.Definition 122 | . (Join-Path $path config.ps1) 123 | 124 | # deploy to Staging 125 | New-Deployment MyApp 1.0.0 -To Staging 126 | ``` 127 | 128 | ### Using AppRolla 129 | 130 | #### Describe your applications 131 | 132 | AppRolla deploys *applications*. Application is a logical group of roles that are deployed/manipulated as a single entity under the same version. You can think application is VS.NET solution and role is VS.NET project. 133 | 134 | Out-of-the-box AppRolla supports two types of roles: 135 | 136 | * **WebSite** role - this is IIS web site 137 | * **Service** role - Windows service 138 | 139 | Application should have at least one role defined, for example website role if you are going to deploy your ASP.NET application. 140 | 141 | Each role should have a corresponding package with application files. Package is just a zip with ASP.NET application or Windows service files - nothing more. Packages should be available for download from remote environment machines. When you build your project with [AppVeyor CI](http://www.appveyor.com) package is an artifact. 142 | 143 | To add a new application: 144 | 145 | ```posh 146 | New-Application MyApp 147 | ``` 148 | 149 | To add website role to the application: 150 | 151 | ```posh 152 | Add-WebsiteRole MyApp MyWebsite -PackageUrl "http://www.site.com/packages/myapp.web.zip" 153 | ``` 154 | 155 | Be default, website role is deployed to "Defaul Web Site" which is cool if you plan to have only one web application running on the target server. However, if you need to create a new IIS web site you can specify its details when adding role: 156 | 157 | ```posh 158 | Add-WebsiteRole MyApp MyWebsite ` 159 | -PackageUrl "http://www.site.com/packages/myapp.web.zip" 160 | -WebsiteName "MyWebsite" ` 161 | -WebsiteProtocol http ` 162 | -WebsiteHost www.mywebsite.com 163 | -WebsitePort 8088 164 | -WebsiteIP * 165 | ``` 166 | 167 | To add service role: 168 | 169 | ```posh 170 | Add-ServiceRole MyApp MyService -PackageUrl "http://www.site.com/packages/myapp.service.zip" 171 | ``` 172 | 173 | By default, a Windows service will use the first .exe file found in the package, have service name and service display name equal to the role name ("MyService" in our case). If you have more than one .exe in the package or want to customize Windows service details you can use extended `Add-ServiceRole` syntax: 174 | 175 | ```posh 176 | Add-ServiceRole MyApp MyService ` 177 | -PackageUrl "http://www.site.com/packages/myapp.service.zip" 178 | -ServiceExecutable "myservice.exe" ` 179 | -ServiceName "myapp.myservice" ` 180 | -ServiceDisplayName "My app service" 181 | -ServiceDescription "The service for hosting WCF back-end of my application." 182 | ``` 183 | 184 | #### Describe your environments 185 | 186 | Application is deployed to *environment*. Environment has a name and could include one or more *servers* with remote PowerShell configured. 187 | 188 | To add a new "staging: environment with two web servers and one application server: 189 | 190 | ```posh 191 | New-Environment Staging 192 | Add-EnvironmentServer Staging web1.hostname.com -DeploymentGroup web 193 | Add-EnvironmentServer Staging web2.hostname.com -DeploymentGroup web 194 | Add-EnvironmentServer Staging app1.hostname.com -DeploymentGroup app 195 | ``` 196 | 197 | 198 | ##### What is deployment group? 199 | 200 | If you have a complex application with several roles that you want to deploy to a multi-server environment how would you tell AppRolla to deploy certain application roles to specific servers? You could use `DeploymentGroup` parameter that could be specified for application roles and environment servers. 201 | 202 | For example, we have an application consisting of web application and a Windows service and we want to deploy web application to IIS *web cluster* and Windows service to an application server. 203 | 204 | First, we apply `DeploymentGroup` when adding website and service roles. Our example above could be extended as: 205 | 206 | ```posh 207 | Add-WebsiteRole MyApp MyWebsite -DeploymentGroup web ... 208 | Add-ServiceRole MyApp MyService -DeploymentGroup app ... 209 | ``` 210 | 211 | Application role could belong to zero or one deployment group only. If a role doesn't have deployment group specified it will be deployed to all servers. `DeploymentGroup` is an arbitrary string and you can use your own naming conventions. 212 | 213 | Each environment server could belong to zero or many deployment groups. If server deployment group is not specified all application roles will be deployed to it. For example: 214 | 215 | ```posh 216 | # this server accepts roles of "web" group only 217 | Add-EnvironmentServer Staging web1.hostname.com -DeploymentGroup web 218 | 219 | # this server accepts roles of "web" and "app" groups 220 | Add-EnvironmentServer Staging srv1.hostname.com -DeploymentGroup web,app 221 | 222 | # this server accepts roles of all groups 223 | Add-EnvironmentServer Staging srv2.hostname.com 224 | ``` 225 | 226 | 227 | #### Deploying application 228 | 229 | To deploy application to "staging" environment use the following command: 230 | 231 | ```posh 232 | New-Deployment MyApp 1.0.0 -To Staging 233 | ``` 234 | 235 | This command will create a new "1.0.0" deployment of "MyApp" application on "Staging" environment servers. Website role will be deployed to `web1.hostname.com` and `web2.hostname.com` servers and service role to `app1.hostname.com`. 236 | 237 | > We recommend using release version as a deployment name, however you can put any semantics into it. 238 | 239 | 240 | ##### Running deployment script server-by-server 241 | 242 | By default, AppRolla deployment tasks are executed on all environment servers in parallel. However, in some cases you might want to run a script server-by-server. For example, you may have a custom task extension removing web node from load balancer before deployment and adding it back after it. To run deployment task server-by-server use `Serial` switch: 243 | 244 | ```posh 245 | New-Deployment MyApp 1.0.0 -To Staging -Serial 246 | ``` 247 | 248 | 249 | ##### Deployment directory structure 250 | 251 | Default base path for all application deployments is `:\applications` where `` is a system drvie on remote server. 252 | 253 | Each application role deployment creates a new directory on remote server: 254 | 255 | \\\ 256 | 257 | for example, deploying version 1.0.0 of our sample application with two roles to a single-server staging environment will create the following directory structure: 258 | 259 | ```posh 260 | c:\applications\MyApp\ 261 | c:\applications\MyApp\MyWebsite 262 | c:\applications\MyApp\MyWebsite\1.0.0 263 | c:\applications\MyApp\MyService 264 | c:\applications\MyApp\MyService\1.0.0 265 | ``` 266 | 267 | After deploying of another 1.0.1 version we will have this sctructure: 268 | 269 | ```posh 270 | c:\applications\MyApp\ 271 | c:\applications\MyApp\MyWebsite 272 | c:\applications\MyApp\MyWebsite\1.0.0 273 | c:\applications\MyApp\MyWebsite\1.0.1 # "current" deployment 274 | c:\applications\MyApp\MyService 275 | c:\applications\MyApp\MyService\1.0.0 276 | c:\applications\MyApp\MyService\1.0.1 # "current" deployment 277 | ``` 278 | 279 | Website root folder and Windows service executable path will be changed to a new path. 280 | 281 | To change a base path for all deployments globally use this command: 282 | 283 | ```posh 284 | Set-DeploymentConfiguration ApplicationsPath 'c:\myapps' 285 | ``` 286 | 287 | You can use environment variables in the path to be resolved on remote server. Path must be set in a **single quotes** then: 288 | 289 | ```posh 290 | Set-DeploymentConfiguration ApplicationsPath '$($env:SystemDrive)\myapps' 291 | ``` 292 | 293 | To set a base path on role level use `BasePath` parameter when adding a role: 294 | 295 | ```posh 296 | # to deploy website to c:\websites\ directory 297 | Add-WebsiteRole MyApp MyWebsite -BasePath 'c:\websites' ... 298 | 299 | # to deploy Windows service to Program Files directory 300 | Add-ServiceRole MyApp MyService -BasePath '$($env:ProgramFiles)\MyService' ... 301 | ``` 302 | 303 | ##### How to deploy locally? 304 | 305 | AppRolla allows running deployment tasks locally. This is useful for development/testing purposes as well as for testing custom deployment tasks. 306 | 307 | To deploy locally use built-in "local" environment: 308 | 309 | ```posh 310 | New-Deployment MyApp 1.0.0 -To local 311 | ``` 312 | 313 | This environment has only one "localhost" server. If you need to deploy to local machine as part of your own environment (it's really hard to figure out a real use-case, but anyway :) add a server with reserved "localhost" name: 314 | 315 | ```posh 316 | New-Environment Dev 317 | ... 318 | Add-EnvironmentServer Dev localhost 319 | ``` 320 | 321 | ##### How to update application configuration files? 322 | 323 | AppRolla can update configuration settings in `appSettings` and `connectionString` sections on web.config and app.config files while deploying web applications and Windows services. 324 | 325 | Specify configuration settings in the format `appSettings.` or `connectionString.` to update keys in corresponding sections while adding a role: 326 | 327 | ```posh 328 | Add-WebsiteRole MyApp MyWebsite ... -Configuration { 329 | "appSettings.SiteUrl" = "http://www.mysite.com" 330 | } 331 | 332 | Add-ServiceRole MyApp MyWebsite ... -Configuration { 333 | "connectionString.Default" = "server=locahost; ..." 334 | } 335 | ``` 336 | 337 | #### Rollback deployment 338 | 339 | What if you did a mistake and accidentially deployed a broken release (it could never happen if you deploy as part of continuous integration process in [AppVeyor](http://www.appveyor.com) as only green builds are being deployed)? 340 | 341 | You can rollback deployment to a previous release by this command: 342 | 343 | ```posh 344 | Restore-Deployment MyApp -On Production 345 | ``` 346 | 347 | To rollback to a specific version: 348 | 349 | ```posh 350 | Restore-Deployment MyApp 1.0.0 -On Production 351 | ``` 352 | 353 | By default, AppRolla stores 5 previous releases on remote servers, but you can change this number by modifying the following setting: 354 | 355 | ```posh 356 | Set-DeploymentConfiguration KeepPreviousVersions 10 357 | ``` 358 | 359 | During the rollback AppRolla switches websites and Windows services to new directories and deletes current release directories. 360 | 361 | #### Removing deployment 362 | 363 | To delete specific (previous as you cannot delete current deployment) application deployment use the following command: 364 | 365 | ```posh 366 | Remove-Deployment MyApp 1.0.0 -From Staging 367 | ``` 368 | 369 | To delete all application deployments ommit version parameter: 370 | 371 | ```posh 372 | Remove-Deployment MyApp -From Staging 373 | ``` 374 | 375 | #### Start/Stop/Restart deployed application 376 | 377 | When you start/stop/restart application on specific environment its role IIS website application pools or Windows services are started/stopped/restarted on all remote servers. 378 | 379 | To stop application: 380 | 381 | ```posh 382 | Stop-Deployment MyApp -On Staging 383 | ``` 384 | 385 | To start application: 386 | 387 | ```posh 388 | Start-Deployment MyApp -On Staging 389 | ``` 390 | 391 | To restart application: 392 | 393 | ```posh 394 | Restart-Deployment MyApp -On Staging 395 | ``` 396 | 397 | 398 | #### PowerShell remoting 399 | 400 | AppRolla uses remote PowerShell to run deployment tasks on remote servers. By relying on remote PowerShell technology we are strongly commited to provide you all required information on how to get started saving you hours of crawling the internet and find the answers. 401 | 402 | Read [complete guide on how to setup PowerShell remoting](https://github.com/AppVeyor/AppRolla/wiki/Configuring-Windows-PowerShell-remoting). In that article you will know how to issue correct SSL certificate that could be used to setup WinRM HTTPS listener, install SSL certificate on remote machine, enable remote PowerShell and configure firewall. 403 | 404 | ##### Configuring PowerShell remoting settings 405 | 406 | By default, AppRolla will try to connect remote environment server via HTTPS on port 5986. To change default communication protocol to HTTP on port 5985 update this global setting: 407 | 408 | ```posh 409 | Set-DeploymentConfiguration UseSSL $false 410 | ``` 411 | 412 | To set a custom port for each environment server use `-Port` parameter when adding a server: 413 | 414 | ```posh 415 | Add-EnvironmentServer Staging web1.hostname.com -Port 51434 ... 416 | ``` 417 | 418 | 419 | ##### Authenticating remote PowerShell sessions 420 | 421 | To connect remote server that is not a member of the same AD domain you should provide user account credentials (username/password). How to securely store those credentials and pass them to AppRolla? 422 | 423 | When using AppRolla interactively from your development machine the best way to store servers credentials is **Windows Credential Manager**. You find Credential Manager by searching for "credential" in Control Panel. You should add **Windows credentials** for each server you are going to deploy to. 424 | 425 | What if you are deploying to a very large environment with dozens of servers and want to use the same username/password to connect them. You can specify credentials for the entire environment using `-Credential` parameter when adding environment. For example, using the code below you will be asked to type "Administrator" account password every time you deploy: 426 | 427 | ```posh 428 | $cred = Get-Credential -UserName Administrator 429 | New-Environment Staging -Credential $cred 430 | ``` 431 | 432 | If you don't want to type a password every time you can store it in the registry and then create credentials object like that: 433 | 434 | ```posh 435 | $secureData = Get-ItemProperty -Path "HKCU:SOFTWARE\AppRolla\Tests" # your path here 436 | $adminUsername = $secureData.adminUsername # your key here 437 | $adminPassword = $secureData.adminPassword # your key here 438 | $securePassword = ConvertTo-SecureString $adminPassword -AsPlainText -Force 439 | $credential = New-Object System.Management.Automation.PSCredential $adminUsername, $securePassword 440 | 441 | New-Environment Staging -Credential $cred 442 | ``` 443 | 444 | Storing any settings in the registry will ensure you don't check-in sensitive information in source control and also allows every developer of your team to use their own settings. 445 | 446 | You can also set credentials for each server individually: 447 | 448 | ```posh 449 | New-Environment Staging 450 | Add-EnvironmentServer Staging web1.hostname.com -Credential $cred ... 451 | ``` 452 | 453 | 454 | ### Custom tasks 455 | 456 | By implementing tasks you can extend existing functionality or add a completely new scenarios to AppRolla. Those tasks could be anything from putting application settings into registry, creating application database or setting permissions to excluding/including server from load balancing during the deployment. 457 | 458 | #### Basic example 459 | 460 | OK, let's say we want to do something *before* application deployment on each environment server. The task could look like: 461 | 462 | ```posh 463 | Set-DeploymentTask mytask1 -Before deploy { 464 | # this code runs on every environment server BEFORE deployment 465 | } 466 | ``` 467 | 468 | #### Extending existing tasks 469 | 470 | You can add a custom action to run *before* or *after* any task. 471 | 472 | AppRolla defines the following standard tasks: 473 | 474 | * init 475 | * deploy 476 | * authenticate-download-client 477 | * deploy-website 478 | * deploy-service 479 | * remove 480 | * rollback 481 | * start 482 | * stop 483 | * restart 484 | 485 | For example, to have a custom task doing something *after* deployment use `-After` parameter: 486 | 487 | ```posh 488 | Set-DeploymentTask mytask2 -After deploy { 489 | # this code runs on every environment server AFTER deployment 490 | } 491 | ``` 492 | 493 | You can combine both `-Before` and `-After` for the same task. Also, you can specify multiple dependent tasks, like that: 494 | 495 | ```posh 496 | Set-DeploymentTask mytask3 -Before deploy -After deploy,rollback { 497 | # this code runs on every environment server BEFORE deployment and AFTER deployment or rollback 498 | } 499 | ``` 500 | 501 | #### Applying task to specific deployment groups 502 | To apply the task to specific deployment groups only use `-DeploymentGroup` parameter: 503 | 504 | ```posh 505 | Set-DeploymentTask mytask4 -Before deploy -DeploymentGroup web { 506 | # this code runs on environment servers of 'web' group only BEFORE deployment 507 | } 508 | ``` 509 | 510 | Sometimes, you have to execute some code once per group, for example to setup an application SQL Server database. Obviously, running database setup scripts on *every* server will cause racing condition and won't work. To run task once per group use `-PerGroup` parameter: 511 | 512 | ```posh 513 | Set-DeploymentTask setup-db -Before deploy -DeploymentGroup web -PerGroup { 514 | # setup application database here 515 | # this code will run on a single server from 'web' deployment group 516 | } 517 | ``` 518 | 519 | Oh, suppose we have to deploy an application to a load-balanced web cluster. Usually, we want to deploy node-by-node with removing a node from load balancing before deployment and adding it back when deployment is finished. How do we do that? Every *-Deployment cmdlet has `-Serial` switch to execute a task no in parallel, but server-by-server. To deploy to load balanced cluster you could use the following skeleton: 520 | 521 | ```posh 522 | Set-DeploymentTask remove-from-lb -Before deploy -DeploymentGroup web { 523 | # code to remove current node from load balancer 524 | } 525 | 526 | Set-DeploymentTask add-to-lb -After deploy -DeploymentGroup web { 527 | # code to add current node to load balancer 528 | } 529 | 530 | New-Deployment MyApp 1.0.0 -To Production -Serial # run script server-by-server 531 | ``` 532 | 533 | 534 | #### Applying task to a specific application 535 | 536 | To define a task specific for some application or even version use `-Application` and `-Version` parameters: 537 | 538 | ```posh 539 | Set-DeploymentTask myapp-specific-task -Before deploy -Application MyApp -Version 1.0.0 { 540 | # do something here 541 | } 542 | ``` 543 | 544 | Another cool example - running some compensation code against application database while rolling back *from* version 1.1.0: 545 | 546 | ```posh 547 | Set-DeploymentTask rollback-db -After rollback -Application MyApp -Version 1.1.0 -PerGroup { 548 | # this code will run only once per environment 549 | # when rolling back MyApp application from 1.1.0 version 550 | } 551 | ``` 552 | 553 | ### Custom tasks 554 | 555 | You can define your own custom tasks and then apply them to machines in certain deployment groups. 556 | 557 | Let's enjoy a greeting from every server in Staging environment: 558 | 559 | ```posh 560 | Set-DeploymentTask hello { 561 | Write-Output "Hello from $($env:COMPUTERNAME)!" 562 | } 563 | 564 | Invoke-DeploymentTask hello -On Staging 565 | ``` 566 | 567 | To run the task on "web" servers only add `-DeploymentGroup`: 568 | 569 | ```posh 570 | Set-DeploymentTask hello-from-web -DeploymentGroup web { 571 | Write-Output "Hello from $($env:COMPUTERNAME)!" 572 | } 573 | ``` 574 | 575 | To run a task not in parallel use `-Serial` parameter: 576 | 577 | ```posh 578 | Invoke-DeploymentTask hello -On Staging -Serial 579 | ``` 580 | 581 | #### Pushing configuration to remote servers 582 | 583 | All tasks run in the same PowerShell scope. The following variables are pre-defined in the scope by AppRolla: 584 | 585 | - `$context.Configuration` - hashtable with global configuration. To set global variables use `Set-DeploymentConfiguration` cmdlet. 586 | - `$context.TaskName` - the name of currently executing task. 587 | - `$context.Application` - the name of currently deploying application. 588 | - `$context.Version` - currently deploying application version. 589 | - `$context.Server.ServerAddress` - the host name of environment server running the script. 590 | - `$context.Server.DeploymentGroup` - array of deployment groups assigned to the current server. 591 | - `$context.Environment.Name` - the name of current environment 592 | - `$context.Environment.Configuration` - hashtable of configuration variables defined on environment level. 593 | 594 | To define configuration variables on environment level use `-Configuration` parameter: 595 | 596 | ```posh 597 | New-Environment Staging -Configuration @{ 598 | "variable1" = "value1" 599 | "variable2" = "value2" 600 | ... 601 | } 602 | ``` 603 | 604 | To define configuration variables on role level use `-Configuration` parameter when adding a role: 605 | 606 | ```posh 607 | Add-WebsiteRole MyApp MyWebsite ... -Configuration @{ 608 | "variable1" = "value1" 609 | "appSettings.setting1" = "value2" 610 | "connectionStrings.Name" = "connection string details" 611 | ... 612 | } 613 | ``` 614 | 615 | When extending standard `-website` or `-service` tasks `$role` variable is added to the scope with current role details: 616 | 617 | - `$role.Type` - role type 618 | - `$role.Name` - the name of role 619 | - `$role.PackageUrl` - URL of role artifact package 620 | - `$role.BasePath` - base path for role releases 621 | - `$role.RootPath` - release installation installation root (application root) 622 | - `$role.Versions` - the list of installed versions (latest first) 623 | 624 | 625 | 626 | ### License 627 | 628 | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 629 | 630 | 631 | 632 | ### How to contribute 633 | 634 | Contributions are welcome! Submit a pull request or issue here on GitHub or just drop us a message at [team@appveyor.com](mailto:team@appveyor.com). 635 | 636 | 637 | 638 | ### Credits 639 | 640 | * [Capistrano](https://github.com/capistrano/capistrano) for the basic idea on how cool deployment framework should look like. 641 | * [Unfold](https://github.com/thomasvm/unfold) for the idea on how Capistrano-like deployment framework might look on Windows platform. Thanks Thomas, the author of Unfold, for super-clean and easy to read code - we got some code snippets and principles from Unfold. 642 | --------------------------------------------------------------------------------