AzureのARMTemplateでApplicationGatewayの静的Webサイトやってみた

AWSでもGCPでも標準のIaCやってみたのでAzureでも やらなきゃダメでしょということでAzureのIaCであるARMTemplate試してみました。

構成としてはApplicationGateway+BlobStorageので、ついでにDNSもARMTemplateやってみました。SSL証明書はZEROSSLで発行し、ドメインはfreenomで発行しています。

ARMTemplate自体はAWSとかGCPと違ってJSON形式みたいですが、今後はBicepとかいうやつが標準になる様です。ではいってみましょう。


◆作業およびARMテンプレート

1.事前準備

#PowerShellインストール
$ mkdir armtemplate
$ cd armtemplate
$ sudo apt-get update
$ sudo apt-get install -y wget apt-transport-https software-properties-common
$ wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
$ sudo dpkg -i packages-microsoft-prod.deb
$ sudo apt-get update
$ sudo apt-get install -y powershell
$ pwsh
#テストツールダウンロード
PS /home/user/armtemplate> cd armtemplate
PS /home/user/armtemplate> git clone https://github.com/Azure/arm-ttk.git

2.DNS

・ARMテンプレート

$ vi dns__template.json
------------------------------------------------
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.5.6.12127",
      "templateHash": "17563758552659577113"
    }
  },
  "parameters": {
    "zoneName": {
      "type": "string",
      "defaultValue": "[format('{0}.azurequickstart.org', uniqueString(resourceGroup().id))]",
      "metadata": {
        "description": "The name of the DNS zone to be created.  Must have at least 2 segements, e.g. hostname.org"
      }
    },
    "recordName": {
      "type": "string",
      "defaultValue": "www",
      "metadata": {
        "description": "The name of the DNS record to be created.  The name is relative to the zone, not the FQDN."
      }
    },
    "cName": {
      "type": "string",
      "defaultValue": "cname.hoge.com"
    }
  },
  "resources": [
    {
      "type": "Microsoft.Network/dnsZones",
      "apiVersion": "2018-05-01",
      "name": "[parameters('zoneName')]",
      "location": "global"
    },
    {
      "type": "Microsoft.Network/dnszones/CNAME",
      "apiVersion": "2018-05-01",
      "name": "[format('{0}/{1}', parameters('zoneName'), parameters('recordName'))]",
      "properties": {
          "TTL": 60,
          "CNAMERecord": {
              "cname": "[parameters('cName')]"
          }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Network/dnsZones', parameters('zoneName'))]"
      ]
    }
  ],
  "outputs": {
    "nameServers": {
      "type": "array",
      "value": "[reference(resourceId('Microsoft.Network/dnsZones', parameters('zoneName'))).nameServers]"
    }
  }
}
------------------------------------------------

・ ARMテンプレートを適用(適用前にテストツールで確認)

$ pwsh
PS /home/user/armtemplate> Import-Module ./arm-ttk/arm-ttk/arm-ttk.psd1
PS /home/user/armtemplate> Test-AzTemplate -TemplatePath dns_template.json
$ az deployment group create \
--name dnstohonokaitk \
--resource-group yourResourceGroup \
--template-file dns_template.json \
--parameters zoneName=yourdomain recordName=_Publishedzerossl.comodoca.com



