从 Docker 容器连接到 CosmosDB

pei*_*ent 5 docker azure-cosmosdb

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

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

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

ARG COSMOS_CONNECTION_STRING="AccountEndpoint=https://host.docker.internal:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
Run Code Online (Sandbox Code Playgroud)

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

ARG COSMOS_CONNECTION_STRING="AccountEndpoint=https://host.docker.internal:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
Run Code Online (Sandbox Code Playgroud)

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

---> 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)
Run Code Online (Sandbox Code Playgroud)

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

尝试 1 将本地主机证书导出为.pfx文件,然后通过以下命令让 Linux 容器信任该文件(在我的 Dockerfile 中)

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
Run Code Online (Sandbox Code Playgroud)

我认为此尝试不起作用,至少部分是由于 Windows 计算机上的证书是为 创建的localhost,而要从 docker 连接到它,地址需要为host.docker.internal。

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

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

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

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

刚刚在 GitHub 上发现了这个问题。看来我并不孤单。

Mat*_*nta 7

更新(2021 年 2 月 10 日)

SDK 现在允许以简单的方式覆盖 SSL 验证(参考https://learn.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);
Run Code Online (Sandbox Code Playgroud)

较旧的信息

参考:

第 1 步 - 导出证书

遵循本指南,但证书需要是PFX,而不是 CRT。它会要求您设置一些密码。

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

例如,我将其放在启动 Docker 时可以映射到的文件夹以及我想要运行的代码中:

图像

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

根据https://learn.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://learn.microsoft.com/en-us/dotnet/architecture/microservices/net-core-net-framework-containers/official-的官方 SDK。网络 docker 图像

我使用交互式 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 启动后,我基本上运行模拟器文档中描述的命令并添加证书。

图像

步骤5

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

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


pei*_*ent 4

首先,感谢Matias Quaranta提供的所有有用文档。在收到他的回答之前,我已经仔细阅读了其中的大部分内容,但我真的很感激编译它所花费的所有时间。

我已经为此奋斗了大约两周,终于得到了一些我认为可行的东西。这项工作主要基于此GitHub 问题中找到的脚本。我发现当您安装 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
}

Run Code Online (Sandbox Code Playgroud)

然后,我在 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
Run Code Online (Sandbox Code Playgroud)

我的项目结构如下:

src/
  scripts/
    azureCosmosDbEmulator.ps1
    trust_cosmos_db_emulator_crt.sh
  certificates/
  DockerFile
Run Code Online (Sandbox Code Playgroud)

Dockerfile 包含以下几行:

COPY ["/scripts/", "/scripts/"]
COPY ["/certificates/", "/certificates/"]
RUN /scripts/trust_cosmos_db_emulator_crt.sh
Run Code Online (Sandbox Code Playgroud)

完成所有这些后,我可以构建 docker 映像,docker build -t temp .然后运行它,docker run -it -p 80:80 temp并且 docker 容器内运行的代码将与本地计算机安装的 Azure Cosmos DB 模拟器版本进行通信。

由于这是一个巨大的痛苦,如果您也遇到这种痛苦,请在此处投票支持 Microsoft 对此提供更好的支持。