DevOps & Scaling

Функции Azure + CaptchaAI: интеграция с облаком

Держать процесс решения CAPTCHA включённым круглосуточно ради десятка задач в час — переплата за простаивающий сервер. Azure Functions снимает эту проблему за счёт serverless-модели: вы платите только за фактическое время выполнения, а вызов CaptchaAI встраивается в HTTP-триггер или обработчик очереди буквально в несколько строк кода. Экосистема Azure добавляет то, что иначе пришлось бы собирать вручную: Key Vault хранит API-ключ отдельно от кода, Queue Storage распределяет задачи между экземплярами функции, а Application Insights показывает, где именно теряется время при опросе res.php. Ниже — рабочий пример на Python, который можно развернуть в европейском регионе Azure для минимальной задержки, если ваша команда работает из России, Беларуси или Казахстана.


HTTP-триггерная функция

Базовый сценарий — HTTP-endpoint, который принимает метод CAPTCHA и параметры, вызывает CaptchaAI и синхронно возвращает токен. Функция ниже отправляет задачу на in.php, затем опрашивает res.php каждые 5 секунд, пока не получит токен или не истечёт тайм-аут в 90 секунд. Для reCAPTCHA v2 передайте googlekey и pageurl в params; для Cloudflare Turnstile — соответствующий sitekey. Такой синхронный вызов удобен, когда клиент готов подождать ответ на HTTP-запрос; для длинных очередей задач лучше подойдёт вариант с Queue Storage ниже.

# function_app.py
import json
import time
import os
import logging
import urllib.request
import urllib.parse
import azure.functions as func

app = func.FunctionApp()


@app.route(route="solve", methods=["POST"])
def solve_captcha(req: func.HttpRequest) -> func.HttpResponse:
    """HTTP trigger for CAPTCHA solving."""
    try:
        body = req.get_json()
    except ValueError:
        return func.HttpResponse(
            json.dumps({"error": "JSON body required"}),
            status_code=400,
            mimetype="application/json",
        )

    method = body.get("method", "userrecaptcha")
    params = body.get("params", {})
    api_key = os.environ["CAPTCHAAI_KEY"]

    try:
        token = solve(api_key, method, params)
        return func.HttpResponse(
            json.dumps({"token": token}),
            mimetype="application/json",
        )
    except Exception as e:
        logging.error(f"Solve failed: {e}")
        return func.HttpResponse(
            json.dumps({"error": str(e)}),
            status_code=500,
            mimetype="application/json",
        )


def solve(api_key, method, params, timeout=90):
    """Solve CAPTCHA via CaptchaAI API."""
    submit_data = urllib.parse.urlencode({
        "key": api_key,
        "method": method,
        "json": 1,
        **params,
    }).encode()

    req = urllib.request.Request(
        "https://ocr.captchaai.com/in.php",
        data=submit_data,
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        result = json.loads(resp.read())

    if result.get("status") != 1:
        raise RuntimeError(f"Submit error: {result.get('request')}")

    task_id = result["request"]

    start = time.time()
    while time.time() - start < timeout:
        time.sleep(5)
        poll_url = (
            f"https://ocr.captchaai.com/res.php"
            f"?key={api_key}&action=get&id={task_id}&json=1"
        )
        with urllib.request.urlopen(poll_url, timeout=15) as resp:
            data = json.loads(resp.read())

        if data["request"] != "CAPCHA_NOT_READY":
            if data.get("status") == 1:
                return data["request"]
            raise RuntimeError(f"Solve error: {data['request']}")

    raise TimeoutError("Solve timeout")

Интеграция с Key Vault

Хранить API-ключ CaptchaAI в переменных окружения функции — рабочий, но небезопасный вариант: ключ виден любому, у кого есть доступ к порталу Azure или к экспорту конфигурации приложения. Key Vault решает это иначе: секрет хранится отдельно, а функция получает к нему доступ через managed identity — без ключа в коде и без ключа в открытом виде в настройках приложения.

# Create Key Vault
az keyvault create \
  --name captchaai-vault \
  --resource-group myResourceGroup

# Store secret
az keyvault secret set \
  --vault-name captchaai-vault \
  --name CaptchaAIKey \
  --value "YOUR_API_KEY"

# Grant function access
az webapp identity assign \
  --name my-captcha-function \
  --resource-group myResourceGroup

az keyvault set-policy \
  --name captchaai-vault \
  --object-id <principal-id> \
  --secret-permissions get

Ссылка в настройках приложения:

[email protected](SecretUri=https://captchaai-vault.vault.azure.net/secrets/CaptchaAIKey/)

Azure резолвит такую ссылку на лету при холодном старте функции — сам ключ никогда не попадает в репозиторий и не светится в логах развёртывания.


Пакетная обработка через Queue Storage

Если задачи CAPTCHA приходят не по одной, а пакетами — например, парсер за раз ставит в очередь сотни страниц с Cloudflare Turnstile — синхронный HTTP-вызов не подходит: клиент не будет держать соединение открытым десятки секунд ради каждой задачи. Триггер очереди работает иначе: сообщение с методом и параметрами кладётся в Queue Storage, функция забирает его в фоне, решает CAPTCHA и сохраняет результат отдельно — в Table Storage или в любом другом хранилище состояния. Готовность результата клиент проверяет уже отдельным запросом, не дожидаясь ответа синхронно.

@app.queue_trigger(
    arg_name="msg",
    queue_name="captcha-tasks",
    connection="AzureWebJobsStorage",
)
def process_queue_task(msg: func.QueueMessage):
    """Process CAPTCHA task from queue."""
    task = json.loads(msg.get_body().decode())
    api_key = os.environ["CAPTCHAAI_KEY"]

    try:
        token = solve(api_key, task["method"], task["params"])
        logging.info(f"Task {task['id']} solved")

        # Store result in Table Storage or return queue
        _store_result(task["id"], "success", token)

    except Exception as e:
        logging.error(f"Task {task['id']} failed: {e}")
        _store_result(task["id"], "error", str(e))


def _store_result(task_id, status, value):
    """Store result (simplified — use Table Storage in production)."""
    logging.info(f"Result: {task_id} = {status}")

Структура проекта

Минимальный проект Azure Functions на Python выглядит так:

captcha-function/
├── function_app.py
├── requirements.txt
├── host.json
└── local.settings.json

requirements.txt:

azure-functions

Явно задайте functionTimeout — 5-минутный тайм-аут по умолчанию слишком короткий, если задача требует нескольких попыток решения CAPTCHA:

host.json:

{
  "version": "2.0",
  "functionTimeout": "00:02:00",
  "logging": {
    "logLevel": {
      "default": "Information"
    }
  }
}

Для локальной разработки ключ можно временно держать в local.settings.json — файл не должен попадать в git и используется только на машине разработчика:

local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "CAPTCHAAI_KEY": "YOUR_API_KEY_FOR_LOCAL_DEV"
  }
}

Развёртывание

Разверните функцию через Azure CLI. В примере ниже используется регион westus2 — командам, работающим из России, Беларуси или Казахстана, обычно выгоднее взять европейский регион (westeurope или northeurope): меньше задержка при опросе res.php, который функция вызывает раз в 5 секунд до готовности токена.

# Create function app
az functionapp create \
  --resource-group myResourceGroup \
  --consumption-plan-location westus2 \
  --runtime python \
  --runtime-version 3.11 \
  --functions-version 4 \
  --name my-captcha-solver \
  --storage-account mystorageaccount

# Deploy
func azure functionapp publish my-captcha-solver

# Test
curl -X POST https://my-captcha-solver.azurewebsites.net/api/solve \
  -H "Content-Type: application/json" \
  -d '{
    "method": "userrecaptcha",
    "params": {
      "googlekey": "SITE_KEY",
      "pageurl": "https://example.com"
    }
  }'

Отправка задач в очередь

Чтобы отправить пакет задач в очередь captcha-tasks, используйте QueueClient из azure-storage-queue — пример ниже ставит в очередь 10 задач подряд. На стороне CaptchaAI важно, чтобы число потоков в тарифе не отставало от параллелизма функции: если очередь одновременно вычитывают 50 экземпляров функции, а на аккаунте активен тариф BASIC ($15/мес, 5 потоков), лишние задачи будут просто ждать освободившийся поток. Для такой нагрузки подойдёт ADVANCE ($90/мес, 50 потоков) — тариф стоит сверять с реальным параллелизмом функции, а не брать с запасом наугад.

from azure.storage.queue import QueueClient
import json

queue = QueueClient.from_connection_string(
    conn_str="YOUR_STORAGE_CONNECTION_STRING",
    queue_name="captcha-tasks",
)

# Submit batch
for i in range(10):
    task = {
        "id": f"task-{i}",
        "method": "userrecaptcha",
        "params": {
            "googlekey": "SITE_KEY",
            "pageurl": f"https://example.com/page{i}",
        },
    }
    queue.send_message(json.dumps(task))
    print(f"Queued task-{i}")

Типичные проблемы и решения

Вот на что чаще всего натыкаются при развёртывании этой связки в проде и что с этим делать.

Проблема Причина Решение
Функция обрывается на 5-й минуте Тайм-аут по умолчанию в host.json Явно задайте functionTimeout, например 00:10:00
Ссылка на Key Vault возвращает пустое значение Managed identity не назначена или нет политики доступа Назначьте identity функции и добавьте политику secret get в Key Vault
Сообщения в очереди повторяются бесконечно Функция выбрасывает необработанное исключение Ловите известные ошибки, логируйте их и завершайте обработку сообщения штатно
Холодный старт дольше 10 секунд Инициализация Python-рантайма на Consumption-плане Перейдите на Premium-план или задайте FUNCTIONS_WORKER_PROCESS_COUNT
API-ключ виден в конфигурации приложения Ключ передан как обычная переменная окружения, а не через Key Vault Используйте ссылку @Microsoft.KeyVault(SecretUri=...) вместо ключа в открытом виде

Часто задаваемые вопросы

Consumption или Premium-план для функции решения CAPTCHA?

Consumption подходит для нерегулярной нагрузки — до сотни задач в день, когда простой между вызовами не критичен. Premium держит экземпляры прогретыми, убирает холодный старт и открывает доступ к VNET — берите его, если задачи идут постоянным потоком и задержка в первые секунды после простоя недопустима.

Как безопасно хранить API-ключ CaptchaAI в Azure Functions?

Не кладите ключ в local.settings.json в проде и не публикуйте его как обычную переменную окружения. Создайте секрет в Key Vault, назначьте функции managed identity и сошлитесь на секрет через @Microsoft.KeyVault(SecretUri=...) в настройках приложения — тогда ключ не хранится в коде и не виден в портале в открытом виде.

Сколько потоков CaptchaAI нужно под очередь из тысячи задач в час?

Ориентируйтесь на среднее время решения конкретного типа CAPTCHA и число одновременных экземпляров функции, а не на общее число задач в час. Если функция держит 20 задач параллельно, 20 потоков обычно достаточно с запасом — план ADVANCE (50 потоков) даёт заметный резерв на пиковую нагрузку без апгрейда на лету.

Можно ли использовать Durable Functions для сложных сценариев?

Да — Durable Functions поддерживают шаблон fan-out/fan-in: отправляете 10 CAPTCHA параллельно, а затем в одном оркестраторе собираете все токены разом. Удобно, когда результат нужен целиком, а не по одной задаче, — например перед отправкой формы, которая требует сразу нескольких решённых проверок.

Почему функция постоянно получает CAPCHA_NOT_READY при опросе res.php?

Это нормальный статус, пока CaptchaAI ещё решает задачу, — не ошибка. Проблема, если он держится дольше типичного времени решения выбранного типа CAPTCHA: тогда стоит проверить, что pageurl и site key переданы верно, и что тайм-аут функции не обрывает опрос раньше, чем задача успевает решиться.


Связанные руководства


Разверните обработку CAPTCHA на Azure — получите API-ключ CaptchaAI уже сегодня.

Комментарии для этой статьи отключены.