В этой статье я объясню, как открыть учетную запись хранения Azure через домен верхнего уровня с SSL-сертификатом Let’s Encrypt, который вы можете получить бесплатно, почти все через Terraform.

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

resource "random_string" "naming" {
  special = false
  upper   = false
  length  = 6
}

resource "azurerm_resource_group" "this" {
  name     = "blog"
  location = "westeurope"
  tags = {
    root  = "false"
    epoch = random_string.naming.id
  }
}

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

resource "azurerm_storage_account" "this" {
  name = "blog${random_string.naming.id}"
  resource_group_name = azurerm_resource_group.this.name
  location = azurerm_resource_group.this.location
  tags = azurerm_resource_group.this.tags
  account_kind = "StorageV2"
  account_tier = "Standard"
  account_replication_type = "LRS"
  static_website {
    index_document = "index.html"
    error_404_document = "404.html"
  }
}

Теперь создайте большой двоичный объект хранилища с именем, типом, типом контента и некоторыми источниками. В этой статье будет создан большой двоичный объект index.html с текстом «работает».

resource "azurerm_storage_blob" "dummy" {
  name                   = "index.html"
  storage_account_name   = azurerm_storage_account.this.name
  storage_container_name = "$web"
  type                   = "Block"
  content_type           = "text/html"
  source_content         = "It works!"
}

Сеть распространения контента (CDN)

Предоставление учетной записи хранения в Интернете под правильным именем с сертификатом имеет важное значение. Инициация профиля CDN в вашей группе ресурсов — это самый простой SKU. Мы будем использовать стандартный Microsoft SKU, потому что он самый простой. Вы также можете использовать другие CDN из Azure, но они не входят в эту статью.

resource "azurerm_cdn_profile" "this" {
  name = "${azurerm_resource_group.this.name}-cdn"
  resource_group_name = azurerm_resource_group.this.name
  location = azurerm_resource_group.this.location
  tags = azurerm_resource_group.this.tags
  sku = "Standard_Microsoft"
}

Получив профиль CDN, мы должны получить в нем конечные точки CDN. Заголовок узла Origin с основного веб-узла вашей учетной записи хранения. Вы также можете указать имя хоста в блоке Origin. Это прокси-сервер маршрутизации и кэширования для вашей учетной записи хранения, где вы определяете правила доставки того, как переписываются ваши URL-адреса. Единственное определенное правило будет применять HTTPS и только протокол HTTPS для вашего веб-сайта. Пожалуйста, обратитесь к документации для более подробной информации по этому вопросу.

resource "azurerm_cdn_endpoint" "this" {
  name                = "edge-${random_string.naming.id}"
  profile_name        = azurerm_cdn_profile.this.name
  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name
  origin_host_header  = azurerm_storage_account.this.primary_web_host

  origin {
    name      = "origin"
    host_name = azurerm_storage_account.this.primary_web_host
  }

  delivery_rule {
    name  = "EnforceHTTPS"
    order = 1

    request_scheme_condition {
      operator     = "Equal"
      match_values = ["HTTP"]
    }

    url_redirect_action {
      redirect_type = "Found"
      protocol      = "Https"
    }
  }

  delivery_rule {
    name  = "cache"
    order = 2

    cache_expiration_action {
      behavior = "SetIfMissing"
      duration = "1.00:00:00"
    }

    url_file_extension_condition {
      match_values = [
        "css",
        "jpeg",
        "jpg",
        "js",
        "webp",
        "woff2"
      ]
      negate_condition = false
      operator         = "EndsWith"
    }
  }
}

Теперь вам нужно определить зону DNS с доменным именем, которое будет видно вашему сайту. Я использовал Google Domains в качестве службы покупки домена для этой статьи, потому что вы не могли купить домен через Azure. По крайней мере, я не смог найти это на портале Azure. Нам нужно получить из него список и зайти в консоль Google Domains для обновления серверов имён на несколько минут.

resource "azurerm_dns_zone" "this" {
  name = var.domain_name
  resource_group_name = azurerm_resource_group.this.name
  tags = azurerm_resource_group.this.tags
}

Направление домена Apex на Azure CDN

Если у вас есть доменное имя верхнего уровня, например ssmertin.com, вы не можете создать для него запись CNAME в DNS. A-запись — это единственный способ назначить удобочитаемое имя хоста для вашей конечной точки CDN. Вы указываете запись в своей зоне DNS через идентификатор целевого ресурса на идентификатор созданной вами конечной точки CDN. Вы должны использовать @ в качестве имени. Но для привязки Azure CDN к вашему профилю CDN требуется нечто большее, чем просто указание идентификатора ресурса через A-запись.

resource "azurerm_dns_a_record" "cdn" {
  name                = "@"
  zone_name           = azurerm_dns_zone.this.name
  resource_group_name = azurerm_resource_group.this.name
  target_resource_id  = azurerm_cdn_endpoint.this.id
  ttl                 = 300
}

Необходимо создать запись CNAME с конкретным именем cdnverify в качестве поддомена для конечной точки CDN, чтобы Azure CDN понимал этот домен.

