Wdrożenie bacpac i aplikacji ASP.NET do Azure

5 minute read

Na początku swojej przygody z blogowaniem przeprowadziłem krótkie rozeznanie na temat dostępnych platform pod blogi. Mój pierwszy wybór padł na BlogEngine.NET. Dość naturalne wydawało mi się, że skoro jest to platforma napisana w .NET, będę miał większą nad nią kontrolę, plus będę mógł ją z łatwością wdrożyć do Azure. Ostatecznie okazało się, że ilość dostępnych dodatków i rozszerzeń nie jest zadowalająca i stanęło na WordPressie. Zanim jednak przesiadłem się na WP, dostosowałem BlogEngine.NET pod chmurę. Poniżej znajdziesz opis tego procesu, da się go pewnie zastosować przy przenoszeniu innych aplikacji ASP.NET do Azure.

Przygotowanie infrastruktury i szablonów ARM

Pierwszym krokiem, który wykonałem, było zastanowienie się, jakich komponentów Azure mój blog potrzebuje. Wybór padł rzecz jasna na usługi w modelu platformowym, czyli PaaS. Sama aplikacja miała wylądować w WebApps na AppService, baza danych w SQL Database, a logowanie wszelkich błędów i danych o wydajności w Application Insights. Dla osób, które taką konfigurację widziały już wielokrotnie, obiecuję jeden trick (bacpac) w dalszej części, którego się przy okazji nauczyłem.

img

Kolejnym krokiem jest przygotowanie szablonu ARM, by nasza architekturę każdorazowo wdrażać automatycznie. Jest to szczególnie ważne, ponieważ otwiera nam to spektrum możliwości i korzyści. Jeżeli temat nie jest Ci bliski, to odsyłam do następujących tematów: Continuous Delivery, Continuous Configuration Automation i Infrastructure as a Code.

W tym celu zrobiłem fork oficjalnego repozytorium BlogEngine (ostateczny wynik z poprawkami opisanymi poniżej znajdziesz tutaj). Do solucji projektu dodałem nowy projekt typu Azure Resource Group z poziomu VisualStudio, natomiast można się bez tego obyć. Sam szablon json, umieszczony luzem poza projektem,  jest w zupełności wystarczający.

img

Od tego momentu możemy zacząć opisywać naszą docelową infrastrukturę w pliku i automatycznie wdrażać ją do Azure. Sam proces wdrażania można wykonać testowo bezpośrednio z VS, lub na wiele innych sposobów, np. przez PowerShell. Osobiście rekomenduję, by zbudować pełny proces w oparciu o VSTS, ale w opisywanym przypadku sytuacja wygląda nieco inaczej ze względu na potrzeby. Pisanie szablonów przy pierwszym zetknięciu może być nieco skomplikowane, na szczęście VS trochę nam sprawę ułatwia. Możemy również skorzystać z gotowców dostępnych na Azure Quickstart Templates.

Zawartość szablonu ARM

Szablon zawiera zwykle trzy sekcje. Pierwsza z nich służy do definicji parametrów wejściowych (w naszym przypadku mogą to być login i hasło do bazy, adres bloga, półka cenowa usług itp). Kolejna sekcja to opis infrastruktury docelowej, wykorzystujący parametry. W ostatniej części znajdują się deklaracje parametrów wyjściowych z procesu.

W moim szablonie definiuję dokładnie to, co zobrazowałem na diagramie w poprzednim akapicie. Poniżej deklaracja Application Insights, na uwagę zasługuje użycie wspomnianych parametrów i chyba jedna z ważniejszych spraw - “dependsOn”, czyli wskazanie co najpierw musi być stworzone, by stworzenie tego kawałka się powiodło.

/*Application Insights */
{
      "apiVersion": "2014-04-01",
      "name": "[parameters('applicationInsightsName')]",
      "type": "Microsoft.Insights/components",
      "location": "westeurope",
      "dependsOn": [
        "[resourceId('Microsoft.Web/sites/', parameters('websiteName'))]"
      ],
      "tags": {
        "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('websiteName'))]": "Resource",
        "displayName": "AppInsightsComponent"
      },
      "properties": {
        "ApplicationId": "[parameters('websiteName')]"
      }
    }

