Tuesday, December 27, 2011

Mobile & Global with HTML5, MVC & Windows Azure, Step 6: Cloud-Deployed

In this series of posts we’re progressively demonstrating a mobile and global sample, Responsive Tours. The source code for all 7 steps is on CodePlex at http://responsivetours.codeplex.com.

Here in Step 6 of 7 we’re going to deploy the solution to a Windows Azure data center. In this step we will:
• Migrate the local SQL Server database to a SQL Azure database in the cloud
• Migrate the promotional item image files to Blob Storage
• Update how we handle session cookies to be compatible with Windows Azure
• Package the application and publish it to the Windows Azure Compute Service
• Configure the Access Control Service for the hosted service

Migrating the Database to SQL Azure
Since Step 3 we’ve been using dynamic content for promotional items, driven by a Promotions table in a SQL Server database. Now we’ll need to move that over to a SQL Azure database in the cloud. To do that, we use the Windows Azure portal to create a virtual database server in the cloud. This involves selecting a data center, specifying an admin username/password, and setting up firewall rules.


Once the database server is created we need to create a Tours database. We can go with the smallest size (1GB) since we’re only storing a small amount of data in this sample.


From this point, working with the database is much like working with SQL Server. We have the choice of using familiar tools like SQL Server Management Studio or managing design and data through the SQL Azure portal. We need to design the Promotions table and migrate the promotional item records we previously created in a local database.



There’s one last item to attend to on the database. With our database now in SQL Azure, we must change the connection string in the web project’s Web.config to reference the cloud database.

  <connectionStrings>
    <add name="Tours" connectionString="Data Source=[MY-SQL-AZURE-SERVER].database.windows.net;Initial Catalog=Tours;UID=[MY-SQL-AZURE-USER]@[MY-SQL-AZURE-SERVER];PWD=[MY-SQL-AZURE-PASSWORD];" />
  </connectionStrings>
 
Migrating Images to Blob Storage

Up till now our promotional images have been part of the web project. To make them truly dynamic like the rest of the promotional content we should be able to change them out quickly and easily. We can achieve this by relocating the image files to Windows Azure Blob Storage, where they can be accessed as Internet URLs if we set appropriate permissions.

We first need to create a Windows Azure storage account in the Windows Azure portal. We’ll need to capture the storage account’s unique name and a storage key.



With the storage account created, we can create a container (allowing public read access) and upload our promotional images to it. Use a tool like Cerebrata Cloud Storage Studio or Azure Storage Explorer for this.


With the images in a new location we must change the promotional item markup on our HTML pages to match. We use the blob container’s URL format, [STORAGE-ACCOUNT-NAME].blob.windows.net/[CONTAINER-NAME]/[BLOB-NAME].
<!-- begin - homepage promos -->
<div class="home_promo_container">
 <div class="home_promo">
  <div class="home_promo_content" style="background-image:url(http://responsive.blob.core.windows.net/images/@(ViewBag.Promos["1"].ImageURL));">
   <h2 data-bind="text: PromoTitle1"></h2>
   <p  data-bind="text: PromoText1"/>
   <a class="button" href="#">Learn more &raquo;</a>
  </div>
 </div>
 <div class="home_promo">
  <div class="home_promo_content" style="background-image:url(http://responsive.blob.core.windows.net/images/@(ViewBag.Promos["2"].ImageURL));">
   <h2 data-bind="text: PromoTitle2"></h2>
   <p  data-bind="text: PromoText2"/>
   <a class="button" href="#">Learn more &raquo;</a>
  </div>
 </div>
 <div class="home_promo">
  <div class="home_promo_content" style="background-image:url(http://responsive.blob.core.windows.net/images/@(ViewBag.Promos["3"].ImageURL));">
   <h2 data-bind="text: PromoTitle3"></h2>
   <p  data-bind="text: PromoText3"/>
   <a class="button" href="#">Learn more &raquo;</a>
  </div>
 </div>
 <div class="clear_both"></div>

Updating Session Cookie Handling for Windows Azure
By default, our WIF-enabled ASP.NET MVC3 application is encrypting session cookies using the Data Protection API (DPAPI). DPAPI is not compatible with Windows Azure, so we need to add some code to encrypt cookies with RSA using a certificate. We do this in global.asax.cs.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Web;
using Microsoft.IdentityModel.Web.Configuration;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace html5_mvc_razor
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        /// Retrieves the address that was used in the browser for accessing 
        /// the web application, and injects it as WREPLY parameter in the
        /// request to the STS 
        /// </summary>
        void WSFederationAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
        {
            //
            // In the Windows Azure environment, build a wreply parameter for  the SignIn request
            // that reflects the real address of the application.
            //
            HttpRequest request = HttpContext.Current.Request;
            Uri requestUrl = request.Url;
            StringBuilder wreply = new StringBuilder();

            wreply.Append(requestUrl.Scheme);     // e.g. "http" or "https"
            wreply.Append("://");
            wreply.Append(request.Headers["Host"] ?? requestUrl.Authority);
            wreply.Append(request.ApplicationPath);

            if (!request.ApplicationPath.EndsWith("/"))
                wreply.Append("/");
            e.SignInRequestMessage.Reply = wreply.ToString();
        }


        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

            routes.MapRoute(
                "Map", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Map", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

        }

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            FederatedAuthentication.ServiceConfigurationCreated += OnServiceConfigurationCreated;
        }

        void OnServiceConfigurationCreated(object sender, ServiceConfigurationCreatedEventArgs e)
        {
            //
            // Use the <serviceCertificate> to protect the cookies that are
            // sent to the client.
            //
            List<CookieTransform> sessionTransforms =
              new List<CookieTransform>(new CookieTransform[] {
                new DeflateCookieTransform(), 
                new RsaEncryptionCookieTransform(e.ServiceConfiguration.ServiceCertificate),
                new RsaSignatureCookieTransform(e.ServiceConfiguration.ServiceCertificate) });
            SessionSecurityTokenHandler sessionHandler = new SessionSecurityTokenHandler(sessionTransforms.AsReadOnly());
            e.ServiceConfiguration.SecurityTokenHandlers.AddOrReplace(sessionHandler);
        }

    }
}


