Skip to content Skip to sidebar Skip to footer

Terraform Azure Upload All Files to Bulk Upload

tl;dr, I don't desire to remember I want to re-create paste

I wanted to migrate my blog off of Google's services for a few reasons. One, since I am seeing more employ of Azure in data-related spaces, I wanted to build some familiarity with their offerings. Two, I wanted to larn Terraform. And three, I was really upset about how Google handled the situation with Timnit Gebru.

Firebase Hosting is a perfectly reasonable hosting solution for a static site, such as this i. In fact, I'd contend that it's among the easiest static site hosting solutions aside from Github Pages. So if you lot're looking for something simple, I'd really recommend just choosing 1 of those solutions, rather than messing around with cloud infrastructure. Nevertheless, I wanted something a little more integrated. In add-on to my blog, I run a number of other servies that I'd like to eventually motion into Azure. So I decided to invest a day to learn the ins and outs of Azure and Terraform, and now I am pleased to put together this guide for how to do information technology yourself, if you're and so inclined.

When possible, I am sticking with the concepts of Infrastructure as Code and Continuous Delivery. Many like guides out there will walk yous through using the Azure portal. Since I was learning as I was going, I did have to use the Portal frequently, but when possible tried migrating those actions dorsum into Terraform. I believe everything should work equally I present information technology hither, but do note that it can be tedious to tear things down and stand them support once again, so information technology'south possible in that location might be some problems along the way. If y'all do detect any, please electronic mail me (ejgorcenski@gmail.com)!

Overview

My goal is to migrate an existing site with the following backdrop:

  • host a static site at a custom domain (due east.thou. emilygorcenski.com);
  • ensure that the site can exist be served over https;
  • force redirection from http to https;
  • ensure that both the noon domain (emilygorcenski.com) and the world wide web subdomain both work over https;
  • automatically update the site on push to source command.

Almost all of these things are fairly easy to practice, except for when information technology comes to implementing TLS. There are a few challenges hither. The kickoff challenge is handling both http and https traffic with a custom domain. The second challenge is enabling a certificant to work for the apex domain. If this was a new site, we could simplify this greatly by forcing everything to a www subdomain. Nevertheless, since this is a migration, and since there are links to the blog not using the world wide web subdomain, this becomes a niggling bit more complicated.

To brand everything work, we're going to proceed with these steps:

  1. Prepare tools and accounts;
  2. Prepare an Azure Storage Account for a static site;
  3. Prepare up an Azure CDN to terminate TLS and handle http to https redirection;
  4. Set up upwards an Azure DNS zone;
  5. Fix an Azure Key Vault and generate an x509 certificate;
  6. Sign the certificate and configure the CDN to utilise it;
  7. Configure CI to deploy content.

Except for the certificate signing step, everything will be automated. Fortunately, certificate signing is a one-off process. At the cease of this workflow, our site should be running, it should be motorcar building and deploying on commit, and TLS should be enabled for maximum security.

Allow'south begin!

I won't walk through all of this, but at a very minimum you'll need to set upwardly the post-obit accounts:

  • Github business relationship;
  • Azure account;
  • Azure subscription fastened to aforementioned business relationship;
  • Account with a certificate signer.

I used namecheap to sign my certificate. You could probably likewise use Permit'due south Encrypt, but I didn't try that avenue. For the Azure subscription, if you're new to Azure you can set up a free trial account. Otherwise, you'll want to create a Pay-as-you-Get subscription.

In addition, y'all'll also need to install the following:

  • git;
  • Terraform;
  • Azure CLI;
  • The static site generator of your choosing (I use Hugo).

One time you lot've done all that, we'll do one more than initial setup step by configuring a terraform backend storage on Azure. This will be in a split Resource Group and this is a former config, so we'll simply exercise this with the Azure CLI.

First, login to the Azure CLI using az login. And so, execute the following commands, replacing the values in <BRACKETS> with your chosen values:

            az group create -g <BACKEND_STORE_RESOURCE_GROUP_NAME> -l <REGION> az storage account create -n <BACKEND_STORE_STORAGE_ACCOUNT_NAME> -1000 <BACKEND_STORE_RESOURCE_GROUP_NAME> -l <REGION> --sku Standard_LRS az storage container create -n terraform-state --account-proper noun <BACKEND_STORE_STORAGE_ACCOUNT_NAME>                      

Cheque the Azure Portal to verify that the Resources Grouping has been properly created: The backend storage resource group should appear in your Resouce Group panel

Next, in your static site directory, create a folder called infrastructure and add the following variables.tf file:

                          # variables.tf              variable              "domain"              {   blazon              =              cord   default              =              "<YOUR_DOMAIN>"              }  variable              "cdn_application_id"              {   default              =              "205478c0-bd83-4e1b-a9d6-db63a3e1e1c8"              # This is azure's application UUID for a CDN endpoint              }  variable              "regions"              {   type              =              map(string)   default              =              {              "primary"              =              "<REGION>"              "cdn"              =              "<CDN_REGION>"              } }                      

