Friday, March 23, 2018

Continuous deployment and delivery of Angular App with VSTS & Azure

Background

This tutorial describes processes and best practices for an end-to-end continuous deployment and delivery of an Angular Application.

Assumptions:

  •       Node.js, npm and angular-cli are installed on dev computer
  •          Angular is installed on your computer: npm install -g @angular/cli
  •          GIT is installed on the dev computer
  •          Developer has access to an account on VSTS (Visual Studio Team Services) at http://www.visualstudio.com
  •          Developer has an account on Azure (http://portal.azure.com)

Creating a simple Angular CLI app on dev computer:

Create an angular app using angular-cli:
ng new angular-hello-world
cd  angular-hello-world
ng serve
Point your browser to http://localhost:4200 and you should see the following:

Web.config file

In order for Angular’s routing to work smoothly on Azure, it is necessary to have a web.config file in the root folder of the application. Therefore, create a web.config file in the src directory and add to it the following code:

<?xml version="1.0"?>
<configuration>
   <system.webServer>
      <rewrite>
         <rules>
         <rule name="Angular Routes" stopProcessing="true">
            <match url=".*" />
            <conditions logicalGrouping="MatchAll">
               <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
               <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
               <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
            </conditions>
            <action type="Rewrite" url="/" />
         </rule>
         </rules>
      </rewrite>
   </system.webServer>
</configuration>

To enable the web.config file to be packaged with production code, edit the angular-cli.json file and add ‘web.config’ to assets, as follows:
. . . . .
"assets": [
  "assets",
  "favicon.ico",
  "web.config"
],
. . . . .

Build app

Build the app using the following command:

ng build --prod

This produces the production version of the application into the dist folder. To test the trans-piled version of the app, point your browser to http://localhost:4200/dist. You should see the same app as before, this time it is being served from production code.


 VSTS

VSTS is Microsoft’s DevOps platform. Login into VSTS and create a new project. In my case I created a new project named angular-hello-world.



Once the project is created in VSTS, it is time for us to push our code to VSTS using GIT. Copy the address of the GIT repo so that we can use it to sync our code.


Back at your computer, run the following commands from your command prompt to push the code into your projects VSTS git repository:

git remote add origin {your-git-url-here}
git push -u origin master
In my case, this would be:

git remote add origin https://medhat.visualstudio.com/DefaultCollection/_git/angular-hello-world
git push -u origin master
Click on Code >> angular-hello-world in VSTS to verify that your code is indeed in VSTS:


The code will look like this:


 Building the code in VSTS

The same steps that we carried out to build our app on the development computer will be translated into tasks in VSTS. To build our app, choose: Build & Release >> Builds:


Click on the “+ New definition” button:



On the “Select a template” page, scroll to the bottom of the list and highlight item “Empty”, then click Apply:


Select “Hosted” in the “Default agent queue” drop-down-list:


Next, click on “+ Add Task” on the left side:


In the filter, enter npm. Highlight the npm task then click on Add:



This task will install all the npm dependencies:


The above task runs the command “npm install”. You do not need to make any changes to this task as it does exactly what we want it to do.

Add another npm task to run “npm run build” command, which is essentially a script in our package.json file:

  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
The second npm task should look like this:


Display name: build
Command: custom
Command and arguments: run build --prod


 Create release package

Click on the ‘Add Task’ button, enter “archive” in the search box, hover over the Archive Files task and click Add. Change the “Archive files” task as follows:


Display name: create release package
Root folder (or file) to archive: dist
Prefix root folder name to archive paths: UNCHECK


 Publish release package

Next, we publish the zip we created in the last step. Click on the Add Task button, enter “publish build” in the search box, hover over the Publish Build Artifacts task and click Add:




Display name: Publish release package
Path to Publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
Artifact Name: drop
Artifact Type: Server
That’s it for all our steps. Let’s run the build! Click on Save and Queue >> Save and Queue in the top right hand corner.


On the next dialog, click on “Queue”

Now the build will run for us. While that is happening let us turn on continuous integration so that when we make changes in our repository it will trigger a new build.

Click on the build number link in the in the top left-side corner. This will allow you to see progress of the build. When the build is completed without any errors, you will see a green “Build Succeeded” message as shown below:


Artifacts

You can see the package that was created by selecting the Artifacts tab:



Download the drop folder to see what was created inside that directory.

Continuous Integration

We will need to setup continuous integration so that whenever new code is committed then the build process is kicked off. Click on the build as shown below:



Click “Edit” on the top right-side:


On the next page, click on “Triggers”:



Enable continuous integration by enabling the switch:



Click: Save & Queue >> Save:



On the next dialog, add a comment then click Save:



Creating Release

Log into Azure and create a web app.



Create a release definition by clicking on Build & Release >> Releases:


Next, click on the blue “+ New definition” button. Under ‘Select a template select Azure App Service Deployment then click Next >:



On the next dialog, make sure that you have the right build name and enable “Continuous deployment”:



Click on “Create”:

Choose your Azure subscription then click on the blue Authorize button:


After you log into your Microsoft account, the border around the “Azure subscription” field will change from red gray. Choose the correct azure web app in the “App Service name” field. Your release task will look like this:



Click Save:



You can enter a comment in the following dialog then click on OK.



Click on Release >> Create Release:


This displays the following type of dialog:



Choose the correct build that will be used for the release. In the above example it is 40.

Click on Create.



Click on the link beside the definition file to see progress:


If all is well, the blue “IN PROGRESS” bar will change to a green “SUCCESS” bar.

Friday, March 16, 2018

Dockerising both ASP.NET Core 2.0 and SQL Server Express in Windows containers

In a previous post I showed how you can have an ASP.NET Core 2.0 application work with a containerized SQL Server Express database server running in a Windows container. In this post I will take this one step further by also containerizing the ASP.NET Core 2.0 application.

Please go through my previous post before continuing with this tutorial.

We will generate the release version of the application by executing the following command from a terminal window in the root directory of your ASP.NET 2.0 project:

dotnet publish --framework netcoreapp2.0 --configuration Release --output dist

The above command instructs the dotnet utility to produce the release version of the application in the dist directory. This results in output similar to the following:

Microsoft (R) Build Engine version 15.6.82.30579 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 340.14 ms for E:\_aspnet\_docker\AspCoreSqlExpress\AspCoreSqlExpress.csproj.
  Restore completed in 340.57 ms for E:\_aspnet\_docker\AspCoreSqlExpress\AspCoreSqlExpress.csproj.
  Restore completed in 339.79 ms for E:\_aspnet\_docker\AspCoreSqlExpress\AspCoreSqlExpress.csproj.
  Restore completed in 346.67 ms for E:\_aspnet\_docker\AspCoreSqlExpress\AspCoreSqlExpress.csproj.
  AspCoreSqlExpress -> E:\_aspnet\_docker\AspCoreSqlExpress\bin\Debug\netcoreapp2.0\AspCoreSqlExpress.dll
  AspCoreSqlExpress -> E:\_aspnet\_docker\AspCoreSqlExpress\dist\

If you inspect the dist directory, you will see content similar to the following:


The highlighted file in the above folder screen capture is my main DLL file that is the entry point into the web application.

Let us run the release version of the web application. To do this, change directory to the dist directory with the following terminal instruction:

cd dist

You can then run your main DLL file. In my case, this file is AspCoreSqlExpress.dll. I executed the following command:

dotnet AspCoreSqlExpress.dll

This generates an understandable error, if the SQL Server Express container is not running, because the application is unable to connect to a database:

Application startup exception: System.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server) ---> System.ComponentModel.Win32Exception (0x80004005): The network path was not found

Contents of the /dist folder give us a good idea about ASP.NET Core 2.0 artifacts that need to be copied into a container. We shall simply copy contents of the dist directory into a Docker container that has the .NET Core 2.0 runtime.

Return to the root directory of your project by typing the following in a terminal window:

cd ..

Code in Startup.cs will need to be modified so that we can pass environment variables into the ASP.NET Core application. Therefore, open Startup.cs in your favorite editor and find method ConfigureServices(). Comment out (or delete) the following code:

services.AddDbContext<ApplicationDbContext>(options =>
   options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

Replaced the above code with:

var host = Configuration["DBHOST"] ?? "localhost";
var db = Configuration["DBNAME"] ?? "AspCoreSqlExpress";
var port = Configuration["DBPORT"] ?? "1433";
var username = Configuration["DBUSERNAME"] ?? "sa";
var password = Configuration["DBPASSWORD"] ?? "Sql!Expre55";

string connStr = $"Data Source={host},{port};Integrated Security=False;";
connStr += $"User ID={username};Password={password};Database={db};";
connStr += $"Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connStr));

Five environment variables are used in the database connection string. These are: DBHOST, DBNAME, DBPORT, DBUSERNAME and DBPASSWORD. If these environment variables are not found then they will take up default values: localhost, AspCoreSqlExpress, 1433, sa and Sql!Expre55 respectively.

We need to create a Windows docker image that will contain the .NET Core 2.0 runtime. A suitable image for this purpose is: microsoft/aspnetcore:2.0-nanoserver-sac2016

Create a text file named Dockerfile.windows and add to it the following content:

FROM microsoft/aspnetcore:2.0-nanoserver-sac2016
COPY dist /app
WORKDIR /app
EXPOSE 80/tcp
ENV ASPNETCORE_URLS http://+:80
ENTRYPOINT ["dotnet", "AspCoreSqlExpress.dll"]

Above are instructions to create a Windows Docker image that will contain our ASP.NET Core 2.0 application. I explain each line below:

FROM microsoft/aspnetcore:2.0-nanoserver-sac2016 Base Windows image microsoft/aspnetcore:2.0-nanoserver-sac2016 will be used
COPY dist /app Contents of the dist directory on the host computer will be copied to directory /app in the container
WORKDIR /app The working directory in the container is /app
EXPOSE 80/tcp Port 80 will be exposed in the container
ENV ASPNETCORE_URLS http://+:80 Expose port 80 for the Web API traffic
ENTRYPOINT ["dotnet", "AspCoreSqlExpress.dll"] The main ASP.NET Core 2.0 web application will be launched by executing "dotnet AspCore4Docker.dll"

There is a risk that the web application container (mvc) starts earlier than the SQL Server container. We need to figure out a way to have the web application wait for the database server to be ready. We can use a PowerShell script to accomplish this task. Create a PowerShell file named wait4port1433.ps1 in the root of the ASP.NET Core 2.0 web application. Add to it the following script:

do {
  Write-Host "waiting..."
  sleep 3      
} until(Test-NetConnection db -Port 1433 | ? { $_.TcpTestSucceeded } )

dotnet AspCoreSqlExpress.dll

The above PowerShell script waits until port 1433 is available on the db container. Once the port is available then it will start the web application by running dotnet demo-docker.dll.

Open the .csproj file and add this markup just before the closing </Project> tag:

<ItemGroup>
   <Content Include="wait4port1433.ps1" CopyToOutputDirectory="Always" />
</ItemGroup>

The above markup ensures that the PowerShell script file is published together with all other required files into the dist folder. After saving the .csproj file, remove the dist folder and re-publish your web application by running the following pair of commands from a terminal window:

rmdir dist /S /Q
dotnet publish --framework netcoreapp2.0 --configuration Release --output dist

We will next compose a docker yml file that orchestrates the entire system which involves two Windows containers: a SQL Server Express database container and a container that holds our web application. In the root folder of your application, create a text file named docker-compose.yml and add to it the following content:

version: '3'

services:
  db:
    image: microsoft/mssql-server-windows-express:2016
    volumes:
       - sqldatavol:c:\sqldata
    restart: always
    networks:
      - aspsqlnet
    environment:
      - sa_password=Sql!Expre55
      - ACCEPT_EULA=Y

  mvc:
    build:
      context: .
      dockerfile: Dockerfile.windows
    depends_on:
      - db
    networks:
      - aspsqlnet
    ports:
      - "8888:80"
    #restart: always
    environment:
      - DBHOST=db
      - DBNAME=sql-docker
      - DBPORT=1433
      - DBUSERNAME=sa
      - DBPASSWORD=Sql!Expre55
      - ASPNETCORE_ENVIRONMENT=Development
    entrypoint: powershell "c:\app\wait4port1433.ps1"

volumes:
  sqldatavol:

networks:
  aspsqlnet:

Below is an explanation of what this file does.

We will have two Windows containers. Each container is considered to be a service. The first service is named db and will host SQL Server Express. The second service is named mvc and will host our ASP.NET Core 2.0 web application.

The most current version of docker-compose is version 3. This is the first line in our docker-compose.yml file.

The SQL Server Express Container

image
Image microsoft/mssql-server-windows-express:2016 will be used for the SQL Server Express container.

volumes
A folder on the host machine named sqldatavol is mapped to folder c:\sqldata in the container. This is so that data is persisted on the host machine in case the container is destroyed as we do not want to lose valuable data.

restart
restart: always is so that if the container stops, it is automatically restarted.

networks
The db container will exist in a virtual network named aspsqlnet.

environment
The sa password will be Sql!Expre55 when SQL Server Express is configured. This is set by the sa_password environment variable. Environment variable ACCEPT_EULA pertains to acceptance of the End-User Licensing Agreement, which is a required setting for the SQL Server image.

The ASP.NET Core 2.0 Web Application Container

build
The container will be built using the instructions in the Dockerfile.windows file and the context used is the current directory.

depends_on
depends_on indicates that the web application relies on the SQL Server Express container (db) to properly function.

networks
The mvc container will also exist in a virtual network named aspsqlnet.

ports
Port 80 in the mvc container is mapped to port 8888 on the host computer.

environment
The environment variables needed by the web app are:

DBHOST pointing to the SQL Server Express service
- DBNAME is the name we wish to give to the database
- DBPORT is the port number that SQL Server Express is listening on
- DBUSERNAME is the database server user-name. This is set to the admin user-name 'sa'
- DBPASSWORD is the password for user-name 'sa'.
- ASPNETCORE_ENVIRONMENT is set to Development. In reality, you should change this to Production once you determine that your web application container works as expected.

entrypoint
We override the entry point declared in Dockerfile.windows. This entry point calls the PowerShell script file wait4port1433.ps1 that waits for port 1433 to exist. Once it becomes available, it then starts the web application by running dotnet demo-docker.dll.

Running the yml file

To find out if this all works, go to a terminal window in the root folder of the ASP.NET Core 2.0 application and run the following command:

docker-compose up

Point your browser to http://localhost:8888/ and you should see the main web page. To ensure that the database works properly, register a user by clicking on the Register link in the top right corner.

In my case, I received confirmation that a user was indeed registered:


As you can see in the top-right corner, the user with email a@a.a has been successfully registered.

To shutdown the two containers container, go to a terminal window in the root folder of the ASP.NET Core 2.0 application and run the following command:

docker-compose down

This opens up a whole new world for containerizing your ASP.NET Core 2.0 web applications in Windows Docker containers.

References: