Развертывание веб-приложения Amazon Web Services (AWS) для Azure

В этой статье описано, как развернуть приложение Yelb в кластере Azure Kubernetes Service (AKS), созданном в статье previous.

Проверка среды

Перед развертыванием приложения убедитесь, что кластер AKS настроен правильно с помощью следующих команд:

  1. Список пространств имен в вашем кластере можно получить с помощью команды kubectl get namespace.

    kubectl get namespace
    

    Если вы установили контроллер доступа NGINX с помощью надстройки маршрутизации приложений, вы увидите пространство имен app-routing-system в выводе:

    NAME                 STATUS   AGE
    app-routing-system   Active   4h28m
    cert-manager         Active   109s
    dapr-system          Active   4h18m
    default              Active   4h29m
    gatekeeper-system    Active   4h28m
    kube-node-lease      Active   4h29m
    kube-public          Active   4h29m
    kube-system          Active   4h29m
    

    Если вы установили контроллер входящего трафика NGINX через Helm, вы увидите ingress-basic пространство имен в выходных данных:

    NAME                STATUS   AGE
    cert-manager        Active   7m42s
    dapr-system         Active   11m
    default             Active   21m
    gatekeeper-system   Active   20m
    ingress-basic       Active   7m19s
    kube-node-lease     Active   21m
    kube-public         Active   21m
    kube-system         Active   21m
    prometheus          Active   8m9s
    
  2. Получите сведения о службе app-routing-system или пространстве имен ingress-basic с помощью kubectl get service command.

    kubectl get service --namespace <namespace-name> -o wide
    

    Если вы использовали надстройку маршрутизации приложений, вы увидите, что EXTERNAL-IP службы nginx является частным IP-адресом. Этот адрес является частным IP-адресом конфигурации фронтенд IP в kubernetes-internal частной системе балансировки нагрузки вашего кластера AKS.

    NAME    TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                      AGE     SELECTOR
    nginx   LoadBalancer   172.16.55.104   10.240.0.7    80:31447/TCP,443:31772/TCP,10254:30459/TCP   4h28m   app=nginx
    

    Если вы использовали Helm, вы должны увидеть, что EXTERNAL-IP для службы nginx-ingress-ingress-nginx-controller — это частный IP-адрес. Этот адрес является частным IP-адресом конфигурации внешнего IP-адреса в kubernetes-internal частной подсистеме балансировки нагрузки кластера AKS.

    NAME                                               TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
    nginx-ingress-ingress-nginx-controller             LoadBalancer   172.16.42.152    10.240.0.7    80:32117/TCP,443:32513/TCP   7m31s
    nginx-ingress-ingress-nginx-controller-admission   ClusterIP      172.16.78.85     <none>        443/TCP                      7m31s
    nginx-ingress-ingress-nginx-controller-metrics     ClusterIP      172.16.109.138   <none>        10254/TCP                    7m31s
    

Подготовка к развертыванию приложения Yelb

Если вы хотите развернуть пример, используя завершение TLS на шлюзе приложений и вызов Yelb через HTTP, можно найти сценарии Bash и шаблоны YAML для развертывания приложения Yelb в папке http.

Если вы хотите развернуть пример, используя архитектуру Implementing end-to-end TLS с Шлюз приложений Azure, вы можете найти Bash-скрипты и YAML-шаблоны для развертывания веб-приложения в папке https.

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

