В этой статье я объясню, как открыть учетную запись хранения 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.