Skip to content

Commit

Permalink
feat: add support for deploying to existing windows Beanstalk environ…
Browse files Browse the repository at this point in the history
…ments
  • Loading branch information
philasmar committed Sep 28, 2022
1 parent 482f60a commit ce3761a
Show file tree
Hide file tree
Showing 24 changed files with 1,018 additions and 145 deletions.
2 changes: 1 addition & 1 deletion src/AWS.Deploy.CLI/Commands/DeployCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ private async Task<Recommendation> GetSelectedRecommendationFromPreviousDeployme
}
else
{
previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication);
previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication, selectedRecommendation);
}

await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, deployedApplication.Name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ public async Task<IActionResult> SetDeploymentTarget(string sessionId, [FromBody
}
else
{
previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment);
previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment, state.SelectedRecommendation);
}

state.SelectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(state.SelectedRecommendation, previousSettings);
Expand Down
22 changes: 22 additions & 0 deletions src/AWS.Deploy.Constants/ElasticBeanstalk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ internal static class ElasticBeanstalk
public const string HealthCheckURLOptionNameSpace = "aws:elasticbeanstalk:application";
public const string HealthCheckURLOptionName = "Application Healthcheck URL";

public const string LinuxPlatformType = ".NET Core";
public const string WindowsPlatformType = "Windows Server";

public const string IISAppPathOptionId = "IISAppPath";
public const string IISWebSiteOptionId = "IISWebSite";

public const string WindowsManifestName = "aws-windows-deployment-manifest.json";

/// <summary>
/// This list stores a named tuple of OptionSettingId, OptionSettingNameSpace and OptionSettingName.
/// <para>OptionSettingId refers to the Id property for an option setting item in the recipe file.</para>
Expand All @@ -35,5 +43,19 @@ internal static class ElasticBeanstalk
new (ProxyOptionId, ProxyOptionNameSpace, ProxyOptionName),
new (HealthCheckURLOptionId, HealthCheckURLOptionNameSpace, HealthCheckURLOptionName)
};