Настройка переменных

  1. Перед выполнением любых скриптов необходимо настроить значения переменных в 00-variables.sh файле. Этот файл включается во все скрипты и содержит следующие переменные:

    # Azure subscription and tenant
    RESOURCE_GROUP_NAME="<aks-resource-group>"
    SUBSCRIPTION_ID="$(az account show --query id --output tsv)"
    SUBSCRIPTION_NAME="$(az account show --query name --output tsv)"
    TENANT_ID="$(az account show --query tenantId --output tsv)"
    AKS_CLUSTER_NAME="<aks-name>"
    AGW_NAME="<application-gateway-name>"
    AGW_PUBLIC_IP_NAME="<application-gateway-public-ip-name>"
    DNS_ZONE_NAME="<your-azure-dns-zone-name-eg-contoso.com>"
    DNS_ZONE_RESOURCE_GROUP_NAME="<your-azure-dns-zone-resource-group-name>"
    DNS_ZONE_SUBSCRIPTION_ID="<your-azure-dns-zone-subscription-id>"
    
    # NGINX ingress controller installed via Helm
    NGINX_NAMESPACE="ingress-basic"
    NGINX_REPO_NAME="ingress-nginx"
    NGINX_REPO_URL="https://kubernetes.github.io/ingress-nginx"
    NGINX_CHART_NAME="ingress-nginx"
    NGINX_RELEASE_NAME="ingress-nginx"
    NGINX_REPLICA_COUNT=3
    
    # Specify the ingress class name for the ingress controller
    # - nginx: Unmanaged NGINX ingress controller installed via Helm
    # - webapprouting.kubernetes.azure.com: Managed NGINX ingress controller installed via AKS application routing add-on
    INGRESS_CLASS_NAME="webapprouting.kubernetes.azure.com"
    
    # Subdomain of the Yelb UI service
    SUBDOMAIN="<yelb-application-subdomain>"
    
    # URL of the Yelb UI service
    URL="https://$SUBDOMAIN.$DNS_ZONE_NAME"
    
    # Secret provider class
    KEY_VAULT_NAME="<key-vault-name>"
    KEY_VAULT_CERTIFICATE_NAME="<key-vault-resource-group-name>"
    KEY_VAULT_SECRET_PROVIDER_IDENTITY_CLIENT_ID="<key-vault-secret-provider-identity-client-id>"
    TLS_SECRET_NAME="yelb-tls-secret"
    NAMESPACE="yelb"
    
  2. Следующую команду az aks show можно выполнить, чтобы получить clientId управляемого удостоверения, назначаемого пользователем, используемого в поставщике Azure Key Vault для драйвера CSI хранилища секретов. Назначить управляемому удостоверению, назначаемому пользователем, для надстройки роль keyVault.bicepKey Vault Administrator, чтобы оно могло получить сертификат, используемый Kubernetes Ingress для публикации службы yelb-ui через контроллер ingress NGINX.

    az aks show \
      --name <aks-name> \
      --resource-group <aks-resource-group-name> \
      --query addonProfiles.azureKeyvaultSecretsProvider.identity.clientId \
      --output tsv \
      --only-show-errors
    
  3. Если вы развернули инфраструктуру Azure с помощью модулей Bicep, предоставленных в этом примере, вы можете приступить к развертыванию приложения Yelb. Если вы хотите развернуть приложение в кластере AKS, можно использовать следующие сценарии для настройки среды. Для установки 02-create-nginx-ingress-controller.sh с включенным брандмауэром веб-приложений с открытым исходным кодом ModSecurity (WAF) можно использовать .

    #!/bin/bash
    
    # Variables
    source ./00-variables.sh
    
    # Check if the NGINX ingress controller Helm chart is already installed
    result=$(helm list -n $NGINX_NAMESPACE | grep $NGINX_RELEASE_NAME | awk '{print $1}')
    
    if [[ -n $result ]]; then
      echo "[$NGINX_RELEASE_NAME] NGINX ingress controller release already exists in the [$NGINX_NAMESPACE] namespace"
    else
      # Check if the NGINX ingress controller repository is not already added
      result=$(helm repo list | grep $NGINX_REPO_NAME | awk '{print $1}')
    
      if [[ -n $result ]]; then
        echo "[$NGINX_REPO_NAME] Helm repo already exists"
      else
        # Add the NGINX ingress controller repository
        echo "Adding [$NGINX_REPO_NAME] Helm repo..."
        helm repo add $NGINX_REPO_NAME $NGINX_REPO_URL
      fi
    
      # Update your local Helm chart repository cache
      echo 'Updating Helm repos...'
      helm repo update
    
      # Deploy NGINX ingress controller
      echo "Deploying [$NGINX_RELEASE_NAME] NGINX ingress controller to the [$NGINX_NAMESPACE] namespace..."
      helm install $NGINX_RELEASE_NAME $NGINX_REPO_NAME/$nginxChartName \
        --create-namespace \
        --namespace $NGINX_NAMESPACE \
        --set controller.nodeSelector."kubernetes\.io/os"=linux \
        --set controller.replicaCount=$NGINX_REPLICA_COUNT \
        --set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \
        --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz
    fi
    
    # Get values
    helm get values $NGINX_RELEASE_NAME --namespace $NGINX_NAMESPACE
    