Considering Azure CDNs don't map 1:ane to Azure regions, it may be necessary to choose a unlike CDN region than what you pick for your resource group, hence the two entries. Once this is done, we'll proceed to setting upwardly the static site.

Step ii: Configuring a Storage Account to host a static site

Once nosotros have a Terraform backend configured, nosotros'll start by using Terraform to configure a Resource Group and set a Storage Business relationship to serve as a static site host. Create a file in your infrastructure folder chosen main.tf and paste the following in it, replacing entries in <BRACKETS> as above.

                          # Configure the Azure provider              terraform {   required_providers {     azurerm              =              {       source              =              "hashicorp/azurerm"              version              =              ">= 2.26"              }   }    backend              "azurerm"              {     resource_group_name              =              "<BACKEND_STORE_RESOURCE_GROUP_NAME>"              storage_account_name              =              "<BACKEND_STORE_STORAGE_ACCOUNT_NAME>"              container_name              =              "terraform-land"              key              =              "terraform.tfstate"              } }  provider              "azurerm"              {   features {} }  resources              "azurerm_resource_group"              "rg"              {   name              =              "<RESOURCE_GROUP_NAME>"              location              =              var.regions["primary"]   tags              =              {     Purpose              =              "Personal Cloud Space"              } }  resource              "azurerm_storage_account"              "blog_storage"              {   name              =              "<STATIC_SITE_STORAGE_ACCOUNT_NAME>"              resource_group_name              =              azurerm_resource_group.rg.name   location              =              azurerm_resource_group.rg.location   account_tier              =              "Standard"              account_replication_type              =              "LRS"              account_kind              =              "StorageV2"              static_website {     index_document              =              "alphabetize.html"              error_404_document              =              "404.html"              }    tags              =              {     environs              =              "product"              purpose              =              "blog"              } }                      

You lot can choose different tags based on your utilize case, the values here don't matter likewise much. I employ Azure tags to gain visibility into billing. This code is fairly straightforward: we're configuring our Terraform provider, setting upward a remote backend using the Resources created in the previous stride, and setting up a Storage Business relationship.

Note: Here we must use account_kind = StorageV2 for static website functionality.

Run the following commands to fix Terraform:

            terraform init terraform program -out=storage_setup                      

Yous should see that the Terraform plan will create a Resource Group and a Storage Business relationship. Get ahead and run terraform employ "storage_setup". This will create the Resources Group for your account and create the Storage Account within information technology. At present, you should be able to easily exam this. Get ahead an upload a simple alphabetize.html file using the Azure Portal. From your Dashboard, you should be able to see your blog storage. Click at that place, then click "Storage Explorer", expand "BLOB CONTAINERS", and then select "$spider web$. You tin can upload a file manually using the UI.

Storage Explorer showing a static website

At present, in the left carte bar, scroll down a bit to where you see "Static Website." Click that, and you lot should exist able to copy the Chief Endpoint. Paste that into a browser window and y'all should encounter your alphabetize.html file that yous uploaded!

That's it for this footstep, let'due south move on.

Step 2.v: Upload your static site to your storage account

At this signal, you lot probably want to copy your static site into your storage account fully. This is piece of cake enough to do with a transmission command, assuming you have a local build of your website. In Hugo, the site is congenital into the public folder, so that'south what I'll refer to here.

Run the following CLI command:

            az storage blob upload-batch --business relationship-name <STATIC_SITE_STORAGE_ACCOUNT_NAME> -d              '$web'              -s public/.                      

Step 3: Set upwardly an Azure CDN

The default endpoint URL is ugly and doesn't support http redirects. So let'south remedy that. Unfortunately, there's no way of doing this without putting our storage business relationship behind some more infrastructure. The best pick is to utilize an Azure CDN. The Azure CDN offers us a lot of benefits: we tin can manage http to https redirects, use a custom domain, utilize our own x509 document, and guard against deprival of service attacks that would otherwise ramp upward our storage transfer costs. The downside is that a CDN will have to be flushed when we add new content, but nosotros'll get to that later.

When I originally did this, I added the CDN incrementally. But this involved a lot of teardown and rebuild of infrastructure as I was learning what was what. So this volition be a little complex, but we can manage.

Get-go, add the following to your main.tf:

            resource              "azurerm_cdn_profile"              "cdn"              {   proper noun              =              "<CDN_PROFILE_NAME>"              location              =              var.regions["cdn"]   resource_group_name              =              azurerm_resource_group.rg.name   sku              =              "Standard_Microsoft"              }                      

