We recently built and deployed an ingestion pipeline for iot data and centered our architecture around aws lambda. Now that we are reaching scale some of these lambdas start timing out, and I want to use temporary EC2 instances to process these longer running tasks.
I have a lambda setup that invokes spins up an ec2 instance and runs a UserData script. This is the relevant code involved:
import { EC2 } from 'aws-sdk'
const region = 'eu-central-1'
const ImageId = 'ami-0a02ee601d742e89f'
const InstanceType = 't2.micro'
const ec2 = new EC2({ apiVersion: '2016-11-15', region })
const ec2Scheduler = async () => {
const initScript = `#!/bin/bash
shutdown -h +5`
const UserData = new Buffer(initScript).toString('base64')
console.log(`Running EC2 instance with script: ${UserData}`)
const instance = await ec2
.runInstances({
ImageId,
InstanceType,
UserData,
MinCount: 1,
MaxCount: 1,
InstanceInitiatedShutdownBehavior: 'terminate',
})
.promise()
const instanceId = instance?.Instances?.[0]?.InstanceId
console.log(
`Ec2 instance with id ${instanceId} created. will auto shutdown in 5 minutes`
)
}
All fine and dandy. Except I'm stuck on how to get my javascript executable transferred over to my temporary EC2 instance.
What is the way to go here? I'm currently considering either:
a. Storing bitbucket credentials in secretsmanager. Then using my userdata script to install node/git and clone the repository from there.
b. Update my deployment pipelines to store the javascript executable in s3, then using the aws cli from my userdata script to fetch the executable and run it.
Both options seem a bit unwieldy. Is there a more direct/straightforward/lazy approach?
EDIT=====================
I think I need to adjust my mental model in a way that is not quite clear to me yet. My problem is not how do i get the code out of s3 from whitin my ec2 instance, but how do i use cloudformation to specify an executable to be made available in s3. From working with cloudformation/lambdas i am used to writing things like:
Scheduler:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
CodeUri: ../dist/task-scheduler
As a result of the package command the code referenced in CodeUri is then bundled and uploaded to an s3 deployment bucket and made available to the lambdas. I imagine a similar solution here. A resource type that i can provide with a codeUri, which my ec2 instance can then fetch from s3, and execute.
Thanks for the insightful comments so far!
A common practice is to download scripts from an S3 bucket. The UserData is limited to 16KB so any large scripts you need to download and execute anyway (unless you bake the AMI with the files on there).
It will be easier to keep it on all in AWS for PROD and you want PROD to be easy!
For dev machines I clone repo's (after copying ssh keys) and set them up that way. For production machines its all Cloud Formation with UserData and S3 (or Artifactory), any credentials stored in SSM Parameter store and all permissions locked down with an IAM role assigned to the EC2 with specific access to the S3 Bucket.
The key point is there's some control before it goes to PROD, we don't clone off repo's directly to the PROD machine, there's a build, test and deployment phase. Where as Dev go for it - clone from a Branch if you want!
Sorry I don't have a version in Javascript/NodeJS, here's a x-platform PowerShell example you can use or at least to follow the steps I use to configure either:
DEV:
$region = 'us-west-1'
Write-Host 'Install NuGet, Git, SSH Keys directory and set region'
choco install git -y
$newPath = "$($env:PATH)C:\Program Files\Git\cmd;"
[Environment]::SetEnvironmentVariable( 'PATH', $newPath, "Machine")
$env:PATH = $newPath
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
install-module posh-git -force
import-module awspowershell
New-Item -ItemType Directory -Force -Path 'C:\Users\Administrator\.ssh' | out-null
$destinDir = "C:\Users\$($env:username)\.ssh"
$prefix = "tempcreds/$user/"
set-defaultawsregion -region $region
if ($user) {
Write-Host 'Download GitHub Keys: Copy-S3Object -bucketname ' + $bucket + ' -key ' + $($prefix) + 'YOURKEYNAME -localfile ' + $destinDir + '\YOURKEYNAME'
Copy-S3Object -bucketname $bucket -key "$($prefix)YOURKEYNAME" -localfile $destinDir\YOURKEYNAME -region $region
Copy-S3Object -bucketname $bucket -key "$($prefix)YOURKEYNAME.pub" -localfile $destinDir\YOURKEYNAME.pub -region $region
Write-Host 'Remove GitHub Keys: -key ' + $($prefix) + 'YOURKEYNAME'
Remove-S3Object -bucketname $bucket -key "$($prefix)YOURKEYNAME" -force -region $region
Remove-S3Object -bucketname $bucket -key "$($prefix)YOURKEYNAME.pub" -force -region $region
Write-Host 'Save the GitHub Known_Hosts file'
add-content -path "$destinDir\known_hosts" `
-value $githubKnownHosts
git config --global user.email $email
git config --global user.name $user
}
Write-Host 'cd to dev directory'
$devDir = 'C:\DEV'
new-item -itemtype directory -force -path $devDir | out-null
cd $devDir
Write-Host 'Execute git clone <git.com/YOUREPO>'
git clone <git.com/YOUREPO>.git
PROD:
#using the AWS API with S3 fetch the powershell install script and execute it
$S3BucketName = "unique-bootstrap-bucketname"
$bootstrap = "install-YOURREPO.ps1"
$script = ($path + $bootstrap)
Set-DefaultAWSRegion -Region $region
Copy-S3Object -BucketName $S3BucketName -key $bootstrap -LocalFile ($path + $bootstrap)
& $script -S3Name $S3BucketName
Related
Developing Active Directory for a scalable and hackable student environment and I cannot manage to preserve the Domain Trust Relationship after the VM's restart. On first launch everything works, but after stopping/starting the AD Set, the trust relationship is broken.
Configuration Basics.
Three machines built and running in AWS (Windows Server 2012)
Domain Controller (Pre-Built Forest, Domains, Users, Computers, GPO's, etc)
Two "Targets" with varying permissions.
AMIs are built and domian joined before imaging.
Prior to imaging, Target Boxes are REMOVED from the domain, leaving a single DC and two un-joined Windows Server 2012 boxes.
Boxes are stopped without SysPrep to preserve SIDs and other variables like admin passwords, and an image is taken. User data is enabled
At this point, I can relaunch these boxes from AMI, re-join the domain, and I have no issues after restarting.
Here are the next steps.
The AMI's are run through a code pipeline that applies user data to the boxes to Domain Join and set the DNS to the IP of the DC.
Code exists to prevent the targets from crating until the DC is listening so they can join the domain.
On creation, things work flawlessly again.
After stopping and restarting, however, I start getting "NO_LOGON_SERVER" errors with tools, and cannot login with a domain user.
There are obvious solutions, but nearly all of them manual, and this must be automated. Furthermore, I must configure this in a way that no passwords are exposed on the box or retained as tickets or hashes in lsass that could ruin the exploint path.
If it helps, here is the User-Data that domain joins the targets.
<powershell>
# Checking for response from DC
do {
Start-Sleep 30
} until(Test-NetConnection -InformationLevel Quiet '{DC_IP_ADDR}')
# "true" if Domain Joined
$dCheck = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
# Join domain if not already, skip if joined.
if ($dCheck -eq 'True') {
echo "I was domain joined after restarting." >> C:\Windows\Temp\log.txt
}
else {
# Allows 'rdesktop' by disabling NLA as a requirement
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\' -Name "fDenyTSConnections" -Value 0
# Set DNS to DC IP address via custom variable
$adapterIndex = Get-NetAdapter | % { Process { If ( $_.Status -eq "up" ) { $_.ifIndex } }}
Set-DNSClientServerAddress –interfaceIndex $adapterIndex –ServerAddresses ('{DC_IP_ADDR}','8.8.8.8')
#Set DA Credential object and join domain
$username = 'MYDOMAIN\ADMINISTRATOR'; $password = ConvertTo-SecureString -AsPlainText 'NotTheActualPasswordButReallySecure' -Force; $Credentials = New-Object System.Management.Automation.PSCredential $Username,$Password
Add-Computer -Domain 'MYDOMAIN.LOCAL' -Credential $Credentials -Force -Restart
}
</powershell>
<persist>true</persist>
And here is the Domain Controller. It is scheduled to change it's DA Password after 4 minutes so that the password exposed in the user data above is no longer valid
<powershell>
# Enable Network Discovery
netsh advfirewall firewall set rule group="Network Discovery" new enable=Yes
# Allows 'rdesktop' by disabling NLA as a requirement
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\' -Name "fDenyTSConnections" -Value 0
Start-Sleep -Seconds 240
# Recycle DA Password
powershell.exe -C "C:\Windows\Temp\recycle.ps1"
echo "Done" > "C:\Users\Administrator\Desktop\done.txt"
</powershell>
I have a CI/CD Multistage template where my CD stages are dependent on a parameter I provide in a yaml file
Pipeline points to pipeline.yml
servers:
DEV:
- srv-apimgmt37p
and in my template I have a loop that checks the servers and passes the value so it can dynamically produce my CI/CD pipeline depending on the above parameter. In my CD stage I have the following variable groups that I pass:
variables:
- group: ${{ variables['Build.DefinitionName'] }}_MS_${{env.key}}
- group: DevSecOps_${{ variables['Build.DefinitionName'] }}_MS_${{env.key}}
In one of those groups I have a variable which is the name of file that is stored in my secure files. Going back to my CD template, I have a Download Secure File task which will download the secure file using the name of the variable from the group called $(test)
- task: DownloadSecureFile#1
displayName: 'Download kafka keytab'
condition: "eq(ne(variables['test'], ''), true)"
inputs:
secureFile: "$(test)"
retryCount: 5
The problem is that when the pipeline starts running, it tries to download the secure file first, but it cannot find it because it doesn't know yet the value of $(test). What should I do as a best practice in this scenario? I'm a little stuck on what a good solution would be.
DownloadSecureFile task is a pre-job. You may try use the powershell to download the Secure File as case Download secure file with PowerShell mentioned.
I was able to download Secure Files using a REST API, the task's
Access Token, and an Accept header for application/octet-stream. I
enabled "Allow scripts to access the OAuth token". Here my task.json
is using a secureFile named "SecureFile."
$secFileId = Get-VstsInput -Name SecureFile -Require
$secTicket = Get-VstsSecureFileTicket -Id $secFileId
$secName = Get-VstsSecureFileName -Id $secFileId
$tempDirectory = Get-VstsTaskVariable -Name "Agent.TempDirectory" -Require
$collectionUrl = Get-VstsTaskVariable -Name "System.TeamFoundationCollectionUri" -Require
$project = Get-VstsTaskVariable -Name "System.TeamProject" -Require
$filePath = Join-Path $tempDirectory $secName
$token= Get-VstsTaskVariable -Name "System.AccessToken" -Require
$user = Get-VstsTaskVariable -Name "Release.RequestedForId" -Require
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $User, $token)))
$headers = #{
Authorization=("Basic {0}" -f $base64AuthInfo)
Accept="application/octet-stream"
}
Invoke-RestMethod -Uri "$($collectionUrl)$project/_apis/distributedtask/securefiles/$($secFileId)?ticket=$($secTicket)&download=true&api-version=5.0-preview.1" -Headers $headers -OutFile $filePath
I am using "$(Build.QueuedById)" to get the user id in build tasks,
but honestly I don't think it matters what string you use there.
If you don't have the Accept header, you'll get JSON metadata back for
the file you're attempting to download.
I decided to slap a parameter in my yaml file that resides in my repo. So in my template, I use the task as such
- ${{ if parameters.keytab[env.key] }}:
- task: DownloadSecureFile#1
name: kafkakeytab
displayName: 'Download kafka keytab'
inputs:
secureFile: ${{parameters.keytab[env.key]}}
retryCount: 5
and in my YAML file, I just reference a parameter as such:
keytab:
DEV: bobbobob.keytab
UAT: blablauat.keytab
This means that if I don't pass this parameter, the pipeline will not include the task in the pipeline, which is what I want. This way, I didn't have to create my own Powershell task to try to achieve this!
After I clone an instance from an image, a few manual steps need to be carried out to get the report server working correctly. Among them is the deletion of all encrypted data, including symmetric key instances on the report server database.
This step requires me to RDP to the server in question, open the Reporting Services Configuration Manager and delete the encrypted data manually.
Without carrying out this step, I get the following error when I try to load up the report server interface of the new server:
The report server cannot open a connection to the report server
database. A connection to the database is required for all requests
and processing. (rsReportServerDatabaseUnavailable)
I'm trying to automate this step, so that it runs as part of a PowerShell script to remotely delete the encrypted data.
I am aware of 'rskeymgmt -d' but this prompts the user for input when run and has no force flag available to circumvent this additional input, rendering it unusable for running remotely as far as I can see:
C:\>rskeymgmt -d
All data will be lost. Are you sure you want to delete all encrypted data from
the report server database (Y/N)?
I've found a solutions to solving this problem. Calling RSKeyMgmt -d through a remote PowerShell session and piping the Y string to the call passes the parameter that RSKeyMgmt prompts the user for. This method is based on Som DT's post on backing up report server encryption keys
I've attached the full script I am using as part of my environment cloning process.
<#
.SYNOPSIS
Deletes encrypted content from a report server
.PARAMETER MachineName
The name of the machine that the report server resides on
.EXAMPLE
./Delete-EncryptedSsrsContent.ps1 -MachineName 'dev41pc123'
Deletes encrypted content from the 'dev41pc123' report server
#>
param([string]$MachineName = $(throw "MachineName parameter required, for command line usage of this script, type: 'get-help ./Delete-EncryptedSSRS.ps1 -examples'"))
trap [SystemException]{Write-Output "`n`nERROR: $_";exit 1}
Set-StrictMode -Version Latest
try
{
Write-Output "`nCreating remote session to the '$machineName' machine now..."
$session = New-PSsession -Computername $machineName
Invoke-Command -Session $Session -ScriptBlock {"Y" | RSKeyMgmt -d}
}
catch
{
Write-Output "`n`nERROR: $_"
}
finally
{
if ($Session)
{
Remove-PSSession $Session
}
}
This is a generalisation of ShaneC's solution, to support deletion of encrypted content on non default instances:
<#
.SYNOPSIS
Deletes encrypted content from a report server
.PARAMETER MachineName
The name of the machine that the report server resides on
.EXAMPLE
./Delete-EncryptedSsrsContent.ps1 -MachineName 'dev41pc123'
Deletes encrypted content from the default instance (MSSQLSERVER) of the 'dev41pc123' report server
.EXAMPLE
./Delete-EncryptedSsrsContent.ps1 -MachineName 'dev41pc123' -InstanceName 'NonDefault'
Deletes encrypted content from the specified non-default instance (e.g. NonDefault) of the 'dev41pc123' report server
#>
param(
[Parameter(Mandatory=$true)]
[string]$MachineName = $(throw "MachineName parameter required, for command line usage of this script, type: 'get-help ./Delete-EncryptedSSRS.ps1 -examples'"),
[Parameter(Mandatory=$false)]
[string]$InstanceName)
trap [SystemException]{Write-Output "`n`nERROR: $_";exit 1}
Set-StrictMode -Version Latest
try
{
Write-Output "`nCreating remote session to the '$MachineName' machine now..."
$session = New-PSsession -Computername $MachineName
if ([string]::IsNullOrEmpty($instanceName))
{
Invoke-Command -Session $Session -ScriptBlock {"Y" | RSKeyMgmt.exe -d}
}
else
{
Write-Output "`nDeleting all encrypted content from the $InstanceName instance on the $MachineName machine now...`n"
$command = """Y""| RSKeyMgmt.exe -d -i""" + $InstanceName + """"
Invoke-Command -Session $Session -ScriptBlock { Invoke-Expression $args[0] } -ArgumentList $command
Write-Output "`n"
}
}
catch
{
Write-Output "`n`nERROR: $_"
}
finally
{
if ($Session)
{
Remove-PSSession $Session
}
}
When running
tf proxy /configure
from the commandline, tfs sets the proxy settings based on AD sites set up within the TFS server.
If this is done before Visual Studio is run for the first time, it appears that VS takes these values by default. However, if you rerun the command, Visual Studio does not update with the new values.
I'd like to give my developers a batch file that configures their proxy settings for the office they are currently in. So that they could easily set up the values when they are in different offices, or if they are working remotely.
I've written the below:
#echo off
set TFDIR=%vs120comnTools%..\IDE
set Path=%Path%;%TFDIR%
tf proxy /enabled:false
echo[
echo Configuring Proxy
tf proxy /configure /collection:[MyUrl]
PAUSE
If I run this, it does appear to load the correct settings, and the tf proxy command returns the appropriate values. However, when I open Visual Studio and go to Tools >> Options >> Source Control >> Team Foundation Server, the proxy settings remain at the last values I set manually.
Is there a way to make the batch file update the visual studio settings.
Update
Thanks to Vickys answer below, I've realised that the problem isn't quite what I thought it was.
When I am running tf.exe, it is correctly updating the TFS Proxy settings for the installation of Visual Studio that is hosting the exe (i.e. the one I'm using the path to). However, it doesn't update the proxy configurations of the other installed visual studio installations.
Since I don't want to have to run the command for all installed versions, I'm looking for a way to make it update them all from a single command.
After you run the batch file, you need to restart Visual Studio.
Thanks to Vickys answer, I have been able to determine the tf.exe will update the TFS settings for the IDE that it is hosted by. e.g. if you are running /14.0/Common7/IDE/tf.exe, it will update the settings for Visual Studio 2015. It won't, however, update 2013, 2012, etc.
I have written the below powershell script that will update the other instances. You will need to update the [MyUrl] Value with the appropriate url for your TFS collection
#TODO: Replace [MyUrl] With the collection Url
#Add New Versions to this list when new versions of VS are released
$VsVersionsToDisable = "10.0", "11.0", "12.0", "14.0"
[System.Collections.ArrayList]$VsVersions = $VsVersionsToDisable
[String]$VsProxyVersionToUse = ""
#Find the Highest installed VS Version, and use it for the TFS.exe Command.
foreach ($version in $VsVersions | Sort-Object -Descending)
{
$keyPath = "HKCU:\Software\Microsoft\VisualStudio\$version`_Config"
If (Test-Path $keyPath)
{
$aliasPath = Get-ItemProperty -Path $keyPath | Select-Object `
-ExpandProperty InstallDir
$proxyPath = Join-Path $aliasPath "tf.exe"
set-alias proxyTF $proxyPath
#Remove the VS Version we're using from the array
#the command will auto-set this value, so we don't need to manually set it.
$VsVersions.Remove($version)
$VsProxyVersionToUse = $version
break
}
}
#Gets the last Check time from the Auto-Configuration, to update the other
#versions
function Get-ProxyLastCheckTime()
{
return Get-ItemProperty `
"HKCU:\Software\Microsoft\VisualStudio\$VsProxyVersionToUse\TeamFoundation\SourceControl\Proxy" `
| Select-Object -ExpandProperty LastCheckTime
}
#For each installed version, updates the proxy settings.
function Set-VSIDEConfig
(
[String]
[Parameter(Mandatory=$true)]
$proxyUrl
)
{
$lastCheckTime = Get-ProxyLastCheckTime
foreach ($version in $VsVersions)
{
Push-Location
$regPath = "HKCU:\Software\Microsoft\VisualStudio\$version\TeamFoundation\SourceControl\Proxy"
if (Test-Path $regPath)
{
Write-Output "Updating Proxy IDE Settings for VS $version"
Set-Location $regPath
Set-ItemProperty . Enabled $true
Set-ItemProperty . Url $proxyUrl
Set-ItemProperty . AutoConfigured $true
Set-ItemProperty . LastCheckTime $lastCheckTime
Set-ItemProperty . LastConfigureTime $lastCheckTime
}
Pop-Location
}
}
#Disables the Current proxy Settings.
function Disable-VSIDEConfig()
{
foreach ($version in $VsVersionsToDisable)
{
Push-Location
$regPath = "HKCU:\Software\Microsoft\VisualStudio\$version\TeamFoundation\SourceControl\Proxy"
if (Test-Path $regPath)
{
Write-Output "Disabling Proxy IDE Settings for VS $version"
Set-Location $regPath
Set-ItemProperty . Enabled $false
}
Pop-Location
}
Write-Output ""
}
#Process the response from the Proxy command.
function Process-ProxyResult
(
[String[]]
[Parameter(Mandatory=$true)]
$result
)
{
$resultUrl = $result | Select -Last 1
if ($resultUrl -match "Successfully configured proxy (?<content>.*)\.")
{
$url = $matches["content"].Trim()
#Update the IDE Settings with the new proxy
Set-VSIDEConfig $url
}
Write-Output ""
}
#Run the TFS Proxy Setup.
function Set-TFSProxy()
{
#First, Disable the proxy settings
proxyTF proxy /enabled:$false
Disable-VSIDEConfig
Write-Output "Getting Proxy data from Team02"
#TODO: Replace [MyUrl] With the collection Url
$output = proxyTF proxy /configure /collection:[MyUrl] 2>&1
Write-Output $output
Write-Output ""
Process-ProxyResult $output
}
#Run it by default.
Set-TFSProxy
I was a bit remiss to find that Octopus, as amazing as it is, doesn't do anything cute or clever about shutting down your web app before it is upgraded.
In our solution we have two web apps (a website and a separate API web app) that rely on the same database, so while one is being upgraded the other is still live and there is potential that web or API requests are still being serviced while the database is being upgraded.
Not clean!
Clean would be for Octopus to shut down the web apps, wait until they are shut-down and then go ahead with the upgrade, bring the app pools back online once complete.
How can that be achieved?
Selfie-answer!
It is easy to make Octopus-deploy take a little extra care with your deployments, all you need is a couple of extra Execute-Powershell steps in your deployment routine.
Add a new first step to stop the app pool:
# Settings
#---------------
$appPoolName = "PushpayApi" # Or we could set this from an Octopus environment setting.
# Installation
#---------------
Import-Module WebAdministration
# see http://technet.microsoft.com/en-us/library/ee790588.aspx
cd IIS:\
if ( (Get-WebAppPoolState -Name $appPoolName).Value -eq "Stopped" )
{
Write-Host "AppPool already stopped: " + $appPoolName
}
Write-Host "Shutting down the AppPool: " + $appPoolName
Write-Host (Get-WebAppPoolState $appPoolName).Value
# Signal to stop.
Stop-WebAppPool -Name $appPoolName
do
{
Write-Host (Get-WebAppPoolState $appPoolName).Value
Start-Sleep -Seconds 1
}
until ( (Get-WebAppPoolState -Name $appPoolName).Value -eq "Stopped" )
# Wait for the apppool to shut down.
And then add another step at the end to restart the app pool:
# Settings
#---------------
$appPoolName = "PushpayApi"
# Installation
#---------------
Import-Module WebAdministration
# see http://technet.microsoft.com/en-us/library/ee790588.aspx
cd IIS:\
if ( (Get-WebAppPoolState -Name $appPoolName).Value -eq "Started" )
{
Write-Host "AppPool already started: " + $appPoolName
}
Write-Host "Starting the AppPool: " + $appPoolName
Write-Host (Get-WebAppPoolState $appPoolName).Value
# To restart the app pool ...
Start-WebAppPool -Name $appPoolName
Get-WebAppPoolState -Name $appPoolName
The approach we took was to deploy an _app_offline.htm (App Offline) file with the application. That way we get a nice message explaining why the site is down.
Then when it is time for deployment we use Mircrosofts Webdeploy to rename the it to app_offline.htm. We put the code for the rename in a powershell script that runs as the first step of our Octopus Deployment.
write-host "Website: $WebSiteName"
# Take Website Offline
$path = "$WebDeployPath";
$path
$verb = "-verb:sync";
$verb
# Take root Website offline
$src = "-source:contentPath=```"$WebSiteName/_app_offline.htm```"";
$src
$dest = "-dest:contentPath=```"$WebSiteName/app_offline.htm```"";
$dest
Invoke-Expression "&'$path' $verb $src $dest";
# Take Sub Website 1 offline
$src = "-source:contentPath=```"$WebSiteName/WebApp1/_app_offline.htm```"";
$dest = "-dest:contentPath=```"$WebSiteName/WebApp1/app_offline.htm```"";
Invoke-Expression "&'$path' $verb $src $dest";
$WebSiteName is usually "Default Web Site". Also note that the ` are not single quotes but actually the backtick character (usually found with the tilda on your keyboard).
Now if octopus is deploying your web site to a new location, your web site will come back online automatically. If you don't want that, you can deploy the new website with the app_offline file allready in place. Then you can use the following script to remove it.
write-host $WebSiteName
# & "c:\Program Files (x86)\IIS\Microsoft Web Deploy V2\msdeploy.exe" -verb:delete -dest:contentPath="$WebSiteName/app_offline.htm"
# those arn't QUOTES!!!!, they are the back accent thing.
write-host "Website: $WebSiteName"
# Put Web app Online.
$path = "$WebDeployPath";
$path
$verb = "-verb:delete";
$verb
$dest = "-dest:contentPath=```"$WebSiteName/app_offline.htm```"";
$dest
Invoke-Expression "&'$path' $verb $dest";
# Put Sub Website Online
$dest = "-dest:contentPath=```"$WebSiteName/WebApp1/app_offline.htm```"";
Invoke-Expression "&'$path' $verb $dest";
Stopping apppool and/or setting App_Offline file is not enough for me. Both didn't give proper explanation to clients why site is down. Especially App_Offline. I need to clean up bin folder and this causes YSOD (http://blog.kurtschindler.net/more-app_offline-htm-woes/).
My solution:
First task redirects deployed site to different folder containing only index.html with proper message. Last task brings back original folder.
A better solution would be to use a network load balancer such as the f5 LTM. You can set up multiple servers to receive traffic for your site and then, when you are deploying, you can just disable the one node in the NLB so all the other traffic goes to the other machine.
I like the f5 because it is very scriptable. When we deploy to our websites we take no outage whatsoever. all traffic to the site is just pointed to the server that is not currently being upgraded.
There are caveats:
You must script the downing disable of the pool member in the NLM so that it works with your site. If your site requires sessions (such as depending on session state or shared objects) then you have to bleed the traffic from the NLB nodes. in f5 you can just disable them and then watch for the connection count to go to zero (also scriptable).
You must enforce a policy with your deveopers / dbas that states that all database changes MUST NOT cause degradation or failure in the existing code. This means that you have to be very careful with the databases and configurations. That way you can do your database updates before you even start deploying to the first pool of your website.