Triển khai tự động ASP.NET Core lên Linux: Đồng bộ tệp tin, quản lý máy chủ và khởi động nóng

Quy trình cập nhật một ứng dụng web thường xuyên có thể tốn thời gian, đặc biệt khi phải triển khai thủ công. Để tối ưu hóa công việc này, một công cụ nhỏ đã được phát triển nhằm tự động hóa việc triển khai các ứng dụng ASP.NET Core lên máy chủ Linux. Công cụ này tập trung vào việc chỉ tải lên các tệp tin đã thay đổi, tự động kiểm soát trạng thái máy chủ web và thực hiện "khởi động nóng" ứng dụng sau khi triển khai.

Để bắt đầu sử dụng, bạn cần cấu hình các thông số cần thiết trong tệp tin App.config. Đây là nơi bạn định nghĩa các thông tin kết nối và đường dẫn quan trọng cho quá trình triển khai:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="RemoteHostAddress" value="***.***.***.***"/>
    <add key="SSHUser" value="***"/>
    <add key="SSHPassword" value="*********"/>    
    <add key="DeploymentTargetDirectory" value="/var/www/mywebapp/"/>
    <add key="LocalPublishDirectory" value="D:\projects\MyWebApp\bin\Release\netcoreapp2.0\linux-x64\publish\"/>
    <add key="FileExclusionPatterns" value="System\..+;Microsoft\..+;.+\.so" />
    <add key="WebServerStopCommand" value="systemctl stop mywebapp.service"/>
    <add key="WebServerStartCommand" value="systemctl start mywebapp.service"/>
    <add key="ApplicationWarmupUrl" value="http://www.mywebapp.com"/>
    <add key="WarmupVerificationString" value="Trang chủ ứng dụng của tôi"/>
  </appSettings>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
    </startup>
</configuration>

Mô tả các tham số cấu hình:

  • RemoteHostAddress: Địa chỉ IP hoặc tên miền của máy chủ Linux từ xa.
  • SSHUser: Tên người dùng SSH (và SFTP) trên máy chủ từ xa.
  • SSHPassword: Mật khẩu SSH (và SFTP) của người dùng.
  • DeploymentTargetDirectory: Đường dẫn tuyệt đối trên máy chủ Linux nơi ứng dụng web sẽ được triển khai. Đường dẫn này nên kết thúc bằng dấu `/`.
  • LocalPublishDirectory: Đường dẫn đến thư mục chứa các tệp tin đã được biên dịch và publish của ứng dụng trên máy phát triển cục bộ. Ví dụ, sau khi chạy lệnh dotnet publish -c release -r linux-x64.
  • FileExclusionPatterns: Một danh sách các biểu thức chính quy (regex) phân tách bằng dấu chấm phẩy (;). Các tệp tin có tên khớp với bất kỳ mẫu nào trong danh sách này sẽ bị bỏ qua trong quá trình so sánh và tải lên, giúp giảm thiểu khối lượng công việc.
  • WebServerStopCommand: Lệnh SSH sẽ được thực thi để dừng máy chủ web hoặc dịch vụ ứng dụng của bạn trước khi tải lên các tệp mới.
  • WebServerStartCommand: Lệnh SSH sẽ được thực thi để khởi động lại máy chủ web hoặc dịch vụ ứng dụng sau khi quá trình tải lên hoàn tất.
  • ApplicationWarmupUrl: URL của ứng dụng web cần được yêu cầu sau khi triển khai. Điều này giúp khởi động ứng dụng và tải các tài nguyên cần thiết, tránh độ trễ ban đầu khi người dùng truy cập lần đầu.
  • WarmupVerificationString: Một chuỗi mong đợi xuất hiện trong nội dung phản hồi HTML từ ApplicationWarmupUrl. Nếu chuỗi này được tìm thấy, quá trình triển khai được coi là thành công.

Tổng quan về logic của công cụ

Công cụ thực hiện các bước sau một cách tuần tự:

  1. Thu thập danh sách các tệp tin cục bộ từ thư mục publish, loại trừ các tệp khớp với mẫu định sẵn.
  2. Kết nối đến máy chủ từ xa qua SFTP để so sánh các tệp tin cục bộ với các tệp trên máy chủ, dựa trên thời gian sửa đổi cuối cùng. Chỉ những tệp mới hoặc đã thay đổi sẽ được đưa vào danh sách chờ tải lên.
  3. Dừng máy chủ web trên máy chủ từ xa bằng lệnh SSH.
  4. Tải lên tất cả các tệp tin đã được xác định là cần cập nhật qua SFTP.
  5. Khởi động lại máy chủ web trên máy chủ từ xa bằng lệnh SSH.
  6. Thực hiện yêu cầu HTTP đến URL ứng dụng để khởi động nóng và xác minh triển khai thành công.

Các thành phần mã nguồn chính

Một số biến toàn cục được sử dụng để quản lý trạng thái và cấu hình:

static List<FileInfo> localFilesToProcess = new List<FileInfo>(); // Chứa các FileInfo của các tệp cục bộ
static List<string> exclusionRegexPatterns = new List<string>(); // Các mẫu regex để loại trừ tệp
static Dictionary<FileInfo, string> filesPendingUpload = new Dictionary<FileInfo, string>(); // Các tệp cần tải lên (FileInfo cục bộ, đường dẫn đích trên server)
static NameValueCollection appConfiguration = ConfigurationManager.AppSettings; // Đọc cấu hình từ App.config

Hàm Main điều phối toàn bộ quá trình triển khai:

static void Main(string[] args)
{
    Console.WriteLine("Bạn có muốn bắt đầu so sánh tệp tin (dựa trên thời gian sửa đổi)? (y/n):");
    if (Console.ReadLine()?.ToLower() != "y")
    {
        Console.WriteLine("Hủy bỏ. Nhấn phím bất kỳ để thoát.");
        Console.ReadKey();
        return;
    }
    Console.WriteLine("Bắt đầu so sánh tệp tin...");
    exclusionRegexPatterns.AddRange(appConfiguration["FileExclusionPatterns"].Split(';', StringSplitOptions.RemoveEmptyEntries));
    CollectLocalFiles(appConfiguration["LocalPublishDirectory"]); // Thu thập tệp cục bộ và áp dụng bộ lọc

    PerformSftpComparisonAndStaging(sftpClient => // So sánh tệp cục bộ với tệp trên server
    {
        if (filesPendingUpload.Count == 0)
        {
            Console.WriteLine("Không có tệp nào cần cập nhật. Nhấn phím bất kỳ để thoát.");
            Console.ReadKey();
            return;
        }

        Console.WriteLine($"Tìm thấy {filesPendingUpload.Count} tệp cần cập nhật.");
        Console.WriteLine("Bạn có muốn dừng máy chủ web và bắt đầu tải lên? (y/n):");
        if (Console.ReadLine()?.ToLower() != "y")
        {
            Console.WriteLine("Hủy bỏ. Nhấn phím bất kỳ để thoát.");
            Console.ReadKey();
            return;
        }

        ControlWebServerViaSsh(() => // Dừng/Khởi động máy chủ web
        {
            foreach (var entry in filesPendingUpload)
            {
                using (var fileStream = entry.Key.OpenRead())
                {
                    sftpClient.BufferSize = 6 * 1024; // Tăng buffer để tải lên hiệu quả hơn
                    sftpClient.UploadFile(fileStream, entry.Value, true); // Tải lên tệp
                }
                Console.WriteLine($"Đã tải lên: {entry.Value}");
            }
        });

        Thread.Sleep(1500); // Đợi một chút cho server xử lý
        Console.WriteLine("Đang yêu cầu URL ứng dụng để khởi động nóng...");
        var htmlContent = FetchUrlContent(appConfiguration["ApplicationWarmupUrl"]); // Yêu cầu URL của ứng dụng

        if (htmlContent.Contains(appConfiguration["WarmupVerificationString"])) // Kiểm tra chuỗi xác minh
        {
            Console.WriteLine("Triển khai thành công và ứng dụng đã được khởi động nóng! Nhấn phím bất kỳ để thoát.");
        }
        else
        {
            Console.WriteLine("Triển khai có thể đã thất bại hoặc xác minh không thành công. Vui lòng kiểm tra thủ công. Nhấn phím bất kỳ để thoát.");
        }
        Console.ReadKey();
    });
}

Hàm CollectLocalFiles thực hiện việc quét đệ quy các tệp tin trong thư mục cục bộ và áp dụng bộ lọc:

static void CollectLocalFiles(string currentDirectoryPath)
{
    var subDirectories = Directory.GetDirectories(currentDirectoryPath);
    foreach (var dir in subDirectories)
    {
        CollectLocalFiles(dir); // Đệ quy vào các thư mục con
    }

    var currentFiles = Directory.GetFiles(currentDirectoryPath);
    foreach (var filePath in currentFiles)
    {
        var currentFileInfo = new FileInfo(filePath);
        var isExcluded = false;
        foreach (var pattern in exclusionRegexPatterns)
        {
            if (Regex.IsMatch(currentFileInfo.Name, pattern))
            {
                isExcluded = true;
                break; // Nếu khớp với một mẫu, bỏ qua tệp này
            }
        }
        if (!isExcluded)
        {
            localFilesToProcess.Add(currentFileInfo);
        }
    }
}

Hàm PerformSftpComparisonAndStaging so sánh tệp cục bộ với tệp trên máy chủ từ xa và chuẩn bị danh sách tệp cần tải lên:

static void PerformSftpComparisonAndStaging(Action<SftpClient> postComparisonAction)
{
    using (var sftpClient = new SftpClient(appConfiguration["RemoteHostAddress"], appConfiguration["SSHUser"], appConfiguration["SSHPassword"]))
    {
        sftpClient.Connect();
        string clientBasePath = appConfiguration["LocalPublishDirectory"];
        string serverBasePath = appConfiguration["DeploymentTargetDirectory"];

        foreach (var localFileInfo in localFilesToProcess)
        {
            // Xử lý đường dẫn để có đường dẫn tương đối trên server
            var relativePath = localFileInfo.FullName.Substring(clientBasePath.Length);
            if (relativePath.StartsWith("\\") || relativePath.StartsWith("/"))
            {
                relativePath = relativePath.Substring(1);
            }
            relativePath = relativePath.Replace('\\', '/');

            var remoteFilePath = serverBasePath + relativePath;

            bool remoteFileExists = sftpClient.Exists(remoteFilePath);

            if (!remoteFileExists)
            {
                filesPendingUpload.Add(localFileInfo, remoteFilePath); // Tệp không tồn tại trên server, cần tải lên
                Console.WriteLine($"Chờ tải lên (mới): {relativePath}");
            }
            else
            {
                var remoteLastWriteTime = sftpClient.GetLastWriteTime(remoteFilePath);
                // So sánh thời gian sửa đổi: nếu tệp cục bộ mới hơn tệp trên server
                if (localFileInfo.LastWriteTime > remoteLastWriteTime)
                {
                    filesPendingUpload.Add(localFileInfo, remoteFilePath);
                    Console.WriteLine($"Chờ tải lên (cập nhật): {relativePath}");
                }
            }
        }                
        postComparisonAction(sftpClient); // Thực hiện hành động sau khi so sánh (tức là tải lên)
    }
}

Hàm ControlWebServerViaSsh quản lý việc dừng và khởi động máy chủ web bằng cách thực thi các lệnh SSH:

static void ControlWebServerViaSsh(Action actionDuringDowntime)
{
    using (var sshConnection = new SshClient(appConfiguration["RemoteHostAddress"], appConfiguration["SSHUser"], appConfiguration["SSHPassword"]))
    {
        sshConnection.Connect();
        
        using (var sshCommand = sshConnection.CreateCommand(appConfiguration["WebServerStopCommand"]))
        {
            sshCommand.Execute();
            Console.WriteLine("Đã gửi lệnh dừng máy chủ web.");
        }

        actionDuringDowntime(); // Thực hiện hành động tải lên trong thời gian máy chủ dừng

        using (var sshCommand = sshConnection.CreateCommand(appConfiguration["WebServerStartCommand"]))
        {
            sshCommand.Execute();
            Console.WriteLine("Đã gửi lệnh khởi động máy chủ web.");
        }
        sshConnection.Disconnect();
    }
}

Hàm FetchUrlContent gửi yêu cầu HTTP để lấy nội dung từ một URL, phục vụ cho việc khởi động nóng ứng dụng và xác minh:

static string FetchUrlContent(string url)
{
    using (HttpClient client = new HttpClient())
    {
        client.Timeout = TimeSpan.FromSeconds(20); // Đặt thời gian chờ 20 giây
        try
        {
            var response = client.GetAsync(url).Result; // Thực hiện yêu cầu đồng bộ
            response.EnsureSuccessStatusCode(); // Ném ngoại lệ nếu mã trạng thái không thành công
            return response.Content.ReadAsStringAsync().Result; // Đọc nội dung phản hồi
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Lỗi HTTP khi truy cập URL {url}: {ex.Message}");
            return string.Empty;
        }
        catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
        {
            Console.WriteLine($"Hết thời gian chờ khi truy cập URL {url}.");
            return string.Empty;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Lỗi không xác định khi truy cập URL {url}: {ex.Message}");
            return string.Empty;
        }
    }
}

Công cụ này sử dụng thư viện SSH.NET để thực hiện các thao tác SSH và SFTP.

Thẻ: ASP.NET Core Linux Deployment SSH.NET sftp Automated Deployment

Đăng vào ngày 11 tháng 11 lúc 07:36