It is necesssary to use sku = "Standard_Microsoft" hither in order to enable the custom domains and redirect rules. Also, add the following:

            resource              "azurerm_cdn_endpoint"              "cdn_blog"              {   name              =              "<CDN_ENDPOINT_NAME>"              profile_name              =              azurerm_cdn_profile.cdn.name   location              =              azurerm_cdn_profile.cdn.location   resource_group_name              =              azurerm_resource_group.rg.name   origin_host_header              =              azurerm_storage_account.blog_storage.primary_web_host    origin {     name              =              "<MEANINGFUL_ORIGIN_NAME>"              host_name              =              azurerm_storage_account.blog_storage.primary_web_host   }    tags              =              {     surroundings              =              "production"              purpose              =              "blog"              }    delivery_rule {     proper noun              =              "EnforceHTTPS"              guild              =              "1"              request_scheme_condition {       operator              =              "Equal"              match_values              =              ["HTTP"]     }      url_redirect_action {       redirect_type              =              "Found"              protocol              =              "Https"              }   } }                      

This is a lot. What this will do is configure our CDN endpoint to point to our blog. This volition create a new domain, <CDN_ENDPOINT_NAME>.azureedge.net that redirects to your static site. In order to make this redirect happen, it is disquisitional to not leave out origin_host_header = azurerm_storage_account.blog_storage.primary_web_host. Go ahead and click this to see if your site is there.

Note: considering this is a CDN, it might accept 5-x minutes for this to propagate. Go grab a tea.

CDN showing a successful configuration

The side by side slightly complex fleck is the delivery_rule. This volition create a rule that forces whatever http traffic to re-route to https. This is a slap-up security exercise that protects you and your users, and moreover makes your site backwards compatible if anyone has linked to an old http:// address out there on the web. We won't be configuring our custom domains just nonetheless.

Clicking on "Rules Engine" on the left should bear witness you that yous accept successfully implemented the EnforceHttps rule.

Http to Https redirect rule

After a few minutes, yous should be able to meet your site at world wide web.yourdomain.com. With that done, nosotros can move on to the hard steps. We have not yet set up upwardly the CDN to accept a custom apex domain, and your DNS is yet being handled by your old provider. We'll address this in the next steps.

Footstep 4: Fix an Azure DNS Zone

First, go to your DNS provider. This step is very important. We're going to add/update two CNAME records. Commencement, add together a CNAME record with the host www and a target of <CDN_ENDPOINT_NAME>.azureedge.internet. If you already have a www entry, then you tin can edit the existing one. Next add together another CNAME tape with a host of cdnverify and a target of cdnverify.<CDN_ENDPOINT_NAME>.azureedge.net. Azure gives cdnverify special treatment, and this is how nosotros'll allow ourselves to add a custom noon domain to the CDN.

Next, we'll create an Azure DNS zone. In principle, this stride is not strictly necessary. Only I believe it volition make things a lot easier. Why? Considering if yous are hosting your DNS off of Azure, you will likely have problems with redirecting the noon domain to the CDN. Y'all tin't guarantee an IP address for the CDN for an A record. Then let's gear up a DNS zone in Azure instead. This has the benefit of allowing you to create an Alias Tape Set. Add the following to your terraform:

            resource              "azurerm_dns_zone"              "<DNS_ZONE_NAME>"              {   name              =              var.domain   resource_group_name              =              azurerm_resource_group.rg.proper name   tags              =              {     purpose              =              "blog"              } }  resource              "azurerm_dns_a_record"              "<DNS_ALIAS_NAME>"              {   proper noun              =              "@"              zone_name              =              azurerm_dns_zone.              <DNS_ZONE_NAME>              .name   resource_group_name              =              azurerm_resource_group.rg.name   ttl              =              300              target_resource_id              =              azurerm_cdn_endpoint.cdn_blog.id    provisioner              "local-exec"              {     command              =              <<EOT   az cdn custom-domain create              \              --endpoint-name ${azurerm_cdn_endpoint.cdn_blog.name}              \              --hostname world wide web.${var.domain}              \              --resources-group ${azurerm_resource_group.rg.name}              \              --contour-name ${azurerm_cdn_profile.cdn.proper noun}              \              -n              <MEANINGFUL_CUSTOM_DOMAIN_NAME>              EOT   }    provisioner              "local-exec"              {     command              =              <<EOT   az cdn custom-domain create              \              --endpoint-name ${azurerm_cdn_endpoint.cdn_blog.name}              \              --hostname ${var.domain}              \              --resource-grouping ${azurerm_resource_group.rg.name}              \              --profile-name ${azurerm_cdn_profile.cdn.proper noun}              -n noon   EOT   }    provisioner              "local-exec"              {     command              =              <<EOT   az cdn custom-domain enable-https              \              --endpoint-proper name ${azurerm_cdn_endpoint.cdn_blog.proper name}              \              --resource-group ${azurerm_resource_group.rg.name}              \              --profile-proper noun ${azurerm_cdn_profile.cdn.name}              \              -north              <MEANINGFUL_CUSTOM_DOMAIN_NAME>              EOT   } }  resources              "azurerm_dns_cname_record"              "www_cname"              {   name              =              "www"              zone_name              =              azurerm_dns_zone.              <DNS_ZONE_NAME>              .name   resource_group_name              =              azurerm_resource_group.rg.name   ttl              =              3600              target_resource_id              =              azurerm_cdn_endpoint.cdn_blog.id }                      

Notation: it may be necessary to add together the following lines to each of the provisioner blocks to make it work on Windows and/or Terraform i.x afterwards the last EOT in each cake:

            interpreter = ["bash", "-c"] working_dir = path.module                      

Thanks to Mehmet Afşar for catching this!

The first block will configure a DNS zone and also add an aliased record pointing to your CDN. This dramatically simplifies DNS management overall. The second block configures an aliased A record pointing to your CDN, something that we can't necessarily exercise with an external DNS provider.

The provisioner commands are a bit more complex. As of this writing, there is no choice in pure Terraform to create a custom domain to adhere to the CDN endpoint. In order to brand this work, we demand to use a provisioner script that will run at the terminate of the creation of the resource. This is where things get tricky. In general, when you create a custom domain in a CDN endpoint, say azure.yourdomain.com, Azure volition await for a matching CNAME record in your DNS with the host name azure. For the apex domain, yourdomain.com, Azure volition look for a cdnverify tape as described in a higher place.

If this command fails, just the resource is otherwise successfully created, the command volition not run once more on a subsequent terraform utilize. Furthermore, the first command will fail if it cannot find a CNAME record pointing www.yourdomain.com to <CDN_ENDPOINT_NAME>.azureedge.net, and the 2nd will neglect if it cannot find a CNAME record with a host name of cdnverify pointing to cdnverify.<CDN_ENDPOINT_NAME>.azureedge.net. That is why information technology is necessary to enter these records in your existing DNS provider before undertaking this step. At this betoken, you lot should be able to navigate to your Azure Dashboard, find the CDN you lot configured, and encounter that you've created two custom domains.

Finally, the third provisioner volition adhere a CDN-managed certificate to your world wide web subdomain. Nosotros will desire to practise this to avert any security warnings that may result from usa changing an existing www CNAME record. Unfortunately, Azure is no longer able to provide CDN-managed certificates for apex domains. Therefore, we'll need to practice a picayune more work in order to properly migrate.

Step v: Set up upwardly Azure Central Vault and generate a certificate

If we were creating a new site, we might be happy just using www.yourdomain.com and existence done with it. However, my existing site allowed the apex domain to work fine, which ways if I don't fully integrate a certificate for the domain, existing links on the internet will be broken. Moreover, since this is a static site being hosted from a storage account, I don't desire to manage Let'south Encrypt certificates every three months manually. So I chose to go ahead and purchase a certificate on Namecheap, but yous can utilize whatever method you're most comfortable with.

In order to bring your own certificate to an Azure CDN, we demand to ready up a Key Vault. Since we're already going to do that, nosotros may too generate a certificate to take signed by a signing authorisation. Azure integrates with a couple of regime, but you can as well use an independent signer. It toll me less than $30 for five years of service.

Annotation: Azure also allows you to generate cocky-signed certificates, simply these exercise not work with the Azure CDN.