If we were using SSL for this site we'd use the SSL certificate. Since we're not, we upload our own certificate to the Windows Azure portal for this purpose and specify it in our Web.config.

  <microsoft.identityModel>
    <service>
      <audienceUris>
        <add value="http://MY-SERVICE-NAME.cloudapp.net/" />
      </audienceUris>
      <federatedAuthentication>
        <wsFederation passiveRedirectEnabled="true" issuer="https://[MY-ACS-NAMESPACE].accesscontrol.windows.net/v2/wsfederation" realm="http://MY-SERVICE-NAME.net/" requireHttps="false" />
        <cookieHandler requireSsl="false" />
      </federatedAuthentication>
      <applicationService>
        <claimTypeRequired>
          <!--Following are the claims offered by STS 'https://[MY-ACS-NAMESPACE].accesscontrol.windows.net/'. Add or uncomment claims that you require by your application and then update the federation metadata of this application.-->
          <claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" optional="true" />
          <claimType type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" optional="true" />
          <!--<claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" optional="true" />-->
          <!--<claimType type="http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider" optional="true" />-->
        </claimTypeRequired>
      </applicationService>
      <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <trustedIssuers>
          <add thumbprint="ED98B07917624AEE89EDDC086589A6149ADE6859" name="https://[MY-ACS-NAMESPACE].accesscontrol.windows.net/" />
        </trustedIssuers>
      </issuerNameRegistry>
      <serviceCertificate>
        <certificateReference storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint" findValue="308EFDEE6453FFF68C402E5ECEEE5B8BB9EAA619"/>
      </serviceCertificate>
      <certificateValidation certificateValidationMode="None" />
    </service>
  </microsoft.identityModel>


Publishing the Site to Windows Azure Compute
Back in Step 4 we set the site up for hosting in Windows Azure Compute but up till now we’ve only been running it locally in the Windows Azure Simulation Environment. Now we want to deploy the solution to a Windows Azure Data Center.

We first create a Hosted Service in the Windows Azure management portal. In our example we chose the South Central US data center and call the service responsive, which means its production URL will be http://responsive.cloudapp.net. Your name will be different as they must be unique.

After setting the number of VM instances in the Windows Azure project’s .cscfg file (to 2, the minimum for high availability), we are almost ready to deploy but we need to make one change to the project first relating to security.

Our impending change in deployment location will cause ACS authentication to fail unless we change our web project's configuration settings (which we'll do now) and ACS configuration (which we'll do later in this post). In the web project's Web.config file, change the realm attribute in the wsFederation element to reflect the new production URL.

<federatedAuthentication>
  <wsFederation passiveRedirectEnabled="true" issuer="https://[MY-ACS-NAMESPACE].accesscontrol.windows.net/v2/wsfederation" realm="http://MY-SERVICE-NAME.net/" requireHttps="false" />
  <cookieHandler requireSsl="false" />
</federatedAuthentication>


We can now proceed to package and deploy our solution, which can done directly from Visual Studio. We do this in Solution Explorer by right-clicking the Windows Azure project (ResponsiveSite.Azure) and selecting Publish. A wizard then guides us through the publishing action, which packages up the solution and its configuration, uploads both to the cloud, allocates VM instances, and deploys an image to them that includes our web site.

The Publish action can take 10-20 minutes, and when complete we’ll see the hosted service showing a status of Ready in the Windows Azure management portal.


Configuring Access Control Service for the Hosted Service
In Step 5 we configured the Windows Azure Access Control Service to allow our local development endpoint as a Relying Party. Now we need add another RP, our hosted service endpoint (http://responsive.cloudapp.net in our example) using the Windows Azure portal.

Running the Solution in the Cloud
All that remains now is try things out. Our hosted service in the cloud is accessed at a production URL based on the unique name we chose when the hosted service was created – http://[SERVICE-NAME].cloudapp.net. Our example deployment is at http://responsive.cloudapp.net. When we access this URL, we see the hosted service respond, now using a database in the cloud for promotional content and promotional images served up from blob storage. As in the past we first have to sign in with a web identity. In short, the site looks and acts like it has in previous steps except that it is now running in a Windows Azure data center accessible on the Internet.
Summary
In Step 6 we moved to the cloud, by hosting our web site in Windows Azure Compute, our promotional images in Blob Storage, and our promotional content in SQL Azure Database. Our site now has the following functionality:
• Embodies responsive web design and runs on desktops, tablets, and phones.
• Uses HTML5 and open standards on the web client
• Uses the Microsoft web platform on the web server.
• Provides server-side dynamic content (promotional items)
• Provides client-side dynamic content (Bing Maps)
• Is set up for Windows Azure Compute
• Can authenticate against web identities
• Is hosted in Windows Azure Compute
• Stores images in Windows Azure Blob Storage
• Stores promotional content in SQL Azure Database
We've come pretty far, but there's more we can do. In the next step, we'll deploy the application globally to multiple Windows Azure data centers around the world.

No comments: