【问题标题】:Connecting to CosmosDB from Docker container从 Docker 容器连接到 CosmosDB
【发布时间】:2020-09-22 04:28:00
【问题描述】:

我正在尝试学习 Docker,并且正在慢慢构建解决方案。我创建了一个暴露了一个 Http 端点的 Azure Function App。我创建了 Docker 文件以在 linux 容器中构建和运行解决方案(图像:mcr.microsoft.com/azure-functions/dotnet:3.0,Debian 图像)。我在 Windows 机器上。

我在我的 Windows 机器上安装了 Azure CosmosDB 模拟器,并希望从在 linux 容器中运行的函数应用连接到它。

我将 cosmos 的连接字符串作为环境变量传递。

ARG COSMOS_CONNECTION_STRING="AccountEndpoint=https://host.docker.internal:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="

我正在尝试通过以下代码连接到模拟器:

    await new CosmosClient(Environment.GetEnvironmentVariable("AzureCosmosConnectionString", EnvironmentVariableTarget.Process)
        .GetContainer("db_name", "container_name")
        .UpsertItemAsync<Dto>(dto)
        .ConfigureAwait(false);

当我这样做时,我收到以下错误(我假设前几行是最相关的,但如果我错了,我会包括其余的):

---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
   at System.Net.Security.SslStream.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo exception)
   at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.PartialFrameCallback(AsyncProtocolRequest asyncRequest)
