You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Azure CI/CD流水线运行Test Containers时连接失败求助

问题:Azure Pipelines中Test Containers连接失败(本地正常)

当前Azure Pipelines配置

trigger:
- master

pool:
  vmImage: ubuntu-latest

variables:
  buildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
  displayName: 'dotnet build $(buildConfiguration)'
  inputs:
    command: 'build'
    projects: '**/BookyWooks.sln' 
    arguments: '--configuration $(buildConfiguration)'
    
# Add step to run tests
- task: DotNetCoreCLI@2
  displayName: 'dotnet test'
  inputs:
    command: 'test'
    projects: '**/*Tests.csproj'  # Specify test project(s) or wildcard pattern
    arguments: '--configuration $(buildConfiguration)'  

问题现象

本地运行测试可正常连接SQL Server和RabbitMQ容器,但在Azure Pipelines流水线中执行测试时出现连接失败错误。

Test Containers配置代码

public class OrderApiApplicationFactory<TEntryPoint> : WebApplicationFactory<Program>, IAsyncLifetime where TEntryPoint : Program
{
    private const string Database = "master";
    private const string Username = "sa";
    private const string Password = "yourStrong(!)Password";
    private string ConnectionString;
    private const ushort MsSqlPort = 1433;
    private const ushort RabbitMqPort = 5672; // RabbitMQ default port
    private readonly IContainer _mssqlContainer;
    private readonly IContainer _rabbitMqContainer;

    public HttpClient HttpClient { get; private set; } = default!;

    private const string RabbitMqUsername = "guest";
    private const string RabbitMqPassword = "guest";

    public OrderApiApplicationFactory()
    {
        _mssqlContainer = new ContainerBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithPortBinding(MsSqlPort, true)
            .WithEnvironment("ACCEPT_EULA", "Y") 
            .WithEnvironment("SQLCMDUSER", Username)
            .WithEnvironment("SQLCMDPASSWORD", Password)
            .WithEnvironment("MSSQL_SA_PASSWORD", Password)
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(MsSqlPort))
            .Build();

        _rabbitMqContainer = new ContainerBuilder()
            .WithImage("rabbitmq:3-management-alpine") // Use rabbitmq:management image
            .WithPortBinding(RabbitMqPort, 5672)
            .WithEnvironment("RABBITMQ_DEFAULT_USER", RabbitMqUsername)
            .WithEnvironment("RABBITMQ_DEFAULT_PASS", RabbitMqPassword)
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(RabbitMqPort)) // Adding wait strategy
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _mssqlContainer.StartAsync();
        await _rabbitMqContainer.StartAsync();

        var host = _mssqlContainer.Hostname;
        var port = _mssqlContainer.GetMappedPublicPort(MsSqlPort);

        ConnectionString = GetDatabaseConnectionString();

        HttpClient = CreateClient();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            RemoveDbContextOptions<BookyWooksOrderDbContext>(services);
            RemoveDbContextOptions<IntegrationEventLogDbContext>(services);

            AddDbContext<BookyWooksOrderDbContext>(services);
            AddDbContext<IntegrationEventLogDbContext>(services);
        });
    }

    private void RemoveDbContextOptions<T>(IServiceCollection services) where T : DbContext
    {
        var descriptor = services.SingleOrDefault(
            d => d.ServiceType == typeof(DbContextOptions<T>));

        if (descriptor != null)
            services.Remove(descriptor);
    }

    private void AddDbContext<T>(IServiceCollection services) where T : DbContext
    {
        services.AddDbContext<T>(options =>
        {
            options.UseSqlServer(ConnectionString,
                builder => builder.MigrationsAssembly(typeof(T).Assembly.FullName));
        });
    }
    private string GetDatabaseConnectionString()
    {
        return $"Server={_mssqlContainer.Hostname},{_mssqlContainer.GetMappedPublicPort(MsSqlPort)};Database={Database};User Id={Username};Password={Password};TrustServerCertificate=True";
    }

    public new async Task DisposeAsync()
    {
        await _mssqlContainer.DisposeAsync();
        await _rabbitMqContainer.DisposeAsync();
    }
}

排查与解决思路

1. 修正RabbitMQ端口绑定逻辑

当前RabbitMQ使用固定端口映射WithPortBinding(RabbitMqPort, 5672),在Azure Pipelines环境中可能出现端口冲突。改为随机端口映射,和SQL Server保持一致:

_rabbitMqContainer = new ContainerBuilder()
    .WithImage("rabbitmq:3-management-alpine")
    .WithPortBinding(RabbitMqPort, true) // 随机映射端口
    .WithEnvironment("RABBITMQ_DEFAULT_USER", RabbitMqUsername)
    .WithEnvironment("RABBITMQ_DEFAULT_PASS", RabbitMqPassword)
    .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(RabbitMqPort))
    .Build();

同时确保RabbitMQ的连接配置使用动态获取的端口:_rabbitMqContainer.GetMappedPublicPort(RabbitMqPort)。

2. 增强容器就绪等待策略

仅检查端口可用性不足以确保服务完全就绪,需添加更精准的等待逻辑:

  • SQL Server:等待SQL命令执行成功,确认服务可接受连接:
.WithWaitStrategy(Wait.ForUnixContainer()
    .UntilPortIsAvailable(MsSqlPort)
    .UntilCommandIsCompleted("sqlcmd -U sa -P yourStrong(!)Password -Q 'SELECT 1'"))
  • RabbitMQ:通过管理API检查服务健康状态:
.WithWaitStrategy(Wait.ForUnixContainer()
    .UntilPortIsAvailable(RabbitMqPort)
    .UntilHttpRequestIsSucceeded(r => r
        .ForPath("/api/health")
        .ForPort(15672) // RabbitMQ管理端口
        .WithBasicAuth(RabbitMqUsername, RabbitMqPassword)))

3. 调整连接字符串的主机名

在Docker-in-Docker(DIND)环境中,容器的Hostname可能无法被Agent进程访问,改用localhost配合映射端口:

private string GetDatabaseConnectionString()
{
    return $"Server=localhost,{_mssqlContainer.GetMappedPublicPort(MsSqlPort)};Database={Database};User Id={Username};Password={Password};TrustServerCertificate=True";
}

4. 优化流水线配置

  • 添加Docker状态检查步骤,确保Agent环境中Docker服务正常:
- script: |
    docker info
    docker version
  displayName: 'Verify Docker environment'
  • 测试步骤添加详细日志输出,便于定位错误:
- task: DotNetCoreCLI@2
  displayName: 'dotnet test'
  inputs:
    command: 'test'
    projects: '**/*Tests.csproj'
    arguments: '--configuration $(buildConfiguration) --verbosity detailed'

5. 检查Docker权限与版本

确保Test Containers使用最新版本(避免DIND环境兼容性问题),同时在流水线中添加权限修复步骤(若需要):

- script: sudo chmod 666 /var/run/docker.sock
  displayName: 'Fix Docker socket permissions'

内容的提问来源于stack exchange,提问作者Ports

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 07:49:51