Azure DevOps 服務 |Azure DevOps Server |Azure DevOps Server 2022
本文提供在 Docker 中執行 Azure Pipelines 代理程式的指示。 您可以在 Azure Pipelines 中設定自我裝載代理程式,以在 Windows Server Core 內執行(適用於 Windows 主機),或使用 Docker 在 Ubuntu 容器中執行。 當您想要使用外部協調流程執行代理程式,例如 Azure 容器實例時,這會很有用。 在本文中,您將逐步解說完整的容器範例,包括處理代理程式自我更新。
Windows 和 Linux 都支援作為容器主機。 Windows 容器應在 Windows vmImage上執行。
若要在 Docker 中執行代理程式,您會將幾個 環境變數 傳遞至 docker run,其會將代理程式設定為連線到 Azure Pipelines 或 Azure DevOps Server。 最後,您可以 自定義容器 以符合您的需求。 工作和腳本可能取決於容器 PATH上可用的特定工具,而且您必須負責確保這些工具可供使用。
窗戶
啟用 Hyper-V
根據預設,Windows 上不會啟用 Hyper-V。 如果您想要在容器之間提供隔離,您必須啟用 Hyper-V。 否則,適用於 Windows 的 Docker 將不會啟動。
注意
您必須在電腦上啟用虛擬化。 它通常預設為啟用。 不過,如果 Hyper-V 安裝失敗,請參閱您的系統檔,以瞭解如何啟用虛擬化。
安裝適用於 Windows 的 Docker (英文)
如果您使用 Windows 10,您可以安裝 Docker Community Edition。 Windows Server 2016 時,安裝 Mirantis Container Runtime(前稱 Docker Enterprise Edition)。
將 Docker 切換為使用 Windows 容器
根據預設,適用於 Windows 的 Docker 會設定為使用 Linux 容器。 若要允許執行 Windows 容器,請確認適用於 Windows 的 Docker 正在執行 Windows 精靈。
建立並建置 Dockerfile
接下來,建立 Dockerfile。
開啟命令提示字元。
建立新的目錄:
mkdir "C:\azp-agent-in-docker\"移至這個新目錄:
cd "C:\azp-agent-in-docker\"將下列內容儲存至名為
C:\azp-agent-in-docker\azp-agent-windows.dockerfile的檔案:FROM mcr.microsoft.com/windows/servercore:ltsc2022 WORKDIR /azp/ COPY ./start.ps1 ./ CMD powershell .\start.ps1將下列內容儲存至
C:\azp-agent-in-docker\start.ps1:function Print-Header ($header) { Write-Host "`n${header}`n" -ForegroundColor Cyan } if (-not (Test-Path Env:AZP_URL)) { Write-Error "error: missing AZP_URL environment variable" exit 1 } if (-not (Test-Path Env:AZP_TOKEN_FILE)) { if (-not (Test-Path Env:AZP_TOKEN)) { Write-Error "error: missing AZP_TOKEN environment variable" exit 1 } $Env:AZP_TOKEN_FILE = "\azp\.token" $Env:AZP_TOKEN | Out-File -FilePath $Env:AZP_TOKEN_FILE } Remove-Item Env:AZP_TOKEN if ((Test-Path Env:AZP_WORK) -and -not (Test-Path $Env:AZP_WORK)) { New-Item $Env:AZP_WORK -ItemType directory | Out-Null } New-Item "\azp\agent" -ItemType directory | Out-Null # Let the agent ignore the token env variables $Env:VSO_AGENT_IGNORE = "AZP_TOKEN,AZP_TOKEN_FILE" Set-Location agent Print-Header "1. Determining matching Azure Pipelines agent..." $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$(Get-Content ${Env:AZP_TOKEN_FILE})")) $package = Invoke-RestMethod -Headers @{Authorization=("Basic $base64AuthInfo")} "$(${Env:AZP_URL})/_apis/distributedtask/packages/agent?platform=win-x64&`$top=1" $packageUrl = $package[0].Value.downloadUrl Write-Host $packageUrl Print-Header "2. Downloading and installing Azure Pipelines agent..." $wc = New-Object System.Net.WebClient $wc.DownloadFile($packageUrl, "$(Get-Location)\agent.zip") Expand-Archive -Path "agent.zip" -DestinationPath "\azp\agent" try { Print-Header "3. Configuring Azure Pipelines agent..." .\config.cmd --unattended ` --agent "$(if (Test-Path Env:AZP_AGENT_NAME) { ${Env:AZP_AGENT_NAME} } else { hostname })" ` --url "$(${Env:AZP_URL})" ` --auth PAT ` --token "$(Get-Content ${Env:AZP_TOKEN_FILE})" ` --pool "$(if (Test-Path Env:AZP_POOL) { ${Env:AZP_POOL} } else { 'Default' })" ` --work "$(if (Test-Path Env:AZP_WORK) { ${Env:AZP_WORK} } else { '_work' })" ` --replace Print-Header "4. Running Azure Pipelines agent..." .\run.cmd } finally { Print-Header "Cleanup. Removing Azure Pipelines agent..." .\config.cmd remove --unattended ` --auth PAT ` --token "$(Get-Content ${Env:AZP_TOKEN_FILE})" }在該目錄中執行下列命令:
docker build --tag "azp-agent:windows" --file "./azp-agent-windows.dockerfile" .最後一個影像會標記
azp-agent:windows。
啟動映像
現在您已建立映像檔,您可以運行容器。 這會安裝最新版的代理程序、設定並執行代理程式。 它會以您選擇的指定 Azure DevOps 或 Azure DevOps Server 實例的指定代理程式集區(預設代理 Default 程式集區)為目標:
docker run -e AZP_URL="<Azure DevOps instance>" -e AZP_TOKEN="<Personal Access Token>" -e AZP_POOL="<Agent Pool Name>" -e AZP_AGENT_NAME="Docker Agent - Windows" --name "azp-agent-windows" azp-agent:windows
如果您遇到網路問題,您可能需要指定 --network 參數。
docker run --network "Default Switch" < . . . >
如果您希望能夠停止容器並移除代理程式,您可能需要指定 --interactive 和 --tty 旗標(或者只需要 -it)。Ctrl + C。
docker run --interactive --tty < . . . >
如果您希望每個管線作業都使用全新的代理容器,請將這個--once旗標傳遞給run命令。
docker run < . . . > --once
--once使用旗標時,您可能會想要使用容器編排系統,例如 Kubernetes 或 Azure 容器執行個體,在作業完成後啟動容器的副本。
您可以使用選擇性 環境變數來控制代理程式名稱、代理程式集區和代理程式工作目錄。
Linux
安裝 Docker
根據您的 Linux 散發套件,您可以安裝 Docker Community Edition 或 Docker Enterprise Edition。
建立並建置 Dockerfile
接下來,建立 Dockerfile。
開啟終端機。
建立新的目錄 (建議):
mkdir ~/azp-agent-in-docker/移至這個新目錄:
cd ~/azp-agent-in-docker/將下列內容儲存至
~/azp-agent-in-docker/azp-agent-linux.dockerfile:針對 Alpine,請使用 此問題中所述的技術:
FROM python:3-alpine ENV TARGETARCH="linux-musl-x64" # Another option: # FROM arm64v8/alpine # ENV TARGETARCH="linux-musl-arm64" RUN apk update && \ apk upgrade && \ apk add bash curl gcc git icu-libs jq musl-dev python3-dev libffi-dev openssl-dev cargo make # Install Azure CLI RUN pip install --upgrade pip RUN pip install azure-cli WORKDIR /azp/ COPY ./start.sh ./ RUN chmod +x ./start.sh RUN adduser -D agent RUN chown agent ./ USER agent # Another option is to run the agent as root. # ENV AGENT_ALLOW_RUNASROOT="true" ENTRYPOINT [ "./start.sh" ]對於 Ubuntu 24.04:
FROM ubuntu:24.04 ENV TARGETARCH="linux-x64" # Also can be "linux-arm", "linux-arm64". RUN apt update && \ apt upgrade -y && \ apt install -y curl git jq libicu74 # Install Azure CLI RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash WORKDIR /azp/ COPY ./start.sh ./ RUN chmod +x ./start.sh # Create agent user and set up home directory RUN useradd -m -d /home/agent agent RUN chown -R agent:agent /azp /home/agent USER agent # Another option is to run the agent as root. # ENV AGENT_ALLOW_RUNASROOT="true" ENTRYPOINT [ "./start.sh" ]針對 Ubuntu 22.04:
FROM ubuntu:22.04 ENV TARGETARCH="linux-x64" # Also can be "linux-arm", "linux-arm64". RUN apt update && \ apt upgrade -y && \ apt install -y curl git jq libicu70 # Install Azure CLI RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash WORKDIR /azp/ COPY ./start.sh ./ RUN chmod +x ./start.sh # Create agent user and set up home directory RUN useradd -m -d /home/agent agent RUN chown -R agent:agent /azp /home/agent USER agent # Another option is to run the agent as root. # ENV AGENT_ALLOW_RUNASROOT="true" ENTRYPOINT [ "./start.sh" ]
如果您想要以 root 身分執行代理程式,請取消
ENV AGENT_ALLOW_RUNASROOT="true"行的註解,並移除在此行之前新增的agent使用者。注意
工作可能取決於容器預期提供的可執行檔。 例如,您必須將
zip和unzip套件新增到RUN apt install -y命令中,才能執行ArchiveFiles和ExtractFiles工作。 此外,由於這是Linux Ubuntu映像供代理程式使用,因此您可以視需要自定義映像。 例如:如果您需要建置 .NET 應用程式,可以遵循檔案 在 Ubuntu 上安裝 .NET SDK 或 .NET 執行環境,然後將其新增至映像檔。將下列內容儲存至
~/azp-agent-in-docker/start.sh,請務必使用 Unix 格式(LF)行尾:#!/bin/bash set -e # Load a token either from the environment variable or by using the service principal credentials. load_azp_token() { # Always un-export AZP_CLIENTSECRET so it is never inherited by # child processes (e.g., agent job processes). It remains available # as a shell variable so that it can be used to generate a token. export -n AZP_CLIENTSECRET if [ -n "$AZP_CLIENTID" ]; then if [ -z "$AZP_CLIENTSECRET" ]; then echo 1>&2 "error: AZP_CLIENTSECRET must be set when AZP_CLIENTID is used" exit 1 fi if [ -z "$AZP_TENANTID" ]; then echo 1>&2 "error: AZP_TENANTID must be set when AZP_CLIENTID is used" exit 1 fi echo "Using service principal credentials to get token" # Isolate Azure CLI state to a dedicated, restrictive directory to avoid # leaking cached credentials/tokens to subsequent pipeline job processes. AZP_TOKEN="$( export AZURE_CONFIG_DIR="$(mktemp -d)" chmod 700 "$AZURE_CONFIG_DIR" az_cli_cleanup() { # Log out and remove the isolated Azure CLI config directory to purge cached tokens. az logout --username "$AZP_CLIENTID" >/dev/null 2>&1 || true az account clear >/dev/null 2>&1 || true rm -rf "$AZURE_CONFIG_DIR" } trap az_cli_cleanup EXIT az login --allow-no-subscriptions --service-principal --username "$AZP_CLIENTID" --password "$AZP_CLIENTSECRET" --tenant "$AZP_TENANTID" >/dev/null # adapted from https://learn.microsoft.com/en-us/azure/databricks/dev-tools/user-aad-token az account get-access-token --query accessToken --output tsv )" echo "Token retrieved" fi if [ -z "${AZP_TOKEN_FILE}" ]; then if [ -z "${AZP_TOKEN}" ]; then echo 1>&2 "error: missing AZP_TOKEN environment variable" exit 1 fi AZP_TOKEN_FILE="$(dirname "$0")/.token" fi if [ -n "${AZP_TOKEN-}" ]; then if [ -d "${AZP_TOKEN_FILE}" ]; then echo 1>&2 "error: AZP_TOKEN_FILE is a directory, expected a file path: ${AZP_TOKEN_FILE}" exit 1 fi # If the file already exists, it must itself be writable. if [ -e "${AZP_TOKEN_FILE}" ]; then if [ ! -w "${AZP_TOKEN_FILE}" ]; then echo 1>&2 "error: existing AZP_TOKEN_FILE is not writable: ${AZP_TOKEN_FILE}" exit 1 fi else if [ ! -w "$(dirname "${AZP_TOKEN_FILE}")" ]; then echo 1>&2 "error: AZP_TOKEN_FILE parent directory is missing or not writable: $(dirname "${AZP_TOKEN_FILE}")" exit 1 fi fi (umask 177 && echo -n "${AZP_TOKEN}" > "${AZP_TOKEN_FILE}") chmod 600 "${AZP_TOKEN_FILE}" || true else if [ ! -r "${AZP_TOKEN_FILE}" ]; then echo 1>&2 "error: AZP_TOKEN_FILE is not readable: ${AZP_TOKEN_FILE}" exit 1 fi if [ ! -s "${AZP_TOKEN_FILE}" ]; then echo 1>&2 "error: AZP_TOKEN_FILE is empty: ${AZP_TOKEN_FILE}" exit 1 fi fi # Unset the AZP_TOKEN environment variable to prevent it from being exposed. # At this point the token is only available in the file specified by AZP_TOKEN_FILE. unset AZP_TOKEN } # Cleanup function to remove the agent configuration. cleanup() { trap "" EXIT if [ -e ./config.sh ]; then print_header "Cleanup. Removing Azure Pipelines agent..." # Try to get a new token if using service principal credentials, as the old one # might have expired between the time it was waiting to run a job and now. # If refresh fails, continue cleanup with the existing token file. if [ -n "$AZP_CLIENTID" ]; then if ! ( load_azp_token ); then echo "Warning: failed to refresh Azure Pipelines token during cleanup. Continuing with the existing token." fi fi # If the agent has some running jobs, the configuration removal process will fail. # So, give it some time to finish the job. But only try max_attempts and then exit. max_attempts=10 attempt=1 while [ $attempt -le $max_attempts ]; do ./config.sh remove --unattended --auth "PAT" --token $(cat "${AZP_TOKEN_FILE}") && break echo "Attempt $attempt failed. Retrying in 30 seconds..." attempt=$((attempt+1)) sleep 30 done if [ $attempt -gt $max_attempts ]; then echo 1>&2 "warning: failed to remove agent configuration after $max_attempts attempts" fi fi } print_header() { lightcyan="\033[1;36m" nocolor="\033[0m" echo -e "\n${lightcyan}$1${nocolor}\n" } if [ -z "${AZP_URL}" ]; then echo 1>&2 "error: missing AZP_URL environment variable" exit 1 fi # Load the AZP token for initial setup. load_azp_token if [ -n "${AZP_WORK}" ]; then mkdir -p "${AZP_WORK}" fi # Let the agent ignore sensitive env variables, including tokens and the client secret. export VSO_AGENT_IGNORE="AZP_TOKEN,AZP_TOKEN_FILE,AZP_CLIENTSECRET" print_header "1. Determining matching Azure Pipelines agent..." AZP_AGENT_PACKAGES=$(curl -LsS \ -u user:$(cat "${AZP_TOKEN_FILE}") \ -H "Accept:application/json" \ "${AZP_URL}/_apis/distributedtask/packages/agent?platform=${TARGETARCH}&top=1") AZP_AGENT_PACKAGE_LATEST_URL=$(echo "${AZP_AGENT_PACKAGES}" | jq -r ".value[0].downloadUrl") if [ -z "${AZP_AGENT_PACKAGE_LATEST_URL}" -o "${AZP_AGENT_PACKAGE_LATEST_URL}" == "null" ]; then echo 1>&2 "error: could not determine a matching Azure Pipelines agent" echo 1>&2 "check that account "${AZP_URL}" is correct and the token is valid for that account" exit 1 fi print_header "2. Downloading and extracting Azure Pipelines agent..." curl -LsS "${AZP_AGENT_PACKAGE_LATEST_URL}" | tar -xz & wait $! . ./env.sh trap "cleanup; exit 0" EXIT trap "cleanup; exit 130" INT trap "cleanup; exit 143" TERM print_header "3. Configuring Azure Pipelines agent..." # Despite it saying "PAT", it can be the token through the service principal ./config.sh --unattended \ --agent "${AZP_AGENT_NAME:-$(hostname)}" \ --url "${AZP_URL}" \ --auth "PAT" \ --token $(cat "${AZP_TOKEN_FILE}") \ --pool "${AZP_POOL:-Default}" \ --work "${AZP_WORK:-_work}" \ --replace \ --acceptTeeEula & wait $! print_header "4. Running Azure Pipelines agent..." chmod +x ./run.sh # To be aware of TERM and INT signals call ./run.sh # Running it with the --once flag at the end will shut down the agent after the build is executed ./run.sh "$@" & wait $!注意
您也必須使用 Kubernetes 或 Azure 容器實例等容器協調流程系統,在工作完成時啟動容器的新複本。
在該目錄中執行下列命令:
docker build --tag "azp-agent:linux" --file "./azp-agent-linux.dockerfile" .最後一個影像會標記
azp-agent:linux。
啟動映像
現在您已建立映像檔,您可以運行容器。 這會安裝最新版的代理程序、設定並執行代理程式。 它會以您選擇的指定 Azure DevOps 或 Azure DevOps Server 實例的指定代理程式集區(預設代理 Default 程式集區)為目標:
docker run -e AZP_URL="<Azure DevOps instance>" -e AZP_TOKEN="<Personal Access Token>" -e AZP_POOL="<Agent Pool Name>" -e AZP_AGENT_NAME="Docker Agent - Linux" --name "azp-agent-linux" azp-agent:linux
如果您希望能夠停止容器並移除代理程式,您可能需要指定 --interactive 和 --tty 旗標(或者只需要 -it)。Ctrl + C。
docker run --interactive --tty < . . . >
如果您希望每個管線作業都使用全新的代理容器,請將這個--once旗標傳遞給run命令。
docker run < . . . > --once
--once使用旗標時,您可能會想要使用容器編排系統,例如 Kubernetes 或 Azure 容器執行個體,在作業完成後啟動容器的副本。
您可以使用選擇性 環境變數來控制代理程式名稱、代理程式集區和代理程式工作目錄。
環境變數
| 環境變數 | 描述 |
|---|---|
| AZP_URL | Azure DevOps 或 Azure DevOps Server 實例的 URL。 |
| AZP_TOKEN | 個人存取權杖 (PAT) |
| AZP_CLIENTID | 服務主體 用戶端識別碼 |
| AZP_CLIENTSECRET | 服務主體用戶端密鑰 |
| AZP_TENANTID | 服務主體租用戶標識碼 |
| AZP_AGENT_NAME | 代理程式名稱(預設值:容器主機名)。 |
| AZP_POOL | 代理程式集區名稱(預設值: Default)。 |
| AZP_WORK | 工作目錄(預設值: _work)。 |
認證
需要下列其中一項:
- 如果使用 PAT:
AZP_TOKEN - 如果使用服務主體:
AZP_CLIENTID、AZP_CLIENTSECRET和AZP_TENANTID
授權
令牌或服務主體必須具有 組織層級的 AZP_URL 範圍。 如果使用 PAT,則必須由有權 設定代理程式的使用者建立令牌。
新增工具和自定義容器
您已建立基本組建代理程式。 您可以擴充 Dockerfile 以包含其他工具和其相依性,或使用此容器作為基底層來建置您自己的容器。 請確保下列項目保持不變:
- 腳本
start.sh是由 Dockerfile 呼叫。 - 腳本
start.sh是 Dockerfile 中的最後一個命令。 - 確定衍生容器不會移除 Dockerfile 所陳述的任何相依性。
在 Docker 容器內使用 Docker
若要在 Docker 容器內使用 Docker,需要掛載 Docker 套接字。
警告
這樣做具有嚴重的安全性影響。 容器內的程式代碼現在可以在 Docker 主機上以 root 使用者身份執行。
如果您確定要這樣做,請參閱 Docker.com 上的綁定掛載文件。
注意
對於 Azure Pipelines 的 容器作業,預設不會將主機 Docker socket(/var/run/docker.sock)對應至作業容器。 如果你的容器作業需要從容器內存取主機上的 Docker 常駐程式,請在容器資源上設定 mapDockerSocket: true。 欲了解更多資訊,請參閱 Docker socket mapping。
使用 Azure Kubernetes Service 叢集
警告
請注意,由於 Docker in Docker 的限制,任何基於 Docker 的任務都無法在 AKS 1.19 或更高版本上運行。 Docker 被 containerd 取代 在 Kubernetes 1.19 中,且 Docker-in-Docker 變得無法使用。
部署和設定 Azure Kubernetes Service
請遵循 快速入門:使用 Azure 入口網站部署 Azure Kubernetes Service (AKS) 叢集中的步驟。 之後,您的 PowerShell 或 Shell 控制台可以使用 kubectl 命令行。
部署及設定 Azure Container Registry
請遵循 快速入門:使用 Azure 入口網站建立 Azure 容器登錄中的步驟。 之後,您可以從 Azure Container Registry 推送和提取容器。
設定機密資料並部署副本集
在 AKS 叢集上建立機密資訊。
kubectl create secret generic azdevops \ --from-literal=AZP_URL=https://dev.azure.com/yourOrg \ --from-literal=AZP_TOKEN=YourPAT \ --from-literal=AZP_POOL=NameOfYourPool執行此指令將您的容器推送至 Container Registry:
docker push "<acr-server>/azp-agent:<tag>"設定現有 AKS 叢集的容器登錄整合。
注意
如果您在 Azure 入口網站上有多個訂用帳戶,請先使用此命令來選取訂用帳戶
az account set --subscription "<subscription id or subscription name>"az aks update -n "<myAKSCluster>" -g "<myResourceGroup>" --attach-acr "<acr-name>"將下列內容儲存至
~/AKS/ReplicationController.yml:apiVersion: apps/v1 kind: Deployment metadata: name: azdevops-deployment labels: app: azdevops-agent spec: replicas: 1 # here is the configuration for the actual agent always running selector: matchLabels: app: azdevops-agent template: metadata: labels: app: azdevops-agent spec: containers: - name: kubepodcreation image: <acr-server>/azp-agent:<tag> env: - name: AZP_URL valueFrom: secretKeyRef: name: azdevops key: AZP_URL - name: AZP_TOKEN valueFrom: secretKeyRef: name: azdevops key: AZP_TOKEN - name: AZP_POOL valueFrom: secretKeyRef: name: azdevops key: AZP_POOL volumeMounts: - mountPath: /var/run/docker.sock name: docker-volume volumes: - name: docker-volume hostPath: path: /var/run/docker.sock此 Kubernetes YAML 會建立副本集和部署,其中
replicas: 1表示在叢集上執行的號碼或代理程式。執行此命令:
kubectl apply -f ReplicationController.yml
現在您的代理程式會執行 AKS 叢集。
設定自訂 MTU 參數
允許為容器工作所使用的網路指定 MTU 值(這在 k8s 叢集中的 docker-in-docker 情況下十分有用)。
您必須設定環境變數AGENT_DOCKER_MTU_VALUE來設定 MTU 值,然後重新啟動自我裝載代理程式。 您可以 在這裡 找到有關代理程式重新啟動的詳細資訊,以及 在這裡為每個個別代理程式設定不同的環境變數。
這可讓您設定作業容器的網路參數,使用此命令類似於在容器網路設定時使用下一個命令:
-o com.docker.network.driver.mtu=AGENT_DOCKER_MTU_VALUE
在 Docker 容器中使用 Docker 掛載磁碟區
如果 Docker 容器在另一個 Docker 容器內執行,它們都會使用主機的精靈,因此所有掛接路徑都會參考主機,而不是容器。
例如,如果我們想要將路徑從主機掛接至外部 Docker 容器,我們可以使用此命令:
docker run ... -v "<path-on-host>:<path-on-outer-container>" ...
如果我們想要將路徑從主機掛接至內部 Docker 容器,則可以使用此命令:
docker run ... -v "<path-on-host>:<path-on-inner-container>" ...
但是,我們無法將外部容器的路徑掛接至內部容器;若要解決此問題,我們必須宣告 ENV 變數:
docker run ... --env DIND_USER_HOME=$HOME ...
在此之後,我們可以使用下列命令,從外部容器啟動內部容器:
docker run ... -v "${DIND_USER_HOME}:<path-on-inner-container>" ...
常見錯誤
如果您使用 Windows,並收到下列錯誤:
standard_init_linux.go:178: exec user process caused "no such file or directory"
下載並安裝 git-scm 以安裝 Git Bash。
執行此命令:
dos2unix ~/azp-agent-in-docker/Dockerfile
dos2unix ~/azp-agent-in-docker/start.sh
git add .
git commit -m "Fixed CR"
git push
然後再試一次。 您不再遇到錯誤。