--- End of stack trace from previous location where exception was thrown ---
   at System.Net.Security.SslStream.ThrowIfExceptional()
   at System.Net.Security.SslStream.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
   at System.Net.Security.SslStream.EndProcessAuthentication(IAsyncResult result)
   at System.Net.Security.SslStream.EndAuthenticateAsClient(IAsyncResult asyncResult)
   at System.Net.Security.SslStream.<>c.<AuthenticateAsClientAsync>b__65_1(IAsyncResult iar)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Azure.Cosmos.DocumentClient.HttpRequestMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
   at Microsoft.Azure.Cosmos.GatewayAccountReader.GetDatabaseAccountAsync(Uri serviceEndpoint)
   at Microsoft.Azure.Cosmos.Routing.GlobalEndpointManager.GetDatabaseAccountFromAnyLocationsAsync(Uri defaultEndpoint, IList`1 locations, Func`2 getDatabaseAccountFn)
   at Microsoft.Azure.Cosmos.GatewayAccountReader.InitializeReaderAsync()
   at Microsoft.Azure.Cosmos.CosmosAccountServiceConfiguration.InitializeAsync()
   at Microsoft.Azure.Cosmos.DocumentClient.InitializeGatewayConfigurationReaderAsync()
   at Microsoft.Azure.Cosmos.DocumentClient.GetInitializationTaskAsync(IStoreClientFactory storeClientFactory)
   at Microsoft.Azure.Cosmos.DocumentClient.EnsureValidClientAsync()
   at Microsoft.Azure.Cosmos.Handlers.RequestInvokerHandler.EnsureValidClientAsync(RequestMessage request)
   at Microsoft.Azure.Cosmos.Handlers.RequestInvokerHandler.SendAsync(RequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Azure.Cosmos.Handlers.RequestInvokerHandler.SendAsync(Uri resourceUri, ResourceType resourceType, OperationType operationType, RequestOptions requestOptions, ContainerInternal cosmosContainerble`1 partitionKey, Stream streamPayload, Action`1 requestEnricher, CosmosDiagnosticsContext diagnosticsContext, CancellationToken cancellationToken)
   at Microsoft.Azure.Cosmos.ContainerCore.ProcessItemStreamAsync(Nullable`1 partitionKey, String itemId, Stream streamPayload, OperationType operationType, ItemRequestOptions requestOptions, CosmosDiagnostiiagnosticsContext, CancellationToken cancellationToken)
   at Microsoft.Azure.Cosmos.ContainerCore.ExtractPartitionKeyAndProcessItemStreamAsync[T](Nullable`1 partitionKey, String itemId, T item, OperationType operationType, ItemRequestOptions requestOptions, CosmcsContext diagnosticsContext, CancellationToken cancellationToken)
   at Microsoft.Azure.Cosmos.ContainerCore.UpsertItemAsync[T](T item, Nullable`1 partitionKey, ItemRequestOptions requestOptions, CancellationToken cancellationToken)

当我在浏览器中本地查看模拟器时,它使用的是 localhost 证书(我假设是使用 dotnet cli 创建的)。

尝试 1 将 localhost 证书导出为 .pfx 文件,然后通过以下命令(在我的 Dockerfile 中)让 linux 容器信任它

ARG CERTIFICATE_PASSWORD="Test|234"
RUN openssl pkcs12 \
-in "/src/localhost.pfx" \
-clcerts \
-nokeys \
-out "/src/localhost.crt" \
-passin pass:${CERTIFICATE_PASSWORD}
RUN cp "/src/localhost.crt" "/usr/local/share/ca-certificates/"
RUN update-ca-certificates

我认为这种尝试不起作用,至少部分原因是 Windows 机器上的证书是为 localhost 创建的,而要从 docker 连接到它,地址需要是 @987654330 @。

尝试 2 将导出的证书添加到运行函数应用程序的 Kestrel 进程中,希望它会通过将以下内容添加到我的 dockerfile 来兑现它 ENV ASPNETCORE_Kestrel__Certificates__Default__Path=/src/localhost.pfx

尝试 3 更新CosmosClient 实例化以包含覆盖HttpClientFactory 的选项,如下所示:

CosmosClient = new CosmosClient(
                Environment.GetEnvironmentVariable("AzureCosmosConnectionString", EnvironmentVariableTarget.Process),
                new CosmosClientOptions
                {

                    HttpClientFactory = () =>
                    {
                         using (var httpClientHandler = new HttpClientHandler())
                         {
                             httpClientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
                             return new HttpClient(httpClientHandler);
                         }
                    }
                });

上面的每一次尝试,我仍然看到同样的错误。不知道还有什么方法可以让它发挥作用...

刚刚在 GitHub 上找到 this issue。好像我不是一个人。

【问题讨论】:

    标签: docker azure-cosmosdb


    【解决方案1】:

    更新(2021 年 2 月 10 日)

    SDK 现在允许以简单的方式覆盖 SSL 验证(参考 https://docs.microsoft.com/azure/cosmos-db/local-emulator?tabs=cli%2Cssl-netstd21#disable-ssl-validation):

    CosmosClientOptions cosmosClientOptions = new CosmosClientOptions()
    {
        HttpClientFactory = () =>
        {
            HttpMessageHandler httpMessageHandler = new HttpClientHandler()
            {
                ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
            };
    
            return new HttpClient(httpMessageHandler);
        },
        ConnectionMode = ConnectionMode.Gateway
    };
    
    
    CosmosClient client = new CosmosClient(endpoint, authKey, cosmosClientOptions);
    

    旧资料

    参考:

    第 1 步 - 导出证书

    遵循此guide,但证书必须是 PFX,而不是 CRT。它会要求你设置一些密码。

    第 2 步 - 将证书放在可以从 docker 复制或访问的地方

    例如,我把它和我想运行的代码放在一个文件夹中,我可以在启动 Docker 时映射到:

    第 3 步 - 获取您机器的 IP 地址

    根据https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator#running-on-mac-or-linux,我使用ipconfig 并获得了我的Windows IP。

    第 4 步 - 启动 docker 映像

    就我而言,我使用的是 NET Core 3.1 SDK,来自https://docs.microsoft.com/en-us/dotnet/architecture/microservices/net-core-net-framework-containers/official-net-docker-images 的官方版本

    我使用交互式 shell 启动容器,并将 localhost 映射到我在第 3 步中获得的 IP。这让您在连接字符串中使用 localhost

    docker run -v /c/DockerSample:/DockerSample --add-host="localhost:192.168.1.15" -it mcr.microsoft.com/dotnet/core/sdk:3.1 /bin/bash

    我还安装了保存项目的文件夹和要导入的证书。这不是必需的,但我不熟悉 Docker 以了解是否有更好的方法来通过证书。

    shell启动后,我基本上运行Emulator doc中描述的命令并添加证书。

    第 5 步

    Docker 容器现在应该具有连接到 localhost 所需的证书,并且您不需要 HttpClientFactory。

    注意:还有一个错误跟踪 HttpClientFactory 未在任何地方使用,这是您的错误https://github.com/Azure/azure-cosmos-dotnet-v3/pull/1548

    【讨论】:

    • 非常感谢您的时间和帮助。不幸的是,我无法按照您的建议使解决方案正常工作,但以下内容对我有用。
    • 它适用于“CosmosClient”,但 v3“DocumentClient”呢?它没有忽略 ssl 错误选项。
    【解决方案2】:

    首先,感谢Matias Quaranta 提供所有有用的文档。在收到他的回答之前,我已经仔细研究了大部分内容,但非常感谢编译它所花费的所有时间。

    我已经为此苦苦挣扎了大约两个星期,终于得到了一些我认为可行的东西。这项工作主要基于此GitHub issue 中的脚本。我发现在您安装 Cosmos DB 模拟器时会在您的计算机上安装一个 PowerShell 模块,因此我尝试尽可能地利用这些功能来完成这项工作。

    脚本的入口点是函数Start-CosmosDbEmulatorForDocker和它

    1. 确保模拟器已停止。
    2. 生成与 Docker 映像一起使用的新证书,并替换在安装模拟器时创建的证书。
    3. 从为模拟器创建的证书生成.pfx 证书。
    4. 新证书准备好后重新启动模拟器。

    函数获取的密码是用于生成.pfx文件的密码。

    azureCosmosDbEmulator.ps1

    using namespace System.ServiceProcess
    
    Function Start-CosmosDbEmulatorForDocker(
      [Parameter()]
      [securestring]
      $password
    ) {
      $cosmosDbInstallLocation = Get-CosmosDbInstallLocation
      If (!$cosmosDbInstallLocation) {
          Install-AzureCosmosDBEmulator
      }
    
      Write-Host "Importing Microsoft.Azure.CosmosDB.Emulator powershell module."
      Import-Module -Name "$($cosmosDbInstallLocation.InstallLocation)\PSModules\Microsoft.Azure.CosmosDB.Emulator"
    
      Install-CosmosDBDockerCertificate -cosmosDbInstallLocation $cosmosDbInstallLocation.InstallLocation -password $password
    
      Start-CosmosDbEmulator -AllowNetworkAccess -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
    }
    
    Function Get-CosmosDbInstallLocation() {
      Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | ForEach-Object { Get-ItemProperty $_.PsPath } | Where-Object { $_.DisplayName -eq "Azure Cosmos DB Emulator" } | Select-Object InstallLocation
    }
    
    Function Install-AzureCosmosDBEmulator() {
      Write-Host "Installing Azure Cosmos Db Emulator."
      $installer = "$PSScriptRoot\cosmosEmulatorInstaller.msi"
      curl "https://aka.ms/cosmosdb-emulator" -O $installer
      Start-Process -Wait -FilePath msiexec -ArgumentList /i, $installer
      Remove-Item $installer
    }
    
    Function Install-CosmosDBDockerCertificate(
      [Parameter()]
      [string]
      $cosmosDbInstallLocation,
    
      [Parameter()]
      [securestring]
      $password
    ) {
      If ((Get-CosmosDbEmulatorStatus) -ne [ServiceControllerStatus]::Stopped) {
        Write-Host "Stopping Cosmos DB emulator."
        Stop-CosmosDbEmulator
      }
    
      $dockerCertificatesPath = Join-Path (Split-Path -Path $PSScriptRoot -Parent) "certificates"
      $cosmosDbPfxCertificatePath = "$($dockerCertificatesPath)\cosmosdbemulator.pfx"
      Uninstall-Certificate -dockerCertificatePath $cosmosDbPfxCertificatePath
    
      Write-Host "Generating new Cosmos DB certificate to work with Docker."
      New-CosmosDbEmulatorCertificate "host.docker.internal"
      Start-Sleep -s 5
    
      New-DockerCertificate -dockerCertificatePath $cosmosDbPfxCertificatePath -password $password
    
      Set-Location (Split-Path -Path $PSScriptRoot -Parent)
    }
    
    Function Uninstall-Certificate(
      [Parameter()]
      [string]
      $dockerCertificatePath
    ) {
      Write-Host "Removing existing DocumentDbEmulatorCertificate certificate."
    
      if (Test-Path $dockerCertificatePath) {
        Remove-Item -Path $dockerCertificatePath
      }
    }
    
    Function New-DockerCertificate(
      [Parameter()]
      [string]
      $dockerCertificatePath,
    
      [Parameter()]
      [securestring]
      $password
    ) {
      Write-Host "Generating new pfx version of DocumentDbEmulatorCertificate certificate for use in Docker image."
    
      Get-CosmosDbEmulatorCertificate | Export-PfxCertificate -Filepath $dockerCertificatePath -Password $password
    }
    
    

    然后我在 Docker 映像中运行一个 shell 脚本,它将 .pfx 证书安装到 Docker 容器中。 COSMOS_DB_EMULATOR_PFX_PASSWORD 值必须与 PowerShell 脚本使用的值匹配。

    trust_cosmos_db_emulator_crt.sh

    #!/bin/bash
    
    # Save current working directory
    PWD=`pwd`
    pushd $PWD
    
    # Find and move to the location of this script
    DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
    cd $DIR
    
    if [ -n "$1" ]; then
        COSMOS_DB_EMULATOR_PFX=$1
    else
        COSMOS_DB_EMULATOR_PFX="/certificates/cosmosdbemulator.pfx"
    fi
    COSMOS_DB_EMULATOR_PFX_PASSWORD="SUPER_SECRET_PASSWORD"
    CERT_TO_TRUST="cosmosdbemulator.crt"
    
    # Generate .crt file if pfx exists
    if [ -f "$COSMOS_DB_EMULATOR_PFX" ]; then
        openssl pkcs12 -in $COSMOS_DB_EMULATOR_PFX -clcerts -nokeys -out cosmosdbemulator.crt -passin pass:$COSMOS_DB_EMULATOR_PFX_PASSWORD;
    fi
    
    # # Trust Cert (will end located in /etc/ssl/certs/ based on *.crt name as a *.pem, e.g. /etc/ssl/certs/cosmosdbemulator.pem for cosmosdbemulator.crt)
    if [ -f "$CERT_TO_TRUST" ]; then
        cp $CERT_TO_TRUST /usr/local/share/ca-certificates/
        update-ca-certificates
        rm $CERT_TO_TRUST;
    fi
    
    # Restore working directory
    popd
    

    我的项目结构如下:

    src/
      scripts/
        azureCosmosDbEmulator.ps1
        trust_cosmos_db_emulator_crt.sh
      certificates/
      DockerFile
    

    Dockerfile 包含以下几行:

    COPY ["/scripts/", "/scripts/"]
    COPY ["/certificates/", "/certificates/"]
    RUN /scripts/trust_cosmos_db_emulator_crt.sh
    

    一切就绪后,我可以使用 docker build -t temp . 构建 docker 映像,然后使用 docker run -it -p 80:80 temp 运行它,并且在 docker 容器内运行的代码将与我本地计算机上安装的 Azure Cosmos DB 版本进行通信模拟器。

    由于这是一个巨大的颈部疼痛,如果您也遇到这种疼痛,请在here 上投票支持 Microsoft 提供更好的支持。

    【讨论】:

    • 这篇文章花了大约一个小时来配置我的具体要求,之前花了 3 天时间试图让它通过其他来源工作,我偶然发现了它。应该在 MS 文档中。喜欢你的工作。
    • @SeanStanden 非常感谢。很高兴它也对你有用!就像我说的,这是几个星期的工作,但我很高兴它也能让其他人受益。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-07-20
    • 2020-02-09
    • 2019-02-24
    • 1970-01-01
    • 2021-07-18
    • 2020-06-23
    • 1970-01-01
    相关资源
    最近更新 更多