W podobny sposób deklarowana jest WebApp (która wymaga swojego hosting planu).

{
      "apiVersion": "2015-08-01",
      "name": "[parameters('websiteHostingPlanName')]",
      "type": "Microsoft.Web/serverfarms",
      "location": "[resourceGroup().location]",
      "tags": {
        "displayName": "HostingPlan"
      },
      "sku": {
        "name": "[parameters('websitePricingTier')]",
        "capacity": "[parameters('websiteInstanceNodeCount')]"
      },
      "properties": {
        "name": "[parameters('websiteHostingPlanName')]"
      }
    },
    {
      "apiVersion": "2015-08-01",
      "name": "[parameters('websiteName')]",
      "type": "Microsoft.Web/sites",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverFarms/', parameters('websiteHostingPlanName'))]"
      ],
      "tags": {
        "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('websiteHostingPlanName'))]": "empty",
        "displayName": "Website"
      },
      "properties": {
        "name": "[parameters('websiteName')]",
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('websiteHostingPlanName'))]"
      },
      "resources": [
        {
          "apiVersion": "2015-08-01",
          "type": "config",
          "name": "connectionstrings",
          "dependsOn": [
            "[resourceId('Microsoft.Web/Sites/', parameters('websiteName'))]",
            "[resourceId('Microsoft.Web/Sites/sourcecontrols', parameters('websiteName'), 'web')]"
          ],
          "properties": {
            "DefaultConnection": {
              "value": "[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers/', parameters('sqlServerName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('databaseName'), ';User Id=', parameters('databaseAdministratorLogin'), '@', parameters('sqlServerName'), ';Password=', parameters('databaseAdministratorPassword'), ';')]",
              "type": "SQLServer"
            },
            "BlogEngine": {
              "value": "[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers/', parameters('sqlServerName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('databaseName'), ';User Id=', parameters('databaseAdministratorLogin'), '@', parameters('sqlServerName'), ';Password=', parameters('databaseAdministratorPassword'), ';')]",
              "type": "SQLServer"
            }
          }
        },
        {
          "apiVersion": "2015-08-01",
          "type": "config",
          "name": "appsettings",
          "dependsOn": [
            "[resourceId('Microsoft.Web/Sites/', parameters('websiteName'))]",
            "[resourceId('Microsoft.Insights/components/', parameters('applicationInsightsName'))]",
            "[resourceId('Microsoft.Web/Sites/sourcecontrols', parameters('websiteName'), 'web')]"
          ],
          "properties": {
            "applicationInsightsInstrumentationKey": "[reference(resourceId('Microsoft.Insights/components', parameters('applicationInsightsName')), '2014-04-01').InstrumentationKey]"
          }
        },
        {
          "apiVersion": "2015-08-01",
          "name": "web",
          "type": "sourcecontrols",
          "dependsOn": [
            "[resourceId('Microsoft.Web/Sites', parameters('websiteName'))]"
          ],
          "properties": {
            "RepoUrl": "https://github.com/mgrabarz/BlogEngine.NET.git",
            "branch": "master",
            "IsManualIntegration": true
          }
        }
      ]
    },

Podczas tworzenie WebApp deklaruję elementy zagnieżdżone, jak appSettings lub connectionStrings. Mają one inne dependsOn, ponieważ ich wartości będą znane po utworzeniu innych zasobów (np parametry połączenia do bazy danych). Jedną z ciekawostek jest podsekcja sourcecontrols. Przy jej pomocy, po wdrożeniu infrastruktury z GitHub zaciągany jest kod aplikacji, kompilowany/testowany w locie i wgrywany do chmury. Wszystko w zależności od zadeklarowanych ustawień.

Identycznie wdrażana jest baza danych oraz reguły monitorujące i alerty – które też można deklarować w szablonach. Cały szablon znajdziesz tutaj, powinien on wystarczyć do wdrożenia większości nieskomplikowanych aplikacji ASP.NET do Azure.

Dodatkowe zmiany w aplikacji

Po wdrożeniu bazy w Azure doczytałem, że BlogEngine domyślnie przechowuje dane bloga w plikach na dysku. W samym Azure WebApp takie podejście by przeszło, powodowało nieco problemów szczególnie przy dużych plikach, nie gwarantowało wszystkich cech ACID transakcji. Spytacie dlaczego, otóż w chmurze mamy skalowanie, dane plikowe się replikują na poszczególne nogi naszego rozwiązania, czasami niektóre serwery się składają, a usługa podstawia nowe.

Zdecydowałem się na pójście drugą z możliwych opcji, czego sygnałem wcześniej jest stworzenie bazy w szablonie ARM. Wykonałem następujące zmiany w konfiguracji aplikacji i uświadomiłem sobie, że baza nie może być pusta tuż po wdrożeniu. Przy pełnym cyklu wdrożeniowym z VSTS nie stanowiłoby to problemu, ja chciałem zostawić jedynie repozytorium na GitHub. Z pomocą przyszła mi technika wdrażania plików bacpac bezpośrednio w szablonie. Tak jak bacpac używałem od dawna, tak nie spodziewałem się ich obsługi w ARM.

Czym są pliki bacpac? Można powiedzieć, że czymś zbliżonym do backupu bazy (struktura plus dane), z kolei pliki dacpac zawierają jedynie strukturę. Na tym różnice się nie kończą. Plik backupu traktowany jest jak czarne pudełko, nie możemy z nim niewiele zrobić poza odtworzeniem. Pliki bacpac/dacpac możemy używać w celu porównywania schematów bazy między sobą, a nawet wdrożenia różnicowego. I tak, możemy wdrożyć różnice między plikiem a bazą, między projektem bazy (w VisualStudio) a bazą, między projektem a plikiem itp. Więcej informacji na temat Data-Tier Applications znajdziesz tutaj.

W skrócie skonfigurowałem lokalną bazę, wgrałem potrzebne dane, zrzuciłem ją do bacpac’a, ten z kolei umieściłem w publicznie dostępnym miejscu. W szablonie, jako podelement szablonu bazy, dokonuję jej importu, zgodnie z zwartością bacpac:

"resources": [
            {
              "type": "extensions",
              "apiVersion": "2014-04-01",
              "properties": {
                "operationMode": "Import",
                "storageKey": "?",
                "storageKeyType": "SharedAccessKey",
                "administratorLogin": "[parameters('databaseAdministratorLogin')]",
                "administratorLoginPassword": "[parameters('databaseAdministratorPassword')]",
                "storageUri": "https://blogenginegithub.blob.core.windows.net/database/database.bacpac"
              },
              "name": "Import",
              "dependsOn": [
                "[resourceId('Microsoft.Sql/servers/databases', parameters('sqlServerName'), parameters('databaseName'))]"
              ]
            }
          ]
        }

Ostatnim krokiem przed wgraniem naszej aplikacji ASP.NET do Azure było skonfigurowanie ApplicationInsights w jej kodzie. W szablonie ARM propagowany jest do “application settings” klucz telemetryczny AI. AI tworzy się w pierwszej kolejności, a “dependsOn” settingów czeka, aż klucz będzie możliwy do pobrania. Proces konfiguracji w kodzie nieco się może różnić w moim przypadku, więc nie będę go tu opisywał, ale dobre wytyczne można znaleźć tutaj.

Podsumowanie

Powyższy proces, jak już wspominałem, powinien być zautomatyzowany w narzędziu takim jak VSTS, TeamCity czy Jenkins. Moim celem było skonfigurowanie wdrożenia w taki sposób, by jednym kliknięciem umieścić rozwiązanie w Azure. Poprzez użycie szablonów ARM, w tym deploymentu git i bacpac, cel został zrealizowany.

Jeżeli chcesz wrzucić BlogEngine.NET do swojej subskrypcji Azure, śmiało klikaj w poniższe guziki.

Deploy

ARM

Photo credit: john farrell macdonald via Visual Hunt / CC BY-SA

Leave a comment