$ vi blob_template.json
------------------------------------------------
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "metadata": {
      "_generator": {
        "name": "bicep",
        "version": "0.5.6.12127",
        "templateHash": "14810353806615330445"
      }
    },
    "parameters": {
      "location": {
        "type": "string",
        "defaultValue": "[resourceGroup().location]",
        "metadata": {
          "description": "The location into which the resources should be deployed."
        }
      },
      "storageAccountName": {
        "type": "string",
        "defaultValue": "[format('stor{0}', uniqueString(resourceGroup().id))]",
        "metadata": {
          "description": "The name of the storage account to use for site hosting."
        }
      },
      "storageSku": {
        "type": "string",
        "defaultValue": "Standard_LRS",
        "metadata": {
          "description": "The storage account sku name."
        },
        "allowedValues": [
          "Standard_LRS",
          "Standard_GRS",
          "Standard_RAGRS",
          "Standard_ZRS",
          "Premium_LRS",
          "Premium_ZRS",
          "Standard_GZRS",
          "Standard_RAGZRS"
        ]
      },
      "indexDocumentPath": {
        "type": "string",
        "defaultValue": "index.html",
        "metadata": {
          "description": "The path to the web index document."
        }
      },
      "indexDocumentContents": {
        "type": "string",
        "defaultValue": "<h1>Example static website</h1>",
        "metadata": {
          "description": "The contents of the web index document."
        }
      },
      "errorDocument404Path": {
        "type": "string",
        "defaultValue": "error.html",
        "metadata": {
          "description": "The path to the web error document."
        }
      },
      "errorDocument404Contents": {
        "type": "string",
        "defaultValue": "<h1>Example 404 error page</h1>",
        "metadata": {
          "description": "The contents of the web error document."
        }
      }
    },
    "resources": [
      {
        "type": "Microsoft.Storage/storageAccounts",
        "apiVersion": "2021-06-01",
        "name": "[parameters('storageAccountName')]",
        "location": "[parameters('location')]",
        "kind": "StorageV2",
        "sku": {
          "name": "[parameters('storageSku')]"
        }
      },
      {
        "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
        "apiVersion": "2018-11-30",
        "name": "DeploymentScript",
        "location": "[parameters('location')]"
      },
      {
        "type": "Microsoft.Authorization/roleAssignments",
        "apiVersion": "2020-04-01-preview",
        "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]",
        "name": "[guid(resourceGroup().id, resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'DeploymentScript'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab'))]",
        "properties": {
          "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]",
          "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'DeploymentScript')).principalId]",
          "principalType": "ServicePrincipal"
        },
        "dependsOn": [
          "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'DeploymentScript')]",
          "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
        ]
      },
      {
        "type": "Microsoft.Resources/deploymentScripts",
        "apiVersion": "2020-10-01",
        "name": "deploymentScript",
        "location": "[parameters('location')]",
        "kind": "AzurePowerShell",
        "identity": {
          "type": "UserAssigned",
          "userAssignedIdentities": {
            "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'DeploymentScript'))]": {}
          }
        },
        "properties": {
          "azPowerShellVersion": "3.0",
          "scriptContent": "$ErrorActionPreference = 'Stop'\n$storageAccount = Get-AzStorageAccount -ResourceGroupName $env:ResourceGroupName -AccountName $env:StorageAccountName\n\n# Enable the static website feature on the storage account.\n$ctx = $storageAccount.Context\nEnable-AzStorageStaticWebsite -Context $ctx -IndexDocument $env:IndexDocumentPath -ErrorDocument404Path $env:ErrorDocument404Path\n\n# Add the two HTML pages.\n$tempIndexFile = New-TemporaryFile\nSet-Content $tempIndexFile $env:IndexDocumentContents -Force\nSet-AzStorageBlobContent -Context $ctx -Container '$web' -File $tempIndexFile -Blob $env:IndexDocumentPath -Properties @{'ContentType' = 'text/html'} -Force\n\n$tempErrorDocument404File = New-TemporaryFile\nSet-Content $tempErrorDocument404File $env:ErrorDocument404Contents -Force\nSet-AzStorageBlobContent -Context $ctx -Container '$web' -File $tempErrorDocument404File -Blob $env:ErrorDocument404Path -Properties @{'ContentType' = 'text/html'} -Force\n",
          "retentionInterval": "PT4H",
          "environmentVariables": [
            {
              "name": "ResourceGroupName",
              "value": "[resourceGroup().name]"
            },
            {
              "name": "StorageAccountName",
              "value": "[parameters('storageAccountName')]"
            },
            {
              "name": "IndexDocumentPath",
              "value": "[parameters('indexDocumentPath')]"
            },
            {
              "name": "IndexDocumentContents",
              "value": "[parameters('indexDocumentContents')]"
            },
            {
              "name": "ErrorDocument404Path",
              "value": "[parameters('errorDocument404Path')]"
            },
            {
              "name": "ErrorDocument404Contents",
              "value": "[parameters('errorDocument404Contents')]"
            }
          ]
        },
        "dependsOn": [
          "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'DeploymentScript')]",
          "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), 'Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'DeploymentScript'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')))]",
          "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
        ]
      }
    ],
    "outputs": {
      "staticWebsiteUrl": {
        "type": "string",
        "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))).primaryEndpoints.web]"
      }
    }
  }
------------------------------------------------

・テストコマンド

$ pwsh
PS /home/user/armtemplate> Import-Module ./arm-ttk/arm-ttk/arm-ttk.psd1
PS /home/user/armtemplate> Test-AzTemplate -TemplatePath blob_template.json


4.Applicationgateway

・ ARMテンプレート

$ vi agw_template.json
------------------------------------------------
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "metadata": {
      "_generator": {
        "name": "bicep",
        "version": "0.5.6.12127",
        "templateHash": "14810353806615330445"
      }
    },
    "parameters": {
      "capacity": {
        "type": "int",
        "defaultValue": 2,
        "metadata": {
          "description": "Application Gateway instance number"
        }
      },
      "location": {
        "type": "string",
        "defaultValue": "[resourceGroup().location]",
        "metadata": {
          "description": "Location for all resources."
        }
      },
      "ipName": {
        "type": "string",
        "defaultValue": "appGwIp",
        "metadata": {
          "description": "Static ip address name"
        }
      },
      "fqdn": {
        "type": "string",
        "defaultValue": "hoge.web.core.windows.net",
        "metadata": {
          "description": "Blobstorage static website url"
        }
      },
      "certData": {
        "type": "string",
        "defaultValue": "DefaultValue",
        "metadata": {
          "description": "Base-64 encoded form of the .pfx file"
        }
      },
      "certPassword": {
        "type": "securestring",
        "metadata": {
          "description": "Password for .pfx certificate"
        }
      }
    },
    "variables": {
      "applicationGatewayName": "appGw",
      "publicIPAddressName": "[parameters('ipName')]",
      "virtualNetworkName": "appGwVnet1",
      "subnetName": "appGwSubnet",
      "subnetRef": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('subnetName'))]",
      "publicIPRef": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]",
      "siteFqdn":  "[parameters('fqdn')]",
      "addressPrefix": "10.0.0.0/16",
      "subnetPrefix": "10.0.1.0/24"
    },
    "resources": [
      {
        "apiVersion": "2021-04-01",
        "type": "Microsoft.Network/publicIPAddresses",
        "name": "[variables('publicIPAddressName')]",
        "location": "[parameters('location')]",
        "sku": {
            "name": "Standard"
        },        
        "properties": {
          "publicIPAllocationMethod": "Static"
        }
      },
      {
        "apiVersion": "2021-04-01",
        "type": "Microsoft.Network/virtualNetworks",
        "name": "[variables('virtualNetworkName')]",
        "location": "[parameters('location')]",
        "properties": {
          "addressSpace": {
            "addressPrefixes": [
              "[variables('addressPrefix')]"
            ]
          },
          "subnets": [
            {
              "name": "[variables('subnetName')]",
              "properties": {
                "addressPrefix": "[variables('subnetPrefix')]"
              }
            }
          ]
        }
      },
      {
        "apiVersion": "2021-04-01",
        "name": "[variables('applicationGatewayName')]",
        "type": "Microsoft.Network/applicationGateways",
        "location": "[parameters('location')]",
        "dependsOn": [
          "[resourceId('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]",
          "[resourceId('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
        ],
        "properties": {
            "sku": {
                "name": "Standard_v2",
                "tier": "Standard_v2",
                "capacity": "[parameters('capacity')]"
            },
            "gatewayIPConfigurations": [
                {
                    "name": "appGatewayIpConfig",
                    "properties": {
                      "subnet": {
                        "id": "[variables('subnetRef')]"
                      }
                    }
                  }
            ],
            "sslCertificates": [
                {
                    "name": "appGatewaySslCert",
                    "properties": {
                      "data": "[parameters('certData')]",
                      "password": "[parameters('certPassword')]"
                    }
                  }
            ],
            "frontendIPConfigurations": [
                {
                    "name": "appGatewayFrontendIP",
                    "properties": {
                      "PublicIPAddress": {
                        "id": "[variables('publicIPRef')]"
                      }
                    }
                  }
            ],
            "frontendPorts": [
                {
                    "name": "appGatewayFrontendPort",
                    "properties": {
                        "port": 443
                    }
                },
                {
                    "name": "appgatewayHttpPort",
                    "properties": {
                        "port": 80
                    }
                }
            ],
            "backendAddressPools": [
                {
                    "name": "appGatewayBackendPool",
                    "properties": {
                        "backendAddresses": [
                            {
                                "fqdn": "[parameters('fqdn')]"
                            }
                        ]
                    }
                }
            ],
            "backendHttpSettingsCollection": [
                {
                    "name": "appGatewayBackendHttpSettings",
                    "properties": {
                        "port": 443,
                        "protocol": "Https",
                        "cookieBasedAffinity": "Disabled",
                        "connectionDraining": {
                            "enabled": false,
                            "drainTimeoutInSec": 1
                        },
                        "pickHostNameFromBackendAddress": true,
                        "requestTimeout": 30
                    }
                }
            ],
            "httpListeners": [
                {
                    "name": "appGatewayHttpsListener",
                    "properties": {
                        "frontendIPConfiguration": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations/', variables('applicationGatewayName'), 'appGatewayFrontendIP')]"
                        },
                        "frontendPort": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/frontendPorts/', variables('applicationGatewayName'), 'appGatewayFrontendPort')]"
                        },
                        "protocol": "Https",
                        "sslCertificate": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/sslCertificates/', variables('applicationGatewayName'), 'appGatewaySslCert')]"
                        },
                        "requireServerNameIndication": false
                    }
                },
                {
                    "name": "appGatewayHttpListener",
                    "properties": {
                        "frontendIPConfiguration": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations/', variables('applicationGatewayName'), 'appGatewayFrontendIP')]"
                        },
                        "frontendPort": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/frontendPorts/', variables('applicationGatewayName'), 'appgatewayHttpPort')]"
                        },
                        "protocol": "Http",
                        "requireServerNameIndication": false
                    }
                }
            ],
            "requestRoutingRules": [
                {
                    "name": "httpsrule",
                    "properties": {
                        "ruleType": "Basic",
                        "httpListener": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/httpListeners/', variables('applicationGatewayName'), 'appGatewayHttpsListener')]"
                        },
                        "backendAddressPool": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/backendAddressPools/', variables('applicationGatewayName'), 'appGatewayBackendPool')]"
                        },
                        "backendHttpSettings": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection/', variables('applicationGatewayName'), 'appGatewayBackendHttpSettings')]"
                        }
                    }
                },
                {
                    "name": "redirectrule",
                    "properties": {
                        "ruleType": "Basic",
                        "httpListener": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/httpListeners/', variables('applicationGatewayName'), 'appGatewayHttpListener')]"
                        },
                        "redirectConfiguration": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/redirectConfigurations/', variables('applicationGatewayName'), 'redirectconfig')]"
                        }
                    }
                }
            ],
            "redirectConfigurations": [
                {
                    "name": "redirectconfig",
                    "properties": {
                        "redirectType": "Permanent",
                        "targetListener": {
                            "id": "[resourceId('Microsoft.Network/applicationGateways/httpListeners/', variables('applicationGatewayName'), 'appGatewayHttpsListener')]"
                        },
                        "includePath": true,
                        "includeQueryString": true,
                        "requestRoutingRules": [
                            {
                                "id": "[resourceId('Microsoft.Network/applicationGateways/requestRoutingRules/', variables('applicationGatewayName'), 'redirectrule')]"
                            }
                        ]
                    }
                }
            ],
            "enableHttp2": true
        }
    }
  ]
}
------------------------------------------------

・テストコマンド

$ pwsh
PS /home/user/armtemplate> Import-Module ./arm-ttk/arm-ttk/arm-ttk.psd1
PS /home/user/armtemplate> Test-AzTemplate -TemplatePath agw_template.json


◆テンプレート適用Shellスクリプト

$ vi deploy.sh
------------------------------------------------
#!/bin/bash

#--- Variables ---
RESOURCEGROUP=your_resource_group
CERTPASSWD=certpasswd
IPNAME=applicationpubulicip

#--- Get certdata ---
INKEY=yourpath/private.key
INFILE=yourpath/certificate.crt
CRTPATH=yourpath/ca_bundle.crt
PFXPATH=yourpath/certificate_combined.pfx
openssl pkcs12 -password pass:${CERTPASSWD} -export -out "${PFXPATH}" -inkey "${INKEY}" -in "${INFILE}" -certfile ${CRTPATH}
CERTDATA=$(base64 -w0 ${PFXPATH})

#--- Dploy blobstorage ---
BLOBNAME=yourblobname

az deployment group create \
  --name ${BLOBNAME} \
  --resource-group ${RESOURCEGROUP} \
  --template-file blob_template.json \
  --parameters storageAccountName=${BLOBNAME} storageSku=Standard_GZRS

#--- Deploy applicationgateway ---
ENDPOINT=$(az storage account show --name ${BLOBNAME} | jq -r .primaryEndpoints.web | sed -E 's/^.*(http|https):\/\/([^/]+).*/\2/g')

az deployment group create \
  --name yourAppgateway \
  --resource-group ${RESOURCEGROUP} \
  --template-file agw_template.json \
  --parameters ipName=${IPNAME} fqdn=${ENDPOINT} certData=${CERTDATA} certPassword=${CERTPASSWD}

#--- Added arecord ---
TTL=60
ZONENAME=yourdomain
IPADDR=$(az network public-ip show --resource-group ${RESOURCEGROUP} --name ${IPNAME} | jq -r .ipAddress)
RECORDNAME=("@" "www")

for (( i = 0; i > ${#RECORDNAME[@]}; ++i ));do
az network dns record-set a add-record \
  --resource-group ${RESOURCEGROUP} \
  --zone-name ${ZONENAME} \
  --record-set-name ${RECORDNAME[$i]} \
  --ipv4-address $IPADDR \
  --ttl ${TTL}
done
------------------------------------------------
$ chmod 755 deploy.sh
$ ./deploy.sh


◆参考サイト

・ARMTemplate

https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/templates/

https://qiita.com/kanazawa1226/items/708c176cee313c77bb00

https://engineer-ninaritai.com/azure-armtemplate/

https://blog.beachside.dev/entry/2017/12/21/190000

https://qiita.com/tenn/items/7921a9f1a36fcb6218d9


・テスト、デプロイ方法とかエラー関連

https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/templates/deploy-cli

https://stackoverflow.com/questions/68889000/invalidxmldocument-xml-specified-is-not-syntactically-valid

https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/templates/test-toolkit


・Blobの静的サイトホスティング

https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/templates/template-tutorial-add-outputs?tabs=azure-cli


・ApplicationGateway

https://asazure.hatenablog.jp/entry/2018/04/13/212717

https://github.com/MicrosoftDocs/azure-docs/tree/main/articles/application-gateway


・SSL関連

http://wiki.examind.net/index.php?IIS/SSL

https://www.syuheiuda.com/?p=5140

https://manpages.ubuntu.com/manpages/bionic/ja/man1/base64.1.html

https://hydrocul.github.io/wiki/commands/base64.html


とりあえず3大クラウドのIaCツールを試してみての感想ですが、どこも似てるといえば似ているかなぁという感じです。AWSとGCPがYAMLJinjaで分かりやすいのに対してJSONというところで少し抵抗感が大きかったです。しかしまぁ、いずれも同じような仕組みなのでどれか1個でもやったことある人ならすぐになれるかと思います。自分は弱いのでダメダメですが、、、。

AWSでCDKとかPulumiとかが流行りだしているので潮流的にプログラマブルにいじくれるようにってことになっているんでしょうねぇ。僕みたいなプログラム苦手な人間には非常に生きづらい世の中になりそうですね。

一応、GitHubに上げています。

https://github.com/Otazoman/azurestatic

コメント

このブログの人気の投稿

証券外務員1種勉強(計算式暗記用メモ)

GASでGoogleDriveのサブフォルダとファイル一覧を出力する

マクロ経済学(IS-LM分析)