Развертывание приложения

  1. Выполните следующий 03-deploy-yelb.sh сценарий, чтобы развернуть приложение Yelb и объект Kubernetes Ingress , чтобы сделать yelb-ui службу доступной для общедоступного Интернета.

    #!/bin/bash
    
    # Variables
    source ./00-variables.sh
    
    # Check if namespace exists in the cluster
    result=$(kubectl get namespace -o jsonpath="{.items[?(@.metadata.name=='$NAMESPACE')].metadata.name}")
    
    if [[ -n $result ]]; then
      echo "$NAMESPACE namespace already exists in the cluster"
    else
      echo "$NAMESPACE namespace does not exist in the cluster"
      echo "creating $NAMESPACE namespace in the cluster..."
      kubectl create namespace $NAMESPACE
    fi
    
    # Create the Secret Provider Class object
    echo "Creating the secret provider class object..."
    cat <<EOF | kubectl apply -f -
    apiVersion: secrets-store.csi.x-k8s.io/v1
    kind: SecretProviderClass
    metadata:
      namespace: $NAMESPACE
      name: yelb
    spec:
      provider: azure
      secretObjects:
        - secretName: $TLS_SECRET_NAME
          type: kubernetes.io/tls
          data: 
            - objectName: $KEY_VAULT_CERTIFICATE_NAME
              key: tls.key
            - objectName: $KEY_VAULT_CERTIFICATE_NAME
              key: tls.crt
      parameters:
        usePodIdentity: "false"
        useVMManagedIdentity: "true"
        userAssignedIdentityID: $KEY_VAULT_SECRET_PROVIDER_IDENTITY_CLIENT_ID
        keyvaultName: $KEY_VAULT_NAME
        objects: |
          array:
            - |
              objectName: $KEY_VAULT_CERTIFICATE_NAME
              objectType: secret
        tenantId: $TENANT_ID
    EOF
    
    # Apply the YAML configuration
    kubectl apply -f yelb.yml
    
    echo "waiting for secret $TLS_SECRET_NAME in namespace $namespace..."
    
    while true; do
      if kubectl get secret -n $NAMESPACE $TLS_SECRET_NAME >/dev/null 2>&1; then
        echo "secret $TLS_SECRET_NAME found!"
        break
      else
        printf "."
        sleep 3
      fi
    done
    
    # Create chat-ingress
    cat ingress.yml |
      yq "(.spec.ingressClassName)|="\""$INGRESS_CLASS_NAME"\" |
      yq "(.spec.tls[0].hosts[0])|="\""$SUBDOMAIN.$DNS_ZONE_NAME"\" |
      yq "(.spec.tls[0].secretName)|="\""$TLS_SECRET_NAME"\" |
      yq "(.spec.rules[0].host)|="\""$SUBDOMAIN.$DNS_ZONE_NAME"\" |
      kubectl apply -f -
    
    # Check the deployed resources within the yelb namespace:
    kubectl get all -n yelb
    
  2. Обновите манифест YAML yelb-ui, чтобы включить определение csi volume и volume mount для считывания сертификата как секрета из Azure Key Vault.

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: yelb
  name: yelb-ui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: yelb-ui
      tier: frontend
  template:
    metadata:
      labels:
        app: yelb-ui
        tier: frontend
    spec:
      containers:
        - name: yelb-ui
          image: mreferre/yelb-ui:0.7
          ports:
            - containerPort: 80
          volumeMounts:
            - name: secrets-store-inline
              mountPath: "/mnt/secrets-store"
              readOnly: true
      volumes:
        - name: secrets-store-inline
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: yelb
  1. Теперь вы можете развертывать приложение. Скрипт использует yelb.yml манифест YAML для развертывания приложения и ingress.yml для создания объекта ingress. Если вы используете Azure общедоступную зону DNS для разрешения доменных имен, можно использовать скрипт 04-configure-dns.sh. Этот скрипт связывает общедоступный IP-адрес контроллера входящего трафика NGINX с доменом, используемым объектом входящего трафика, который предоставляет yelb-ui службу. Скрипт выполняет следующие задачи.

    1. Извлекает общедоступный IP-адрес Azure, используемый фронтальной конфигурацией IP-адреса в шлюзе приложений.
    2. Проверяет, существует ли A запись для поддомена, используемого службой yelb-ui.
    3. A Если запись не существует, скрипт создает его.