We'll implement our last bit of Terraform code at present:

            data              "azurerm_client_config"              "current"              {}  resource              "azuread_service_principal"              "sp"              {   application_id              =              var.cdn_application_id }  resource              "azurerm_key_vault"              "kv"              {   name              =              "<MEANINGFUL_KEYVAULT_NAME>"              location              =              azurerm_resource_group.rg.location   resource_group_name              =              azurerm_resource_group.rg.name   tenant_id              =              data.azurerm_client_config.current.tenant_id   sku_name              =              "standard"              soft_delete_enabled              =              true   soft_delete_retention_days              =              seven              access_policy {     tenant_id              =              data.azurerm_client_config.electric current.tenant_id     object_id              =              information.azurerm_client_config.current.object_id      certificate_permissions              =              [              "create",              "delete",              "deleteissuers",              "get",              "getissuers",              "import",              "listing",              "listissuers",              "managecontacts",              "manageissuers",              "purge",              "setissuers",              "update",     ]      key_permissions              =              [              "fill-in",              "create",              "decrypt",              "delete",              "encrypt",              "go",              "import",              "list",              "purge",              "recover",              "restore",              "sign",              "unwrapKey",              "update",              "verify",              "wrapKey",     ]      secret_permissions              =              [              "fill-in",              "delete",              "become",              "list",              "purge",              "recover",              "restore",              "set up",     ]   }   access_policy {     tenant_id              =              data.azurerm_client_config.electric current.tenant_id     object_id              =              azuread_service_principal.sp.id      certificate_permissions              =              [              "get",              "listing",     ]      secret_permissions              =              [              "get",              "list",     ]   }   tags              =              {     purpose              =              "blog"              } }  resource              "azurerm_key_vault_certificate"              "cert"              {   proper noun              =              "<MEANINGFUL_CERTIFICATE_NAME>"              key_vault_id              =              azurerm_key_vault.kv.id    certificate_policy {     issuer_parameters {       name              =              "Unknown"              }      key_properties {       exportable              =              true       key_size              =              2048              key_type              =              "RSA"              reuse_key              =              true     }      lifetime_action {       activity {         action_type              =              "EmailContacts"              }        trigger {         days_before_expiry              =              30              }     }      secret_properties {       content_type              =              "application/x-pkcs12"              }      x509_certificate_properties {              # Server Authentication = one.three.half dozen.1.five.5.7.3.1              # Customer Hallmark = one.iii.6.one.5.5.7.3.2              extended_key_usage              =              ["one.3.half-dozen.ane.v.5.7.3.one"]        key_usage              =              [              "cRLSign",              "dataEncipherment",              "digitalSignature",              "keyAgreement",              "keyCertSign",              "keyEncipherment",       ]        subject_alternative_names {         dns_names              =              ["www.${var.domain}"]       }        subject area              =              "CN=${var.domain}"              validity_in_months              =              12              }   }    tags              =              {     purpose              =              "blog"              } }                      

Don't fall into my traps. First, we must set name = "Unknown" in the issuer_parameters cake to use an externally-signed document. Moreover, nosotros must apply content_type = "application/x-pkcs12".

Running this with terraform utilise will generate a Key Vault and a certificate. It should add together two access policies to the Key Vault: ane for your administrator business relationship, which is necessary to generate certificates and secrets, and one for the CDN application, which just needs become and list permissions assault certificates and secrets.

Footstep 6: Sign the Certificate and configure the CDN to use information technology

Once we have generated the document, we now need to do some manual steps.

Navigate to your Azure Central Vault, select Certificates, and click on your certificate. Side by side, select the "Document Functioning" link.

Certificate management UI

From here click "Download CSR". Use this with your signing authority and configure your preferred hallmark method. I chose DNS hallmark and followed the instructions provided. Nevertheless, you can besides apply HTTP authentication and upload a file to your web host, or any other means offered.

Once yous receive a signed certificate (this took me less than ten minutes once I figured it all out), y'all can then click "Merge Signed Request" to upload your signed certificate. This will automatically enable the certificate. We only have two steps left: telling the CDN to apply these certificates, and and then switching nameservers to Azure.

Navigate dorsum to your CDN. You should see your custom domains. Click on either of them, and then enable "Custom domain HTTPS." Choose the pick for "Employ my ain document" and then select the Key Vault, Document, and Certificate Version from the driblet downs. Click save.

The last step is to switch DNS nameservers with our DNS provider. We'll want to do that at present and then that the CDN can properly provision the certificates.

Go dorsum to your Azure Dashboard, and select your DNS Zone. On the Overview screen, you lot should see four nameservers in the top right. Ostend that you have an A record with the Name @ configured to point to an Azure CDN resources. If and so, then continue to your DNS provider and switch nameservers to the Azure ones.

One time complete, y'all should be ready to go! The last step will be to configure your build tooling to update your static site.

Step vii: Continuously Deliver with Github Actions

We'll desire to be able to redeploy our content automatically. Ideally, we'd likewise deploy our infrastructure in our CI/CD environs. However, the Terraform scripting above uses some Azure Active Directory resource. In order to get these working in the CI/CD pipeline, there is a fleck more piece of work that needs to be done to adequately manage permissions. It'south probably possible to do this, just I've timeboxed myself to 2 days of work for this learning, and I will non implement that here. For now, running Terraform locally is sufficient to stand up and destroy all the infrastructure we demand.

To get started, we're going to use Github actions to build our site and deploy it. In order to practise this, we'll demand to do the following:

  • compile the static site using our static site generator, in this case Hugo;
  • upload compiled site to Azure;
  • purge the CDN.

In guild to do this, we get-go need to create a Service Principal to utilise in Azure: az advertisement sp create-for-rbac --name "azure-actions-tf" --role Correspondent --scopes /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME> --sdk-auth. This will output some JSON. Do non add this to version control. Instead, nosotros'll navigate to our Github repo, click Settings, and then Secrets, and add together it as a new Repository Secret chosen AZURE_CREDENTIALS.

Next, create a folder in your repository called .github. In it, create a subfolder called workflows and then create a text file, deploy.yml. In it, paste something similar to the following:

                          on:              push:              branches: [              primary ]              jobs:              deploy:              runs-on:              ubuntu-latest              steps:       -              uses:              actions/checkout@v2              with:              submodules:              true              # Fetch Hugo themes (true OR recursive)              fetch-depth:              0              # Fetch all history for .GitInfo and .Lastmod              -              name:              Setup Hugo              uses:              peaceiris/actions-hugo@v2              with:              hugo-version:              '0.68.3'              # extended: true              -              name:              Build              run:              hugo              -              uses:              azure/login@v1              with:              creds:              ${{ secrets.AZURE_CREDENTIALS }}              -              name:              Upload to hulk storage              uses:              azure/CLI@v1              with:              azcliversion:              2.0.72              inlineScript: |                                                        az storage hulk upload-batch --account-name <STATIC_SITE_STORAGE_ACCOUNT_NAME> -d '$spider web' -s public/.              -              proper name:              Purge CDN endpoint              uses:              azure/CLI@v1              with:              azcliversion:              2.0.72              inlineScript: |                                                        az cdn endpoint purge --content-paths  "/*" --profile-name "<CDN_PROFILE_NAME>" --name "<CDN_ENDPOINT_NAME>" --resource-group "<RESOURCE_GROUP_NAME>"              -              name:              logout              run: |                                                        az logout                      

Here you'll want to replace the specifics of your static site generator. Save this and push this to your repo. If all goes well, y'all should exist able to deploy new content on every commit to main!

TL;DR Give me the copy-pasta

Everything in <BRACKETS> needs to exist replaced with a cord of your choosing.

Run to configure a remote backend:

            az login az group create -yard <BACKEND_STORE_RESOURCE_GROUP_NAME> -fifty <REGION> az storage account create -n <BACKEND_STORE_STORAGE_ACCOUNT_NAME> -chiliad <BACKEND_STORE_RESOURCE_GROUP_NAME> -l <REGION> --sku Standard_LRS az storage container create -n terraform-state --account-proper name <BACKEND_STORE_STORAGE_ACCOUNT_NAME>                      

Create DNS CNAME entries:

  • www pointing to <CDN_ENDPOINT_NAME>.azureedge.net and
  • cdnverify pointing to cdnverify.<CDN_ENDPOINT_NAME>.azureedge.net.

Create variables.tf:

                          # variables.tf              variable              "domain"              {   type              =              string   default              =              "<YOUR_DOMAIN>"              }  variable              "cdn_application_id"              {   default              =              "205478c0-bd83-4e1b-a9d6-db63a3e1e1c8"              # This is azure's awarding UUID for a CDN endpoint              }  variable              "regions"              {   type              =              map(string)   default              =              {              "primary"              =              "<REGION>"              "cdn"              =              "<CDN_REGION>"              } }                      

Create main.tf:

                          # main.tf              # Configure the Azure provider              terraform {   required_providers {     azurerm              =              {       source              =              "hashicorp/azurerm"              version              =              ">= 2.26"              }   }    backend              "azurerm"              {     resource_group_name              =              "<BACKEND_STORE_RESOURCE_GROUP_NAME>"              storage_account_name              =              "<BACKEND_STORE_STORAGE_ACCOUNT_NAME>"              container_name              =              "terraform-state"              primal              =              "terraform.tfstate"              } }  provider              "azurerm"              {   features {} }  resources              "azurerm_resource_group"              "rg"              {   name              =              "<RESOURCE_GROUP_NAME>"              location              =              var.regions["principal"]   tags              =              {     Purpose              =              "Personal Cloud Space"              } }  resource              "azurerm_storage_account"              "blog_storage"              {   name              =              "<STATIC_SITE_STORAGE_ACCOUNT_NAME>"              resource_group_name              =              azurerm_resource_group.rg.proper noun   location              =              azurerm_resource_group.rg.location   account_tier              =              "Standard"              account_replication_type              =              "LRS"              account_kind              =              "StorageV2"              static_website {     index_document              =              "index.html"              error_404_document              =              "404.html"              }    tags              =              {     environment              =              "product"              purpose              =              "blog"              } }  resources              "azurerm_cdn_profile"              "cdn"              {   name              =              "<CDN_PROFILE_NAME>"              location              =              var.regions["cdn"]   resource_group_name              =              azurerm_resource_group.rg.name   sku              =              "Standard_Microsoft"              }  resource              "azurerm_cdn_endpoint"              "cdn_blog"              {   name              =              "<CDN_ENDPOINT_NAME>"              profile_name              =              azurerm_cdn_profile.cdn.name   location              =              azurerm_cdn_profile.cdn.location   resource_group_name              =              azurerm_resource_group.rg.name   origin_host_header              =              azurerm_storage_account.blog_storage.primary_web_host    origin {     name              =              "<MEANINGFUL_ORIGIN_NAME>"              host_name              =              azurerm_storage_account.blog_storage.primary_web_host   }    tags              =              {     environment              =              "production"              purpose              =              "web log"              }    delivery_rule {     name              =              "EnforceHTTPS"              order              =              "i"              request_scheme_condition {       operator              =              "Equal"              match_values              =              ["HTTP"]     }      url_redirect_action {       redirect_type              =              "Establish"              protocol              =              "Https"              }   } }  resources              "azurerm_dns_zone"              "<DNS_ZONE_NAME>"              {   proper noun              =              var.domain   resource_group_name              =              azurerm_resource_group.rg.name   tags              =              {     purpose              =              "weblog"              } }  resource              "azurerm_dns_a_record"              "<DNS_ALIAS_NAME>"              {   name              =              "@"              zone_name              =              azurerm_dns_zone.              <DNS_ZONE_NAME>              .name   resource_group_name              =              azurerm_resource_group.rg.name   ttl              =              300              target_resource_id              =              azurerm_cdn_endpoint.cdn_blog.id    provisioner              "local-exec"              {     control              =              <<EOT   az cdn custom-domain create              \              --endpoint-proper name ${azurerm_cdn_endpoint.cdn_blog.proper name}              \              --hostname www.${var.domain}              \              --resource-group ${azurerm_resource_group.rg.name}              \              --contour-name ${azurerm_cdn_profile.cdn.name}              \              -n              <MEANINGFUL_CUSTOM_DOMAIN_NAME>              EOT   }    provisioner              "local-exec"              {     command              =              <<EOT   az cdn custom-domain create              \              --endpoint-name ${azurerm_cdn_endpoint.cdn_blog.name}              \              --hostname ${var.domain}              \              --resource-group ${azurerm_resource_group.rg.name}              \              --contour-name ${azurerm_cdn_profile.cdn.name}              -n apex   EOT   }    provisioner              "local-exec"              {     command              =              <<EOT   az cdn custom-domain enable-https              \              --endpoint-name ${azurerm_cdn_endpoint.cdn_blog.name}              \              --resource-group ${azurerm_resource_group.rg.name}              \              --profile-name ${azurerm_cdn_profile.cdn.name}              \              -north emilygorcenski   EOT   } }  resource              "azurerm_dns_cname_record"              "www_cname"              {   name              =              "www"              zone_name              =              azurerm_dns_zone.              <DNS_ZONE_NAME>              .proper name   resource_group_name              =              azurerm_resource_group.rg.name   ttl              =              3600              target_resource_id              =              azurerm_cdn_endpoint.cdn_blog.id }  data              "azurerm_client_config"              "current"              {}              # Remove this if this service principal has already been created for this subscription              resource              "azuread_service_principal"              "sp"              {   application_id              =              var.cdn_application_id }  resources              "azurerm_key_vault"              "kv"              {   name              =              "<MEANINGFUL_KEYVAULT_NAME>"              location              =              azurerm_resource_group.rg.location   resource_group_name              =              azurerm_resource_group.rg.proper noun   tenant_id              =              data.azurerm_client_config.electric current.tenant_id   sku_name              =              "standard"              soft_delete_enabled              =              true   soft_delete_retention_days              =              7              access_policy {     tenant_id              =              data.azurerm_client_config.current.tenant_id     object_id              =              data.azurerm_client_config.current.object_id      certificate_permissions              =              [              "create",              "delete",              "deleteissuers",              "become",              "getissuers",              "import",              "list",              "listissuers",              "managecontacts",              "manageissuers",              "purge",              "setissuers",              "update",     ]      key_permissions              =              [              "backup",              "create",              "decrypt",              "delete",              "encrypt",              "get",              "import",              "list",              "purge",              "recover",              "restore",              "sign",              "unwrapKey",              "update",              "verify",              "wrapKey",     ]      secret_permissions              =              [              "backup",              "delete",              "get",              "listing",              "purge",              "recover",              "restore",              "set",     ]   }              # if you lot've already created the service principal for the subscription, remove this cake              # you'll demand to add together it manually, but a future improvement will automate this              access_policy {     tenant_id              =              data.azurerm_client_config.current.tenant_id     object_id              =              azuread_service_principal.sp.id      certificate_permissions              =              [              "get",              "list",     ]      secret_permissions              =              [              "go",              "list",     ]   }   tags              =              {     purpose              =              "blog"              } }  resources              "azurerm_key_vault_certificate"              "cert"              {   name              =              "<MEANINGFUL_CERTIFICATE_NAME>"              key_vault_id              =              azurerm_key_vault.kv.id    certificate_policy {     issuer_parameters {       proper noun              =              "Unknown"              }      key_properties {       exportable              =              true       key_size              =              2048              key_type              =              "RSA"              reuse_key              =              true     }      lifetime_action {       action {         action_type              =              "EmailContacts"              }        trigger {         days_before_expiry              =              30              }     }      secret_properties {       content_type              =              "application/x-pkcs12"              }      x509_certificate_properties {              # Server Hallmark = 1.iii.half dozen.1.5.5.7.3.1              # Customer Authentication = 1.iii.6.one.v.5.7.iii.2              extended_key_usage              =              ["1.3.6.1.five.5.7.three.one"]        key_usage              =              [              "cRLSign",              "dataEncipherment",              "digitalSignature",              "keyAgreement",              "keyCertSign",              "keyEncipherment",       ]        subject_alternative_names {         dns_names              =              ["world wide web.${var.domain}"]       }        field of study              =              "CN=${var.domain}"              validity_in_months              =              12              }   }    tags              =              {     purpose              =              "web log"              } }                      

Run terraform init followed by terraform plan followed past terraform apply

Upload your static site to your new storage business relationship with: az storage hulk upload-batch --account-name <STATIC_SITE_STORAGE_ACCOUNT_NAME> -d '$web' -due south public/.

Download your CSR certificate from the generated Document in the Fundamental Vault and have it signed. Merge the signed certificate back. Add together the certificates to your custom domains in the CDN endpoint. Replace your DNS nameservers with those from the Azure DNS Zone.

Run az advert sp create-for-rbac --name "azure-actions-tf" --role Contributor --scopes /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME> --sdk-auth and copy the output to a Github secret chosen AZURE_CREDENTIALS.

Copy to .github/workflows/delivery.yml:

                          # delivery.yml              on:              button:              branches: [              principal ]              jobs:              deploy:              runs-on:              ubuntu-latest              steps:       -              uses:              actions/checkout@v2              with:              submodules:              true              # Fetch Hugo themes (true OR recursive)              fetch-depth:              0              # Fetch all history for .GitInfo and .Lastmod              -              name:              Setup Hugo              uses:              peaceiris/actions-hugo@v2              with:              hugo-version:              '0.68.3'              # extended: truthful              -              name:              Build              run:              hugo              -              uses:              azure/login@v1              with:              creds:              ${{ secrets.AZURE_CREDENTIALS }}              -              name:              Upload to blob storage              uses:              azure/CLI@v1              with:              azcliversion:              2.0.72              inlineScript: |                                                        az storage blob upload-batch --account-proper name <STATIC_SITE_STORAGE_ACCOUNT_NAME> -d '$web' -s public/.              -              name:              Purge CDN endpoint              uses:              azure/CLI@v1              with:              azcliversion:              ii.0.72              inlineScript: |                                                        az cdn endpoint purge --content-paths  "/*" --profile-name "<CDN_PROFILE_NAME>" --proper noun "<CDN_ENDPOINT_NAME>" --resource-group "<RESOURCE_GROUP_NAME>"              -              name:              logout              run: |                                                        az logout                      

Push to Github.

Conclusions

Truthfully, this was a lot of piece of work for very footling gain exterior of learning. This is a lot of work for hosting a static site, and tools similar Github Pages and Firebase are far amend suited for the task, and handle all the messy $.25 like http to https redirection and custom subdomains for you. Notwithstanding, this was useful for me to learn Terraform and Azure.

Some of the mistakes and traps I encountered along the fashion:

  • wait for nameservers to propagate before turning off your old host. This will take upwardly to 48 hours;
  • possibly consider using the same Resource Group for your Terraform backend;
  • the Service Principal pace for configuring the Azure CDN application is complex and will require a lot more research.

I am certain some of this work can exist tightened up. I tested this by start migrating my weblog, and and then I migrated whentheycamedown. That worked pretty seamlessly, and had I non been too hasty in deleting my quondam host, I could have accomplished the zero downtime migration that I wanted.

The large affair I have left to exercise: figure out the Service Principal stuff so I can integrate infrastructure deployments into CI/CD and remove a potential transmission pace.

Anyhow, I have seen some blog posts embrace moving static sites to Azure before, but none using Terraform. Then hopefully this is a useful resources, and if it is, delight permit me know!.

References

  • Adding a Root Domain to Azure CDN endpoint–this has some outdated advice that no longer works but the principles are notwithstanding useful;
  • Gear up a GitHub Actions workflow to deploy your static website in Azure Storage;
  • Tutorial: Configure HTTPS on an Azure CDN custom domain;
  • Delegation of DNS zones with Azure DNS;
  • Walkthrough: Set up Custom Domains with HTTPS on Azure Static Websites;
  • Deploying to Azure using Terraform and GitHub Deportment

This mail was updated on 05.07.2021 to include some potential extra provisioner logic to make this work on Windows, or with later versions of Terraform.

mulhallembefors.blogspot.com

Source: https://www.emilygorcenski.com/post/migrating-a-static-site-to-azure-with-terraform/

Postar um comentário for "Terraform Azure Upload All Files to Bulk Upload"