Showing posts with label Linux. Show all posts
Showing posts with label Linux. Show all posts

Saturday, March 26, 2022

Deploy DB driven Laravel web app to Azure App Service running on Linux NGINX web server through GitHub Actions

Laravel is an open-source PHP framework designed to make developing web apps mush easier and faster through built-in features. It is based on the MVC design pattern.

In this article, I will show you how to deploy a Laravel application to Azure App Services. The Azure web server is NGINX running on Linux version "Debian GNU/Linux 10 (buster)".

Assumptions

The following is assumed:

  • You have Git, PHP and Composer installed on your computer.
  • You have an Azure subscription
  • You have a GitHub account

Getting Started

We will start our journey with a very simple Laravel application that lists some data from a SQLite database. Go into a working directory on your computer and run the following command from a terminal window to clone a GitHub repo:

git clone https://github.com/medhatelmasry/laravel-azure.git

It is a good idea to delete the .git folder on your computer.

Once the application is cloned, change directory to the cloned app and start your app as shown below:

cd laravel-azure
composer install
php artisan serve --port=8888

The web server will start on your computer listening on port 8888.

Starting Laravel development server: http://127.0.0.1:8888
[Sat Mar 26 15:36:29 2022] PHP 8.1.2 Development Server (http://127.0.0.1:8888) started

Point your browser to http://localhost:8888/ and you will see the following page:


For demo purposes, data is pulled from a SQLite database file at database/autos.db

Disclaimer: It is normal practice that the .env file is excluded from being pushed to source control. If you look at the .gitignore file, the line pertaining to .env is commented out because there is really no confidential information in this file.

Create a repository in your GitHub account and push the source code to it.

Create Azure App Service

Login to your Azure account by visiting https://portal.azure.com. Enter "app services" in the filter field, then click on "App Services".



Click on "+ Create" on the top left-side.

On the "Create Web App Page", choose. your subscription then create new resource group.



Give your app a suitable host name that is unique.


Next, ensure these settings for Publish, Runtime stack and Operating System:


Choose suitable values for the remaining settings based on your individual preference then click on the blue "Review + create" button:


Click on Create button after you have reviewed your. choices.


Once your app service is successfully provisioned, you can click on the "Go to resource" button.


You can see a default web page to your web app be clicking on the URL link on the top right-side.


The default page looks like this.

\

CI/CD pipeline

Of course, we want to deploy our Laravel PHP site from our GitHub repo. An easy way to achieve this is to use Azure's "Deployment Center". Click on "Deployment Center" in the left-side navigation.

In the "Deployment Center" blade, select GitHub.


If this is your first time to connect Azure to your GitHub account, you will be asked to go through the GitHub authentication process. Thereafter, select the appropriate GitHub Organization, Repository and Branch. My experience was:


Once you click on Save, the deployment process commences.


To go to your GitHub repo, click on the main link under Branch.

To see the deployment in action, click on Actions in your GitHub repository.


Click on the workflow in progress.


The workflow will be going through a build and deploy process. 


Be patient as it takes about 15 minutes to complete. This is because during build the command "composer install" is executed. This produces a multitude of files under the vendors folder, which are thereafter sent to Azure. Once the workflow is completed, your GitHub workflow page will look like this:


The workflow files were automatically generated and placed a .yml file in the .github/workflows folder with your source code.


You can click on the .yml file to see what the file looks like.


At this point, it is worth going back to the code on your computer and doing a pull of your code so that you get a copy of the .yml file that was added to your source code.

Configuring app on Azure

Back on the Azure portal, if you refresh the default page of our web app, you will experience a "403 Forbidden" message, which typically means that the root directory has no welcome page.




This is understandable because the main index.php page in Laravel resides in the /public folder. THis means that we need to do some configuration work on Azure. 

In Azure, enter the term advanced in the search input field then click on "Advanced Tools".


Click "Go -->".


A page opens up in a new browser tab. Click on SSH on the top menu.


A Linux terminal window is open.



If you are interested to know what version of Linux this is, enter the following terminal command:

cat /etc/os-release

The value beside PRETTY_NAME is the Linux distribution and version.

PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

We will make a copy of the existing nginx configuration and place the file inside the /home/site directory with the following command:

cp /etc/nginx/sites-available/default /home/site/default

Once copied, edit the /home/site/default file with nano or vi. I will use nano.

nano /home/site/default

Make the following changes:

FromToAround Line #
root /home/site/wwwrootroot /home/site/wwwroot/public6
location / {
   index  index.php index.html index.htm hostingstart.html;
}
      
location / {
   index  index.php index.html index.htm hostingstart.html;
   try_files $uri $uri/ /index.php?$query_string;
}
      
10


In nano, hit CTRL X to save, enter Y then hit return.

We need to create a bash script file that overrides the existing default file with our customized version, then restart the server. Change directory to the site folder.

cd site

Inside the site directory, using vi or nano, create a file named startup.sh. To create a file with nano, type "nano startup.sh". Otherwise,  to create a file with vi, type "vi startup.sh". Add the following content to startup.sh:

#!/bin/bash

cp /home/site/default /etc/nginx/sites-available/default
service nginx reload

We will make our bash script file executable with:

chmod u+x startup.sh

While in the terminal window on Azure, let's visit the folder that contains our Laravel application. Do this by going to the wwwroot folder.

cd wwwroot
ls -a

This reveals the existence of all the files that we had on GitHub plus files in the vendor folder.


Navigate back to your App Service via the Azure Portal. Select Configuration in the Settings section.


Click on the "General Settings" tab, enter "/home/site/startup.sh" for the "Startup Command", then click on Save.


Click on blue Continue button the the "Save changes" prompt.


Now, when you refresh the website, you should see that our web app is working as expected.

Conclusion

We have successfully deployed a database driven Laravel PHP application to Azure App Services through GitHub. I hope you use the principles you learned in this tutorial to deploy much more interesting Laravel applications.

Saturday, February 27, 2021

Build REST API using EF with Azure Functions v3 & .NET Core 3.1

In this tutorial I will demonstrate how to build a REST API application using Azure Functions v3. The application we will build together uses Entity Framework Core Migrations, Dependency Injection and .NET Core 3.1. We will use the light-weight VS Code editor so that you can go through this tutorial on Windows 10, Mac or Linux.

Source code: https://github.com/medhatelmasry/AzureFunctionsEF.git

Install the appropriate Azure CLI for your operating system from https://docs.microsoft.com/en-us/cli/azure/install-azure-cli.

You need to install the Azure Functions extension for Visual Studio Code before proceeding with this tutorial. Once the extension is installed, you will find it among your extensions.


In your working directory, create the following folder structure:


Inside the AzureFunctionsEF directory, execute the following terminal window commands:

dotnet new sln
dotnet new classlib -f netcoreapp3.1 -o DataLayer
dotnet sln add DataLayer/DataLayer.csproj

Start Visual Studio Code.  Under the Functions tab, select your Azure subscription. You will then be able to create a new Azure Functions project.


Choose the AzureFunctionsEF/Functions folder.


Select C#.
Select HttpTrigger.


Name your Azure function HttpWebAPI.


Give your function app the namespace Snoopy.Function.



Select Anonymous for access rights.



Finally, select "Open in current window".

Let us see what the app does. Enter the following in the terminal window inside the AzureFunctionsEF directory:

cd Functions
func start

The following will appear:


Copy the URL (http://localhost:7071/api/HttpWebAPI) and paste it in the address line of your browser. You will see a response like this:


The message in your browser suggests that you should pass a name query string. I appended the following to the URL: ?name=Superman. I got the following result:


Hit CTRL C to terminate the running app.

Back in the terminal window inside the AzureFunctionsEF directory, execute the following commands:

dotnet sln add Functions/Functions.csproj
dotnet add Functions/Functions.csproj reference DataLayer/DataLayer.csproj


DataLayer class library project

We will work on the DataLayer project by adding a Student class, an Entity Framework database context class, a connection string, and a class that is capable of reading configurations settings.

Add the following packages to the DataLayer project by executing the following commands in a terminal window inside the DataLayer folder:

dotnet add package Microsoft.EntityFrameworkCore -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.Tools -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.Design -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 3.1.9
dotnet add package Microsoft.Extensions.Configuration.Json -v 3.1.9
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables -v 3.1.9 

Open the DataLayer folder in Visual Studio Code.

Delete DataLayer/Class1.cs.

Add a Models folder. Inside the Models folder, add a C# Student class file with the following code:

public class Student {
  public string StudentId { get; set; }
  [Required]
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
  [Required]
  public string School { get; set; }
}

Also, in the Models folder, add another C#  Config class file with the following code:

public static class Config {
    private static IConfiguration configuration;
    static Config() {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
            .AddEnvironmentVariables();

        configuration = builder.Build();
    }

    public static string Get(string name) {
        string appSettings = configuration[name];
        return appSettings;
    }
}

Now let us add a settings file named local.settings.json right inside the root DataLayer folder with the following content:

{
  "DefaultConnection":"Server=(localdb)\\mssqllocaldb;Database=SchoolDB;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Since we are using Entity Framework, we need to add a database context class. Add the following ApplicationDbContext.cs class to the DataLayer root directory:

public class ApplicationDbContext : DbContext {
  public DbSet<Student> Students { get; set; }

  public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

  public ApplicationDbContext(DbContextOptions options) : base(options) { }

  public ApplicationDbContext() : base() { }

  protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer(Config.Get("DefaultConnection"));

  protected override void OnModelCreating(ModelBuilder builder) {
    base.OnModelCreating(builder);

    builder.Entity<Student>().HasData(
      new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "Jane",
        LastName = "Smith",
        School = "Medicine"
      }, new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "John",
        LastName = "Fisher",
        School = "Engineering"
      }, new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "Pamela",
        LastName = "Baker",
        School = "Food Science"
      }, new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "Peter",
        LastName = "Taylor",
        School = "Mining"
      }
    );
  }
}