source ./00-variables.sh

# Get the address of the Application Gateway Public IP
echo "Retrieving the address of the [$AGW_PUBLIC_IP_NAME] public IP address of the [$AGW_NAME] Application Gateway..."
PUBLIC_IP_ADDRESS=$(az network public-ip show \
    --resource-group $RESOURCE_GROUP_NAME \
    --name $AGW_PUBLIC_IP_NAME \
    --query ipAddress \
    --output tsv \
    --only-show-errors)
if [[ -n $PUBLIC_IP_ADDRESS ]]; then
    echo "[$PUBLIC_IP_ADDRESS] public IP address successfully retrieved for the [$AGW_NAME] Application Gateway"
else
    echo "Failed to retrieve the public IP address of the [$AGW_NAME] Application Gateway"
    exit
fi
# Check if an A record for todolist subdomain exists in the DNS Zone
echo "Retrieving the A record for the [$SUBDOMAIN] subdomain from the [$DNS_ZONE_NAME] DNS zone..."
IPV4_ADDRESS=$(az network dns record-set a list \
    --zone-name $DNS_ZONE_NAME \
    --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
    --subscription $DNS_ZONE_SUBSCRIPTION_ID \
    --query "[?name=='$SUBDOMAIN'].ARecords[].IPV4_ADDRESS" \
    --output tsv \
    --only-show-errors)
if [[ -n $IPV4_ADDRESS ]]; then
    echo "An A record already exists in [$DNS_ZONE_NAME] DNS zone for the [$SUBDOMAIN] subdomain with [$IPV4_ADDRESS] IP address"
    if [[ $IPV4_ADDRESS == $PUBLIC_IP_ADDRESS ]]; then
        echo "The [$IPV4_ADDRESS] ip address of the existing A record is equal to the ip address of the ingress"
        echo "No additional step is required"
        continue
    else
        echo "The [$IPV4_ADDRESS] ip address of the existing A record is different than the ip address of the ingress"
    fi
    # Retrieving name of the record set relative to the zone
    echo "Retrieving the name of the record set relative to the [$DNS_ZONE_NAME] zone..."
    RECORDSET_NAME=$(az network dns record-set a list \
        --zone-name $DNS_ZONE_NAME \
        --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
        --subscription $DNS_ZONE_SUBSCRIPTION_ID \
        --query "[?name=='$SUBDOMAIN'].name" \
        --output tsv \
        --only-show-errors 2>/dev/null)
    if [[ -n $RECORDSET_NAME ]]; then
        echo "[$RECORDSET_NAME] record set name successfully retrieved"
    else
        echo "Failed to retrieve the name of the record set relative to the [$DNS_ZONE_NAME] zone"
        exit
    fi
    # Remove the A record
    echo "Removing the A record from the record set relative to the [$DNS_ZONE_NAME] zone..."
    az network dns record-set a remove-record \
        --ipv4-address $IPV4_ADDRESS \
        --record-set-name $RECORDSET_NAME \
        --zone-name $DNS_ZONE_NAME \
        --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
        --subscription $DNS_ZONE_SUBSCRIPTION_ID \
        --only-show-errors 1>/dev/null
    if [[ $? == 0 ]]; then
        echo "[$IPV4_ADDRESS] ip address successfully removed from the [$RECORDSET_NAME] record set"
    else
        echo "Failed to remove the [$IPV4_ADDRESS] ip address from the [$RECORDSET_NAME] record set"
        exit
    fi