resource "azurerm_dns_cname_record" "cdnverify" {
  name                = "cdnverify"
  zone_name           = azurerm_dns_zone.this.name
  resource_group_name = azurerm_resource_group.this.name
  record              = "cdnverify.${azurerm_cdn_endpoint.this.fqdn}"
  ttl                 = 300
}

Чтобы подключить Azure CDN к домену, вам нужно было создать личный домен для конечной точки Azure CDN, который зависит от записи CNAME для псевдонима cdnverify. В этой статье мы собираемся остановиться на HTTPS, управляемом пользователем. Мы хотим сертификат от Let’s Encrypt и используем его где-то еще, кроме сайта. А чтобы использовать управляемый пользователем HTTPS, у вас должен быть секрет Azure Key Vault. И я настоятельно рекомендую использовать секретный идентификатор без версии, чтобы вы меньше беспокоились о времени простоя во время обновления сертификата.

resource "azurerm_cdn_endpoint_custom_domain" "this" {
  name            = replace(azurerm_dns_zone.this.name, ".", "-")
  cdn_endpoint_id = azurerm_cdn_endpoint.this.id
  host_name       = azurerm_dns_zone.this.name

  user_managed_https {
    key_vault_secret_id = azurerm_key_vault_certificate.this.versionless_secret_id
  }

  depends_on = [
    azurerm_dns_cname_record.cdnverify
  ]
}

Предоставление сертификатов CDN

Давайте продолжим и создадим Azure Key Vault. Вам потребуется группа ресурсов и идентификатор арендатора, чтобы создать Azure Key Vault через Terraform. Добавьте списки управления доступом к сети, чтобы разрешить весь трафик из служб Azure и запретить любой трафик, кроме нашего IP-адреса. Кроме того, определите две политики доступа. Первый будет для вашего пользователя, который может делать практически все что угодно с ключами, секретами и сертификатами. Другая политика предназначена для приложения CDN, которому необходимо считывать секреты и сертификаты из этого конкретного Key Vault.

data "azurerm_client_config" "current" {}

data "http" "ip" {
  url = "https://ifconfig.me/ip"
}

resource "azurerm_key_vault" "this" {
  name                       = "kv-${random_string.naming.id}"
  location                   = azurerm_resource_group.this.location
  resource_group_name        = azurerm_resource_group.this.name
  tenant_id                  = data.azurerm_client_config.current.tenant_id
  sku_name                   = "standard"
  soft_delete_retention_days = 7

  network_acls {
    bypass         = "AzureServices"
    default_action = "Deny"
    ip_rules       = [data.http.ip.response_body]
  }

  access_policy {
    tenant_id = data.azurerm_client_config.current.tenant_id
    object_id = data.azurerm_client_config.current.object_id
    key_permissions = ["Create", "Delete", "Get", "Import",
    "List", "Sign", "Update", "Verify", "Rotate"]
    secret_permissions  = ["Delete", "Get", "List", "Set"]
    storage_permissions = ["Delete", "Get", "List", "Set", "Update"]
    certificate_permissions = ["Create", "Delete", "Get", "Import",
    "List", "Update", "Purge", "Recover"]
  }

  access_policy {
    tenant_id               = data.azurerm_client_config.current.tenant_id
    object_id               = azuread_service_principal.azure_cdn.object_id
    certificate_permissions = ["Get"]
    secret_permissions      = ["Get"]
  }
}

Вам необходимо зарегистрировать приложение Azure CDN в Active Directory. Это не работает из коробки для чего-то, что является нативным для платформы. Вы можете сделать это через ресурс субъекта-службы от поставщика azuread terraform. Вы можете получить идентификатор приложения с другого веб-сайта, если не доверяете этой статье.

resource "azuread_service_principal" "azure_cdn" {
  application_id = "205478c0-bd83-4e1b-a9d6-db63a3e1e1c8"
}

К слову о сервис-менеджерах. Нам нужно иметь еще один для пользователя, который мог бы установить TXT-запись в нашей зоне DNS. Мы хотим максимально ограничить разрешения для этого субъекта-службы. Поэтому мы создаем для него пользовательское определение роли IAM и назначаем это определение роли в области Azure DNS для этого конкретного субъекта-службы. Помните, что ваша роль azuread_custom_directory_role отличается от того, что вам нужно. Согласно вашему определению роли IAM, вам нужны назначаемые области для всей группы ресурсов.

resource "azurerm_role_definition" "letsencrypt" {
  name        = "Letsencrypt Contributor"
  description = "This custom role allows managing DNS TXT records"
  scope       = azurerm_resource_group.this.id

  permissions {
    actions = [
      "Microsoft.Network/dnsZones/TXT/*",
      "Microsoft.Network/dnsZones/read",
      "Microsoft.Authorization/*/read",
      "Microsoft.ResourceHealth/availabilityStatuses/read",
      "Microsoft.Resources/deployments/read",
      "Microsoft.Resources/subscriptions/resourceGroups/read"
    ]
    not_actions = []
  }

  assignable_scopes = [
    azurerm_resource_group.this.id
  ]
}

resource "azurerm_role_assignment" "update_txt_record" {
  scope              = azurerm_dns_zone.this.id
  role_definition_id = azurerm_role_definition.letsencrypt.role_definition_resource_id
  principal_id       = azuread_service_principal.letsencrypt.object_id
}

Очевидно, что оно будет раскрыто при регистрации приложения портала Azure по крайней мере в 2023 году, поэтому перед созданием субъекта-службы вам необходимо создать приложение Active Directory с именем по вашему выбору.

resource "azuread_application" "letsencrypt" {
  display_name = "letsencrypt"
}

resource "azuread_service_principal" "letsencrypt" {
  application_id = azuread_application.letsencrypt.application_id
}

resource "time_rotating" "monthly" {
  rotation_days = 30
}

resource "azuread_service_principal_password" "letsencrypt" {
  service_principal_id = azuread_service_principal.letsencrypt.object_id
  rotate_when_changed = {
    rotation = time_rotating.monthly.id
  }
}

ACME Терраформ-провайдер

Для этой статьи мы максимально используем Terraform. Лучше всего использовать провайдера ACME для использования центрального органа Let’s Encrypt.

terraform {
  required_providers {
    acme = {
      source  = "vancluever/acme"
      version = "~> 2.0"
    }
  }
}

provider "acme" {
  // don't use staging endpoint, as it obviously won't work with AKV
  server_url = "https://acme-v02.api.letsencrypt.org/directory"
}

Прежде чем вы сможете что-либо делать с центральным органом Let’s Encrypt, вам необходимо зарегистрироваться. Для регистрации необходимо создать закрытый ключ с помощью поставщика TLS Terraform. Имейте в виду, что закрытый ключ будет храниться в вашем состоянии Terraform. Обеспечьте безопасный доступ к состоянию Terraform и не передавайте его в какое-либо легкодоступное место, кроме вашего внутреннего круга доверия. Вы можете навлечь на себя неприятности.

resource "tls_private_key" "private_key" {
  algorithm = "RSA"
}

resource "acme_registration" "me" {
  account_key_pem = tls_private_key.private_key.private_key_pem
  email_address   = var.email_for_renewal_alerts
}

Как только вы зарегистрируете свой адрес электронной почты в центральном органе Let’s Encrypt с закрытым ключом, вы можете запрашивать SSL-сертификаты по протоколу ACME. Мы будем использовать протокол запроса DNS и субъект-службу, которую мы только что создали, и предоставили переменные среды через атрибут config. Вы можете спросить: зачем мне создавать субъект-службу? Может ли он авторизоваться с помощью учетных данных, которые я уже использую для изменения этой зоны DNS? К сожалению, поставщик ACME не использует те же переменные среды, что и ваш azurerm: AZURE_CLIENT_ID по сравнению с ARM_CLIENT_ID. Поэтому вы должны быть очень творческими и очень явными с учетными данными безопасности, которые вы используете. Провайдер ACME использует библиотеку LEGO для работы с протоколом ACME для получения сертификатов от Let’s Encrypt.

resource "acme_certificate" "certificate" {
  account_key_pem = acme_registration.me.account_key_pem
  common_name     = azurerm_dns_zone.this.name

  depends_on = [
    azurerm_role_assignment.update_txt_record
  ]

  dns_challenge {
    provider = "azure"
    config = {
      AZURE_TENANT_ID       = data.azurerm_client_config.current.tenant_id
      AZURE_CLIENT_ID       = azuread_application.letsencrypt.application_id
      AZURE_CLIENT_SECRET   = azuread_service_principal_password.letsencrypt.value
      AZURE_SUBSCRIPTION_ID = data.azurerm_client_config.current.subscription_id
      AZURE_RESOURCE_GROUP  = azurerm_resource_group.this.name
    }
  }
}

Получение сертификата от центра Let’s Encrypt может занять минуту, но потом его нужно где-то хранить. Требуется Key Vault. Потому что как еще CDN узнает о ваших самых последних сертификатах Let’s Encrypt?

resource "azurerm_key_vault_certificate" "this" {
  name         = replace(azurerm_dns_zone.this.name, ".", "-")
  key_vault_id = azurerm_key_vault.this.id

  certificate {
    contents = acme_certificate.certificate.certificate_p12
  }
}

Кроме того, вы можете запланировать Terraform ежедневно/еженедельно/ежемесячно для автоматического обновления сертификатов Let’s Encrypt. Для этого следует создать выделенный субъект-службу Azure. Какие разрешения вам понадобятся? Прежде всего, субъект-служба должен иметь возможность считывать все ресурсы в этой конкретной группе ресурсов.

В противном случае чтение из файла состояния terraform завершится ошибкой. Затем субъект-служба должен иметь возможность изменять SSL-сертификаты в вашем Key Vault, а вы должны иметь возможность изменять записи TXT в своей зоне DNS.

Помните, что состояние Terraform содержит конфиденциальную информацию, такую ​​как ваш частный сертификат TLS, который вы используете для запроса новых сертификатов от Let's Encrypt, сам сертификат Let's Encrypt и пароль для субъекта-службы Active Directory, который вы создали для изменять записи в зоне DNS.

Смотрите также