Now that we have out models and database context classes in place, let us go ahead and run Entity Framework Migrations. In a terminal window inside the DataLayer root folder, execute the following commands:

dotnet-ef migrations add M1 -o Data/Migrations
dotnet-ef database update

At this point, if all goes well, the database would be created and seeded with sample data.


Azure Functions Project

Add the following package to the Functions project by executing the following command in a terminal window inside the Functions folder:

dotnet add package Microsoft.Extensions.Http -v 3.1.10

Open the Functions folder in Visual Studio Code.

Add the following connection string to local.settings.json.

"DefaultConnection":"Server=(localdb)\\mssqllocaldb;Database=SchoolDB;Trusted_Connection=True;MultipleActiveResultSets=true",

We will use dependency injection to access the database context and HttpClient objects. Therefore, create a Startup.cs file in the root Functions folder and add to it this code:

[assembly: WebJobsStartup(typeof(StartUp))]
namespace Functions {
  public class StartUp : IWebJobsStartup {
    public void Configure(IWebJobsBuilder builder) {
      builder.Services.AddDbContext<ApplicationDbContext>(options1 => {
        options1.UseSqlServer(
          Config.Get("DefaultConnection"),
          builder =>
          {
            builder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null);
            builder.CommandTimeout(10);
          }
        );
      });

      builder.Services.AddHttpClient();
    }
  }
}


Delete the static keyword from the class declaration of HttpWebAPI class:

public static class HttpWebAPI

We will use dependency injection inside our Functions class to access the database context and HttpClient objects. Therefore, add these instance variables and constructor to HttpWebAPI.cs:

private readonly HttpClient _client;
private readonly ApplicationDbContext _context;

public HttpWebAPI(IHttpClientFactory httpClientFactory,
    ApplicationDbContext context) {
    _client = httpClientFactory.CreateClient();
    _context = context;
}

Add the following method to the HttpWebAPI.cs class:

[FunctionName("GetStudents")]
public IActionResult GetStudents(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "students")] HttpRequest req,
ILogger log) {
  log.LogInformation("C# HTTP GET/posts trigger function processed a request.");

  var studentsArray = _context.Students.OrderBy(s => s.School).ToArray();

  return new OkObjectResult(studentsArray);
}

All that is left for us to find out is whether or not our API works. Hit CTRL F5 inside the Visual Studio Code.


CTRL Click with your mouse on http://localhost:7071/api/students. It should open up a browser window with the following data:


It is left up to you to complete this tutorial with POST, PUT and DELETE functionality.

Sunday, March 4, 2018

Dockerizing a Node Express API app with MongoDB in a Linux container

Node, Express and MongoDB work very well together. Put Docker into the mix and you have a very interesting cocktail.

In this post we shall develop a Node Express app from scratch and then containerize it using Docker. Before we start, you will need to ready your environment with the following pre-requisites for your specific operating system:

- Install Node.js from https://nodejs.org/en/download/
- Install MongoDB from https://www.mongodb.com/download-center#community
- Install Docker from https://docs.docker.com/install/
- Optionally, you can install Visual Studio Code from https://code.visualstudio.com/Download

In a terminal window, go to a suitable location on your computer's hard drive and create a directory named nodemongo:

mkdir nodemongo

Change into the nodemongo directory:

cd nodemongo

Let us initialize a node application. This is done by running the following command:

npm init

Respond as below:

package name: [Hit ENTER]
version: [Hit Enter]
description: Dockerizing node/express/mongo app
entry point: server.js
test command: [Hit Enter]
git repository: [Hit Enter]
keywords: [Hit Enter]
author: [Enter your name]
license: [Hit Enter]

The package.json file is displayed and you are asked to confirm. Enter y to confirm.

This is what my project.json file looks like:

{
  "name": "nodemongo",
  "version": "1.0.0",
  "description": "Dockerizing node/express/mongo app.",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Medhat Elmasry",
  "license": "ISC"
}

We will need two node packages: express and mongojs. Install these packages with the following command:

npm install express mongojs

I am using Visual Studio Code to edit my application. Use whatever editor you are familiar with like sublime, atom, brackets, etc....

Create a file named server.js in the nodemongo folder and add to it the following code:

var express = require("express");
var users = require("./routes/");
var config = require('./config');

var app = express();

app.use("/", users);

app.listen(config.port, function() {
    console.log("Server started on port " + config.port)
});

Lines 2 and 3 above suggest that we are missing folders /routes and /config. Therefore, create folders routes and config inside the nodemongo directory.

Inside the /config directory, create a JavaScript file named index.js and add to it the following code:

module.exports = {  
    // Setting port for server
    'port': process.env.PORT || 3000,
    
    // Setting mongo host name
    'mongo_host': process.env.MONGO_HOST || 'localhost',

    // Setting mongo port number
    'mongo_port': process.env.MONGO_PORT || 27017,

    // Setting mongo database name
    'mongo_database': process.env.MONGO_DATABASE_NAME || 'demodb',
};

The above code configures environment variables for: (1) the server listening port number, (2) the MongoDB host name, (3) the MongoDB listening port number and (4) the MongoDB database name. 

Inside the /routes directory, create another JavaScript  file named index.js and add to it the following code:

var express = require("express");
var mongojs = require('mongojs');
var config = require('../config');

var connStr = 'mongodb://' + config.mongo_host;
connStr += ':' + config.mongo_port;
connStr += '/' + config.mongo_database;

var db = mongojs(connStr, ['students']);
var usersCollection = db.collection('users');

var router = express.Router();

router.get('/', (req, res, next) => {
    usersCollection.count({},(err, docCount) => {
        if (docCount == 0) {
            const users = [
                {"name":"user_1","email":"user_1@bogus.com"},
                {"name":"user_2","email":"user_2@bogus.com"},
                {"name":"user_3","email":"user_3@bogus.com"}            
            ];
        
            // use the Event model to insert/save
            usersCollection.save(users, (err, data) => {
                if (err) {
                    res.send(err);
                }
            })
    
        }
    })

    usersCollection.find( (err, data) => {
        if (err)
            res.send(err);
        
        res.json(data);
    })

});

router.get('/add', (req, res, next) => {
    var name = 'user_'+ Math.floor(Math.random() * 1000);
    var email = name + '@bogus.com';

    var doc = {'name': name, 'email': email};
    usersCollection.save(doc, (err, data) => {
        if (err) {
            res.send(err);
        }
        res.json(data);
    })
});

// get single user
router.get("/users/:id", (req, res, next) => {
    usersCollection.findOne({_id: mongojs.ObjectId(req.params.id)},function(err, data){
        if (err) {
            res.send(err);
        }
        res.json(data);
    });
});

module.exports = router;

The above code sets up the following API endpoints:

http://localhost:3000/ Displays all documents in the users collection. If the users collection is empty then it will be seeded with three documents.
http://localhost:3000/add Adds a user document with a randomly generated name and email
http://localhost:3000/users/:id Displays a singe document by MongoDB object id

If the MongoDB database server has not started already, start it. At this point, we are able to test our application and make sure it is working as expected.

After starting the database, launch the application by executing the following terminal command:

npm start

You should see the following message in the terminal window:

> nodemongo@1.0.0 start D:\demo\_docker\nodemongo
> node server.js

Server started on port 3000

This indicates that our node web server  is running and listening on port number 3000. Point your browser to http://localhost:3000/. You should see the following seeded data comprising of three JSON objects in your browser:

[{"_id":"5a9b8f225e78082ddccba361","name":"user_1","email":"user_1@bogus.com"},{"_id":"5a9b8f225e78082ddccba362","name":"user_2","email":"user_2@bogus.com"},{"_id":"5a9b8f225e78082ddccba363","name":"user_3","email":"user_3@bogus.com"}]

Try adding a user by pointing to http://localhost:3000/add. It should randomly generate a user document similar to the following:

{"name":"user_20","email":"user_20@bogus.com","_id":"5a9b8fdd5e78082ddccba364"}

If you try http://localhost:3000/ again, you should see four JSON objects similar to the following:

[{"_id":"5a9b8f225e78082ddccba361","name":"user_1","email":"user_1@bogus.com"},{"_id":"5a9b8f225e78082ddccba362","name":"user_2","email":"user_2@bogus.com"},{"_id":"5a9b8f225e78082ddccba363","name":"user_3","email":"user_3@bogus.com"},{"_id":"5a9b8fdd5e78082ddccba364","name":"user_20","email":"user_20@bogus.com"}]

Lastly, you can get a document by MongoDB object ID with endpoint http://localhost:3000/users/:id. Copy one of the IDs and and it to the address line. I used one of my IDs and pointed my browser to the following endpoint: http://localhost:3000/users/5a9b8f225e78082ddccba361 and experienced the following response:

{"_id":"5a9b8f225e78082ddccba361","name":"user_1","email":"user_1@bogus.com"}

Of course, you will not have the same ID as I do and should use one of the IDs in your users documents.

At this stage, we have determined that our application works as expected. The next and final step is to containerize both MongoDB and our Node/Express API application.

Stop both the Node web server and the MongoDB database server.

Create a text file named .dockerignore in the root of your node project and add to it the following content. This instructs Docker not to copy the contents of node_modules into the container:

node_modules

Create a text file named DockerFile in the root folder of your application and add to it the following:

FROM node:9.7.1
WORKDIR /app
COPY package.json /app/package.json
RUN npm install
COPY . /app
EXPOSE 3000

Above are instructions to create a Docker image that will contain our Node/Express API web application. I describe each line below:

FROM node:9.7.1 Base image node:9.7.1 will be used
WORKDIR /app The working directory in the container is /app
COPY package.json /app/package.json package.json is copied to /app in the container
RUN npm install  This is run inside the containers /app directory
EXPOSE 3000 Port 3000 will be exposed in the container

Create another text file named docker-compose.yml in the root of your node project, and add to it the following content:

version: "3"

services:

  db:
    image: mongo:3.6.3
    ports:
      - "27017:27017"
    restart: always

  web:
    build: 
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    links:
      - db
    environment:
      - MONGO_HOST=db
      - MONGO_PORT=27017
      - MONGO_DATABASE_NAME=demodb
    command: node /app/server.js

Below is an explanation of what this file does.

We will be having two containers. Each container is considered to be a service. The first service is named db and will host MongoDB. The second service is named web and will host our Node/Express API app.

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

The MongoDB Container

Image mongo version 3.6.3 will be used for the MongoDB container.

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

Port 27017 is the default MongoDB listening port. This port number, inside the container, is being mapped to the same port number outside the container.

Node/Express API app Container

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

Port 3000 in the web container is mapped to port 3000 on the host computer.

The environment variables needed by the web app are:

- MONGO_HOST pointing to the MongoDB service name
- MONGO_PORT sets the MongoDB listening port number
- MONGO_DATABASE_NAME is the name which we want to use for the database

Running the yml file

To find out if this all works, go to a terminal window and run the following command:

docker-compose -f docker-compose.yml up

Point your browser to http://localhost:3000/ and you should see three seeded users JSON objects as below:


You can view the two running containers by executing the following Docker command in a terminal window:

docker ps -a

You will see something similar to this:

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
979d458c9ba3        nodemongo_web       "node /app/server.js"    21 minutes ago      Up 20 minutes       0.0.0.0:3000->3000/tcp     nodemongo_web_1
ae80a1b9283c        mongo               "docker-entrypoint.s…"   21 minutes ago      Up 21 minutes       0.0.0.0:27017->27017/tcp   nodemongo_db_1

To shutdown and remove thes two containers, run the following Docker command:

docker-compose -f docker-compose.yml down

Thanks for coming this far in my tutorial and I hope you found it useful.