fi
# Create the A record
echo "Creating an A record in [$DNS_ZONE_NAME] DNS zone for the [$SUBDOMAIN] subdomain with [$PUBLIC_IP_ADDRESS] IP address..."
az network dns record-set a add-record \
    --zone-name $DNS_ZONE_NAME \
    --resource-group $DNS_ZONE_RESOURCE_GROUP_NAME \
    --subscription $DNS_ZONE_SUBSCRIPTION_ID \
    --record-set-name $SUBDOMAIN \
    --ipv4-address $PUBLIC_IP_ADDRESS \
    --only-show-errors 1>/dev/null
if [[ $? == 0 ]]; then
    echo "A record for the [$SUBDOMAIN] subdomain with [$PUBLIC_IP_ADDRESS] IP address successfully created in [$DNS_ZONE_NAME] DNS zone"
else
    echo "Failed to create an A record for the $SUBDOMAIN subdomain with [$PUBLIC_IP_ADDRESS] IP address in [$DNS_ZONE_NAME] DNS zone"
fi

Примечание.

Перед развертыванием приложения Yelb и созданием объекта ingress скрипт создает SecretProviderClass, чтобы получить сертификат TLS из Azure Key Vault и создать секрет Kubernetes для объекта ingress. Важно отметить, что драйвер CSI Secrets Store для Key Vault создает секрет Kubernetes, содержащий сертификат TLS, только когда SecretProviderClass и определение тома включены в deployment. Чтобы сертификат TLS правильно извлекается из Azure Key Vault и хранится в секрете Kubernetes, используемом объектом ingress, необходимо внести следующие изменения в манифест YAML развертывания yelb-ui:

  • Добавьте определение csi volume с помощью драйвера secrets-store.csi.k8s.io, который ссылается на объект SecretProviderClass, отвечающий за получение сертификата TLS из Azure Key Vault.
  • Включите volume mount для чтения сертификата в виде секрета из Azure Key Vault.

Дополнительные сведения см. в разделе "Настройка драйвера CSI хранилища секретов", чтобы включить контроллер входящего трафика NGINX с помощью TLS.

Тестирование приложения

05-call-yelb-ui.sh Используйте скрипт для вызова yelb-ui службы, имитации внедрения SQL, атак XSS и отслеживания того, как управляемый набор правил ModSecurity блокирует вредоносные запросы.

#!/bin/bash
# Variables
source ./00-variables.sh
# Call REST API
echo "Calling Yelb UI service at $URL..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL
# Simulate SQL injection
echo "Simulating SQL injection when calling $URL..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL/?users=ExampleSQLInjection%27%20--
# Simulate XSS
echo "Simulating XSS when calling $URL..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL/?users=ExampleXSS%3Cscript%3Ealert%28%27XSS%27%29%3C%2Fscript%3E
# A custom rule blocks any request with the word blockme in the querystring.
echo "Simulating query string manipulation with the 'blockme' word in the query string..."
curl -w 'HTTP Status: %{http_code}\n' -s -o /dev/null $URL/?users?task=blockme

Скрипт Bash должен создать следующие выходные данные, где первый вызов успешно выполнен, а правила ModSecurity блокируют следующие два вызова:

Calling Yelb UI service at https://yelb.contoso.com...
HTTP Status: 200
Simulating SQL injection when calling https://yelb.contoso.com...
HTTP Status: 403
Simulating XSS when calling https://yelb.contoso.com...
HTTP Status: 403
Simulating query string manipulation with the 'blockme' word in the query string...
HTTP Status: 403

Мониторинг приложения

В предлагаемом решении процесс развертывания автоматически настраивает ресурс Шлюз приложений Azure для сбора журналов диагностики и метрик в рабочую область Azure Log Analytics. Включив журналы, вы можете получить ценные сведения об оценках, совпадениях и блоках, выполняемых Брандмауэр веб-приложений Azure (WAF) в шлюзе приложений. Дополнительные сведения см. в журналах диагностики шлюза приложений. Вы также можете использовать Log Analytics для проверки данных в журналах брандмауэра. Если у вас есть журналы брандмауэра в рабочей области Log Analytics, вы можете просматривать данные, записывать запросы, создавать визуализации и добавлять их на панель мониторинга портала. Подробные сведения о запросах журнала см. в разделе Обзор запросов журнала в Azure Monitor.

Изучение данных с помощью запросов Kusto

В предлагаемом решении процесс развертывания автоматически настраивает ресурс Шлюз приложений Azure для сбора журналов диагностики и метрик в рабочую область Azure Log Analytics. Включив журналы, вы можете получить сведения об оценках, совпадениях и блоках, выполняемых Брандмауэр веб-приложений Azure (WAF) в шлюзе приложений. Дополнительные сведения см. в журналах диагностики шлюза приложений.

Вы также можете использовать Log Analytics для проверки данных в журналах брандмауэра. Если у вас есть журналы брандмауэра в рабочей области Log Analytics, вы можете просматривать данные, записывать запросы, создавать визуализации и добавлять их на панель мониторинга портала. Дополнительные сведения о запросах журнала см. раздел Обзор запросов журнала в Azure Monitor.

AzureDiagnostics 
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| limit 10

Кроме того, при работе с таблицей для конкретного ресурса можно получить доступ к необработанным данным журнала брандмауэра с помощью следующего запроса. Дополнительные сведения о таблицах, относящихся к ресурсам, см. в справочной документации по данным мониторинга .

AGWFirewallLogs
| limit 10

После получения данных можно углубиться и создать графы или визуализации. Ниже приведены некоторые дополнительные примеры запросов KQL , которые можно использовать:

Совпадающие или заблокированные запросы по IP-адресу

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize count() by clientIp_s, bin(TimeGenerated, 1m)
| render timechart

Совпадающие или заблокированные запросы по URI

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize count() by requestUri_s, bin(TimeGenerated, 1m)
| render timechart

Топ сопоставленных правил

| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize count() by ruleId_s, bin(TimeGenerated, 1m)
| where count_ > 10
| render timechart

Пять основных сопоставленных групп правил

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "ApplicationGatewayFirewallLog"
| summarize Count=count() by details_file_s, action_s
| top 5 by Count desc
| render piechart

Проверка развернутых ресурсов

Можно использовать Azure CLI или Azure PowerShell для перечисления развернутых ресурсов в группе ресурсов.

Перечислить развернутые ресурсы в группе ресурсов с помощью команды [az resource list][az-resource-list].

az resource list --resource-group <resource-group-name>

Вы можете использовать Azure CLI или Azure PowerShell для удаления группы ресурсов, если вам больше не нужны ресурсы, созданные в этом руководстве.

Удалите группу ресурсов и связанные с ней az group delete ресурсы с помощью команды.

az group delete --name <resource-group-name>

Дальнейшие действия

Вы можете повысить безопасность и защиту от угроз решения с помощью Azure защиты от атак DDoS и Брандмауэр Azure. Дополнительные сведения см. в следующих статьях:

Если вы используете контроллер входящего трафика NGINX или любой другой размещенный контроллер входящего трафика AKS вместо Шлюз приложений Azure, вы можете использовать Брандмауэр Azure для проверки трафика в кластер AKS и защиты кластера от кражи данных и другого нежелательного сетевого трафика. Дополнительные сведения см. в следующих статьях:

Соавторы

Microsoft поддерживает эту статью. Следующие авторы изначально написали его:

Основной автор:

Другие участники: