前書き
現在使用されているCI / CDシステムはたくさんあります。誰もが特定の長所と短所を持っており、誰もがプロジェクトに最も適したものを選択します。この記事の目的は、.NET 5へのさらなる更新を目的として、廃止された.NET Frameworkを使用するWebプロジェクトの例を使用して、Nukeについて理解することです。プロジェクトはすでにFakeコレクターを使用していますが、更新および改良する必要があり、最終的に移行に至りました。ヌケに。
初期データ
.NET Framework 4.8、Razor Pages + JSファイルにコンパイルされたTypeScriptフロントエンドスクリプトに基づいてC#で記述されたWebプロジェクト。
使用してアプリケーションをビルドして公開フェイク4。
AWSでのホスティング(Amazon Webサービス)
設定:プロダクション、ステージング、デモ
ゴール
拡張性と柔軟なカスタマイズを提供しながら、ビルドシステムを更新する必要があります。また、Web.configファイルの構成が指定された環境用に構成されていることを確認する必要があります。
ビルドシステムのさまざまなオプションを検討しましたが、非常にシンプルで、実際にはパッケージを介して拡張可能なコンソールアプリケーションであるため、最終的にはNukeに選択されました。さらに、Nukeは非常に動的で、十分に文書化されています。プラスは、IDE(開発環境-Rider)用のプラグインの存在です。プロジェクトの言語的一貫性を確保し、新しい開発者のエントリーしきい値を下げたいという願望のため、私はFake5への切り替えを拒否しました。また、スクリプトはデバッグが困難です。Cake、Psakeもその「スクリプト」のためにそれを落としました。
準備
Nuke dotnet tool, build-. .
$ dotnet tool install Nuke.GlobalTool --global
nuke :setup
, wizard , , .
_build
boot shell- .
Build . - Target-. Logger. :
Logger.Info($"Starting build for {ApplicationForBuild} using {BuildEnvironment} environment");
. Build [Parameter]. .
Nuget-
,
[Parameter("Configuration to build - Default is 'Release'")]
readonly Configuration Configuration = Configuration.Release;
[Parameter(Name="application")]
readonly string ApplicationForBuild;
[Parameter(Name="environment")]
public readonly string BuildEnvironment;
. OnBuildInitialized, , , . NukeBuild On, (, / ).
protected override void OnBuildInitialized()
{
ConfigurationProvider = new ConfigurationProvider(ApplicationForBuild, BuildEnvironment, RootDirectory);
string configFilePath = $"./appsettings.json";
if (!File.Exists(configFilePath))
{
throw new FileNotFoundException($"Configuration file {configFilePath} is not found");
}
string configFileContent = File.ReadAllText(configFilePath);
if (string.IsNullOrEmpty(configFileContent))
{
throw new ArgumentNullException($"Config file {configFilePath} content is empty");
}
/* typescript */
ToolsConfiguration = JsonConvert.DeserializeObject<ToolsConfiguration>(configFileContent);
if (ToolsConfiguration == null || string.IsNullOrEmpty(ToolsConfiguration.TypeScriptCompilerFolder))
{
throw new ArgumentNullException($"Typescript compiler path is not defined");
}
base.OnBuildInitialized();
}
public class ApplicationConfig
{
public string ApplicationName { get; set; }
public string DeploymentGroup { get; set; }
/* Web.config */
public Dictionary<string, string> WebConfigReplacingParams { get; set; }
public ApplicationPathsConfig Paths { get; set; }
}
public class ConfigurationProvider
{
readonly string Name;
readonly string DeployEnvironment;
readonly AbsolutePath RootDirectory;
ApplicationConfig CurrentConfig;
public ConfigurationProvider(string name,
string deployEnvironment,
AbsolutePath rootDirectory)
{
RootDirectory = rootDirectory;
DeployEnvironment = deployEnvironment;
Name = name;
}
public ApplicationConfig GetConfigForApplication()
{
if (CurrentConfig != null) return CurrentConfig;
string configFilePath = $"./BuildConfigs/{Name}/{DeployEnvironment}.json";
if (!File.Exists(configFilePath))
{
throw new FileNotFoundException($"Configuration file {configFilePath} is not found");
}
string configFileContent = File.ReadAllText(configFilePath);
if (string.IsNullOrEmpty(configFileContent))
{
throw new ArgumentNullException($"Config file {configFilePath} content is empty");
}
CurrentConfig = JsonConvert.DeserializeObject<ApplicationConfig>(configFileContent);
CurrentConfig.Paths = new ApplicationPathsConfig(RootDirectory, Name, CurrentConfig.ApplicationName);
return CurrentConfig;
}
}
Nuget-
(Clean) , . : , , (RootDirectory) :
Target Restore => _ => _
.DependsOn(Clean)
.Executes(() =>
{
NuGetTasks.NuGetRestore(config =>
{
config = config
.SetProcessToolPath(RootDirectory / ".nuget" / "NuGet.exe")
.SetConfigFile(RootDirectory / ".nuget" / "NuGet.config")
.SetProcessWorkingDirectory(RootDirectory)
.SetOutputDirectory(RootDirectory / "packages");
return config;
});
});
. .NET-, TypeScript- JavaScript-.
Target Compile => _ => _
.DependsOn(Restore)
.Executes(() =>
{
AbsolutePath projectFile = ApplicationConfig.Paths.ProjectDirectory.GlobFiles("*.csproj").FirstOrDefault();
if (projectFile == null)
{
throw new ArgumentNullException($"Cannot found any projects in {ApplicationConfig.Paths.ProjectDirectory}");
}
MSBuild(config =>
{
config = config
.SetOutDir(ApplicationConfig.Paths.BinDirectory)
.SetConfiguration(Configuration) // : Debug/Release
.SetProperty("WebProjectOutputDir", ApplicationConfig.Paths.ApplicationOutputDirectory)
.SetProjectFile(projectFile)
.DisableRestore(); // ,
return config;
});
/* tsc . */
IProcess typeScriptProcess = ProcessTasks.StartProcess(@"node",$@"tsc -p {ApplicationConfig.Paths.ProjectDirectory}", ToolsConfiguration.TypeScriptCompilerFolder);
if (!typeScriptProcess.WaitForExit())
{
Logger.Error("Typescript build is failed");
throw new Exception("Typescript build is failed");
}
CopyDirectoryRecursively(ApplicationConfig.Paths.TypeScriptsSourceDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory, DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);
});
: .
Web.config . . json- .
CodeDeploy . AWS NuGet- AWSSDK: AWSSDK.Core, AWSSDK.S3, AWSSDK.CodeDeploy. AWS CodeDeploy. Build.
Target Publish => _ => _
.DependsOn(Compile)
.Executes(async () =>
{
PrepareApplicationForPublishing();
await PublishApplicationToAws();
});
void PrepareWebConfig(Dictionary<string, string> replaceParams)
{
if (replaceParams?.Any() != true) return;
Logger.Info($"Setup Web.config for environment {BuildEnvironment}");
AbsolutePath webConfigPath = ApplicationConfig.Paths.ApplicationOutputDirectory / "Web.config";
if (!FileExists(webConfigPath))
{
Logger.Error($"{webConfigPath} is not found");
throw new FileNotFoundException($"{webConfigPath} is not found");
}
XmlDocument webConfig = new XmlDocument();
webConfig.Load(webConfigPath);
XmlNode settings = webConfig.SelectSingleNode("configuration/appSettings");
if (settings == null)
{
Logger.Error("Node configuration/appSettings in the config is not found");
throw new ArgumentNullException(nameof(settings),"Node configuration/appSettings in the config is not found");
}
foreach (var newParam in replaceParams)
{
XmlNode nodeForChange = settings.SelectSingleNode($"add[@key='{newParam.Key}']");
((XmlElement) nodeForChange)?.SetAttribute("value", newParam.Value);
}
webConfig.Save(webConfigPath);
}
void PrepareApplicationForPublishing()
{
AbsolutePath specFilePath = ApplicationConfig.Paths.PublishDirectory / AppSpecFile;
AbsolutePath specFileTemplate = ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile;
PrepareWebConfig(ApplicationConfig.WebConfigReplacingParams);
DeleteFile(ApplicationConfig.Paths.ApplicationOutputDirectory);
CopyDirectoryRecursively(ApplicationConfig.Paths.ApplicationOutputDirectory, ApplicationConfig.Paths.PublishDirectory / DeployAppDirectory,
DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);
CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory,
DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);
CopyFile(ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile, ApplicationConfig.Paths.PublishDirectory / AppSpecFile, FileExistsPolicy.Overwrite);
CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.PublishDirectory / DeployScriptsDirectory,
DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);
Logger.Info($"Creating archive '{ApplicationConfig.Paths.ArchiveFilePath}'");
CompressionTasks.CompressZip(ApplicationConfig.Paths.PublishDirectory, ApplicationConfig.Paths.ArchiveFilePath);
}
async Task PublishApplicationToAws()
{
string s3bucketName = "";
IAwsCredentialsProvider awsCredentialsProvider = new AwsCredentialsProvider(null, null, "");
using S3FileManager fileManager = new S3FileManager(awsCredentialsProvider, RegionEndpoint.EUWest1);
using CodeDeployManager codeDeployManager = new CodeDeployManager(awsCredentialsProvider, RegionEndpoint.EUWest1);
Logger.Info($"AWS S3: upload artifacts to '{s3bucketName}'");
FileMetadata metadata = await fileManager.UploadZipFileToBucket(ApplicationConfig.Paths.ArchiveFilePath, s3bucketName);
Logger.Info(
$"AWS CodeDeploy: create deploy for '{ApplicationConfig.ApplicationName}' in group '{ApplicationConfig.DeploymentGroup}' with config '{DeploymentConfig}'");
CodeDeployResult deployResult =
await codeDeployManager.CreateDeployForRevision(ApplicationConfig.ApplicationName, metadata, ApplicationConfig.DeploymentGroup, DeploymentConfig);
StringBuilder resultBuilder = new StringBuilder(deployResult.Success ? "started successfully\n" : "not started\n");
resultBuilder = ProcessDeloymentResult(deployResult, resultBuilder);
Logger.Info($"AWS CodeDeploy: deployment has been {resultBuilder}");
DeleteFile(ApplicationConfig.Paths.ArchiveFilePath);
Directory.Delete(ApplicationConfig.Paths.ApplicationOutputDirectory, true);
string deploymentId = deployResult.DeploymentId;
DateTime startTime = DateTime.UtcNow;
/* */
do
{
if(DateTime.UtcNow - startTime > TimeSpan.FromMinutes(30)) break;
Thread.Sleep(3000);
deployResult = await codeDeployManager.GetDeploy(deploymentId);
Logger.Info($"Deployment proceed: {deployResult.DeploymentInfo.Status}");
}
while (deployResult.DeploymentInfo.Status == DeploymentStatus.InProgress
|| deployResult?.DeploymentInfo.Status == DeploymentStatus.Created
|| deployResult?.DeploymentInfo.Status == DeploymentStatus.Queued);
Logger.Info($"AWS CodeDeploy: deployment has been done");
}
, . , . . build .
いくつかのステージを別々のターゲットに分割し、個々のステージを無効にする機能を追加することでメソッドのコードの長さを短縮することで、コードを改善できます。しかし、この記事の目的は、Nukeコレクターを紹介し、実際の例で使用法を示すことです。