/// <summary>
/// This is the list of option settings available for Windows Beanstalk deployments.
/// This list stores a named tuple of OptionSettingId, OptionSettingNameSpace and OptionSettingName.
/// <para>OptionSettingId refers to the Id property for an option setting item in the recipe file.</para>
/// <para>OptionSettingNameSpace and OptionSettingName provide a way to configure the environments metadata and update its behaviour.</para>
/// <para>A comprehensive list of all configurable settings can be found <see href="https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/beanstalk-environment-configuration-advanced.html">here</see></para>
/// </summary>
public static List<(string OptionSettingId, string OptionSettingNameSpace, string OptionSettingName)> WindowsOptionSettingQueryList = new()
{
new (EnhancedHealthReportingOptionId, EnhancedHealthReportingOptionNameSpace, EnhancedHealthReportingOptionName),
new (XRayTracingOptionId, XRayTracingOptionNameSpace, XRayTracingOptionName),
new (HealthCheckURLOptionId, HealthCheckURLOptionNameSpace, HealthCheckURLOptionName)
};
}
}
1 change: 1 addition & 0 deletions src/AWS.Deploy.Constants/RecipeIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal static class RecipeIdentifier
{
// Recipe IDs
public const string EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkEnvironment";
public const string EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkWindowsEnvironment";
public const string PUSH_TO_ECR_RECIPE_ID = "PushContainerImageEcr";

// Replacement Tokens
Expand Down
6 changes: 3 additions & 3 deletions src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,11 +530,11 @@ public async Task<List<PlatformSummary>> GetElasticBeanstalkPlatformArns(params
var allPlatformSummaries = new List<PlatformSummary>();
if (platformTypes.Contains(BeanstalkPlatformType.Linux))
{
allPlatformSummaries.AddRange(await fetchPlatforms(".NET Core"));
allPlatformSummaries.AddRange(await fetchPlatforms(Constants.ElasticBeanstalk.LinuxPlatformType));
}
else if (platformTypes.Contains(BeanstalkPlatformType.Windows))
if (platformTypes.Contains(BeanstalkPlatformType.Windows))
{
var windowsPlatforms = await fetchPlatforms("Windows Server");
var windowsPlatforms = await fetchPlatforms(Constants.ElasticBeanstalk.WindowsPlatformType);
SortElasticBeanstalkWindowsPlatforms(windowsPlatforms);
allPlatformSummaries.AddRange(windowsPlatforms);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public async Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloud

orchestrator._interactiveService.LogSectionStart($"Creating application version", "Uploading deployment bundle to S3 and create an Elastic Beanstalk application version");

// This step is only required for Elastic Beanstalk Windows deployments since a manifest file needs to be created for that deployment.
if (recommendation.Recipe.Id.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID))
{
elasticBeanstalkHandler.SetupWindowsDeploymentManifest(recommendation, deploymentPackage);
}

var versionLabel = $"v-{DateTime.Now.Ticks}";
var s3location = await elasticBeanstalkHandler.CreateApplicationStorageLocationAsync(applicationName, versionLabel, deploymentPackage);
await s3Handler.UploadToS3Async(s3location.S3Bucket, s3location.S3Key, deploymentPackage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,77 @@

using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Amazon.ElasticBeanstalk;
using Amazon.ElasticBeanstalk.Model;
using AWS.Deploy.Common;
using AWS.Deploy.Common.IO;
using AWS.Deploy.Common.Recipes;
using System.Text.Json.Serialization;

namespace AWS.Deploy.Orchestration.ServiceHandlers
{
public interface IElasticBeanstalkHandler
{
/// <summary>
/// Deployments to Windows Elastic Beanstalk envvironments require a manifest file to be included with the binaries.
/// This method creates the manifest file if it doesn't exist, or it creates a new one.
/// The two main settings that are updated are IIS Website and IIS App Path.
/// </summary>
void SetupWindowsDeploymentManifest(Recommendation recommendation, string dotnetZipFilePath);
Task<S3Location> CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage);
Task<CreateApplicationVersionResponse> CreateApplicationVersionAsync(string applicationName, string versionLabel, S3Location sourceBundle);
Task<bool> UpdateEnvironmentAsync(string applicationName, string environmentName, string versionLabel, List<ConfigurationOptionSetting> optionSettings);
List<ConfigurationOptionSetting> GetEnvironmentConfigurationSettings(Recommendation recommendation);
}

/// <summary>
/// This class represents the structure of the Windows manifest file to be included with Windows Elastic Beanstalk deployments.
/// </summary>
public class ElasticBeanstalkWindowsManifest
{
[JsonPropertyName("manifestVersion")]
public int ManifestVersion { get; set; } = 1;

[JsonPropertyName("deployments")]
public ManifestDeployments Deployments { get; set; } = new();

public class ManifestDeployments
{

[JsonPropertyName("aspNetCoreWeb")]
public List<AspNetCoreWebDeployments> AspNetCoreWeb { get; set; } = new();

public class AspNetCoreWebDeployments
{

[JsonPropertyName("name")]
public string Name { get; set; } = "app";


[JsonPropertyName("parameters")]
public AspNetCoreWebParameters Parameters { get; set; } = new();

public class AspNetCoreWebParameters
{
[JsonPropertyName("appBundle")]
public string AppBundle { get; set; } = ".";

[JsonPropertyName("iisPath")]
public string IISPath { get; set; } = "/";

[JsonPropertyName("iisWebSite")]
public string IISWebSite { get; set; } = "Default Web Site";
}
}
}
}

public class AWSElasticBeanstalkHandler : IElasticBeanstalkHandler
{
private readonly IAWSClientFactory _awsClientFactory;
Expand All @@ -37,6 +89,121 @@ public AWSElasticBeanstalkHandler(IAWSClientFactory awsClientFactory, IOrchestra
_optionSettingHandler = optionSettingHandler;
}

private T GetOrCreateNode<T>(object? json) where T : new()
{
try
{
return JsonSerializer.Deserialize<T>(json?.ToString() ?? string.Empty);
}
catch
{
return new T();
}
}

/// <summary>
/// Deployments to Windows Elastic Beanstalk envvironments require a manifest file to be included with the binaries.
/// This method creates the manifest file if it doesn't exist, or it creates a new one.
/// The two main settings that are updated are IIS Website and IIS App Path.
/// </summary>
public void SetupWindowsDeploymentManifest(Recommendation recommendation, string dotnetZipFilePath)
{
var iisWebSiteOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISWebSiteOptionId);
var iisAppPathOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISAppPathOptionId);

var iisWebSiteValue = _optionSettingHandler.GetOptionSettingValue<string>(recommendation, iisWebSiteOptionSetting);
var iisAppPathValue = _optionSettingHandler.GetOptionSettingValue<string>(recommendation, iisAppPathOptionSetting);

var iisWebSite = !string.IsNullOrEmpty(iisWebSiteValue) ? iisWebSiteValue : "Default Web Site";
var iisAppPath = !string.IsNullOrEmpty(iisAppPathValue) ? iisAppPathValue : "/";

var newManifestFile = new ElasticBeanstalkWindowsManifest();
newManifestFile.Deployments.AspNetCoreWeb.Add(new ElasticBeanstalkWindowsManifest.ManifestDeployments.AspNetCoreWebDeployments
{
Parameters = new ElasticBeanstalkWindowsManifest.ManifestDeployments.AspNetCoreWebDeployments.AspNetCoreWebParameters
{
IISPath = iisAppPath,
IISWebSite = iisWebSite
}
});

using (var zipArchive = ZipFile.Open(dotnetZipFilePath, ZipArchiveMode.Update))
{
var zipEntry = zipArchive.GetEntry(Constants.ElasticBeanstalk.WindowsManifestName);
var serializedManifest = JsonSerializer.Serialize(new Dictionary<string, object>());
if (zipEntry != null)
{
using (var streamReader = new StreamReader(zipEntry.Open()))
{
serializedManifest = streamReader.ReadToEnd();
}
}

var jsonDoc = GetOrCreateNode<Dictionary<string, object>>(serializedManifest);

if (!jsonDoc.ContainsKey("manifestVersion"))
{
jsonDoc["manifestVersion"] = newManifestFile.ManifestVersion;
}

if (jsonDoc.ContainsKey("deployments"))
{
var deploymentNode = GetOrCreateNode<Dictionary<string, object>>(jsonDoc["deployments"]);

if (deploymentNode.ContainsKey("aspNetCoreWeb"))
{
var aspNetCoreWebNode = GetOrCreateNode<List<object>>(deploymentNode["aspNetCoreWeb"]);
if (aspNetCoreWebNode.Count == 0)
{
aspNetCoreWebNode.Add(newManifestFile.Deployments.AspNetCoreWeb[0]);
}
else
{
// We only need 1 entry in the 'aspNetCoreWeb' node that defines the parameters we are interested in. Typically, only 1 entry exists.
var aspNetCoreWebEntry = GetOrCreateNode<Dictionary<string, object>>(JsonSerializer.Serialize(aspNetCoreWebNode[0]));

var nameValue = aspNetCoreWebEntry.ContainsKey("name") ? aspNetCoreWebEntry["name"].ToString() : string.Empty;
aspNetCoreWebEntry["name"] = !string.IsNullOrEmpty(nameValue) ? nameValue : newManifestFile.Deployments.AspNetCoreWeb[0].Name;

if (aspNetCoreWebEntry.ContainsKey("parameters"))
{
var parametersNode = GetOrCreateNode<Dictionary<string, object>>(aspNetCoreWebEntry["parameters"]);
parametersNode["appBundle"] = ".";
parametersNode["iisPath"] = iisAppPath;
parametersNode["iisWebSite"] = iisWebSite;

aspNetCoreWebEntry["parameters"] = parametersNode;
}
else
{
aspNetCoreWebEntry["parameters"] = newManifestFile.Deployments.AspNetCoreWeb[0].Parameters;
}
aspNetCoreWebNode[0] = aspNetCoreWebEntry;
}
deploymentNode["aspNetCoreWeb"] = aspNetCoreWebNode;
}
else
{
deploymentNode["aspNetCoreWeb"] = newManifestFile.Deployments.AspNetCoreWeb;
}

jsonDoc["deployments"] = deploymentNode;
}
else
{
jsonDoc["deployments"] = newManifestFile.Deployments;
}

using (var jsonStream = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(jsonDoc, new JsonSerializerOptions { WriteIndented = true })))
{
zipEntry ??= zipArchive.CreateEntry(Constants.ElasticBeanstalk.WindowsManifestName);
using var zipEntryStream = zipEntry.Open();
jsonStream.Position = 0;
jsonStream.CopyTo(zipEntryStream);
}
}
}

public async Task<S3Location> CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage)
{
string bucketName;
Expand Down Expand Up @@ -83,7 +250,20 @@ public List<ConfigurationOptionSetting> GetEnvironmentConfigurationSettings(Reco
{
var additionalSettings = new List<ConfigurationOptionSetting>();

foreach (var tuple in Constants.ElasticBeanstalk.OptionSettingQueryList)
List<(string OptionSettingId, string OptionSettingNameSpace, string OptionSettingName)> tupleList;
switch (recommendation.Recipe.Id)
{
case Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID:
tupleList = Constants.ElasticBeanstalk.OptionSettingQueryList;
break;
case Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID:
tupleList = Constants.ElasticBeanstalk.WindowsOptionSettingQueryList;
break;
default:
throw new InvalidOperationException($"The recipe '{recommendation.Recipe.Id}' is not supported.");
};

foreach (var tuple in tupleList)
{
var optionSetting = _optionSettingHandler.GetOptionSetting(recommendation, tuple.OptionSettingId);

Expand Down
Loading

0 comments on commit ce3761a

Please sign in to comment.