Posts Tagged nginx
Architecting Cloud-Optimized Apps with AKS (Azure’s Managed Kubernetes), Azure Service Bus, and Cosmos DB
Posted by Gary A. Stafford in Azure, Cloud, Java Development, Software Development on December 10, 2017
An earlier post, Eventual Consistency: Decoupling Microservices with Spring AMQP and RabbitMQ, demonstrated the use of a message-based, event-driven, decoupled architectural approach for communications between microservices, using Spring AMQP and RabbitMQ. This distributed computing model is known as eventual consistency. To paraphrase microservices.io, ‘using an event-driven, eventually consistent approach, each service publishes an event whenever it updates its data. Other services subscribe to events. When an event is received, a service (subscriber) updates its data.’
That earlier post illustrated a fairly simple example application, the Voter API, consisting of a set of three Spring Boot microservices backed by MongoDB and RabbitMQ, and fronted by an API Gateway built with HAProxy. All API components were containerized using Docker and designed for use with Docker CE for AWS as the Container-as-a-Service (CaaS) platform.
Optimizing for Kubernetes on Azure
This post will demonstrate how a modern application, such as the Voter API, is optimized for Kubernetes in the Cloud (Kubernetes-as-a-Service), in this case, AKS, Azure’s new public preview of Managed Kubernetes for Azure Container Service. According to Microsoft, the goal of AKS is to simplify the deployment, management, and operations of Kubernetes. I wrote about AKS in detail, in my last post, First Impressions of AKS, Azure’s New Managed Kubernetes Container Service.
In addition to migrating to AKS, the Voter API will take advantage of additional enterprise-grade Azure’s resources, including Azure’s Service Bus and Cosmos DB, replacements for the Voter API’s RabbitMQ and MongoDB. There are several architectural options for the Voter API’s messaging and NoSQL data source requirements when moving to Azure.
- Keep Dockerized RabbitMQ and MongoDB – Easy to deploy to Kubernetes, but not easily scalable, highly-available, or manageable. Would require storage optimized Azure VMs for nodes, node affinity, and persistent storage for data.
- Replace with Cloud-based Non-Azure Equivalents – Use SaaS-based equivalents, such as CloudAMQP (RabbitMQ-as-a-Service) and MongoDB Atlas, which will provide scalability, high-availability, and manageability.
- Replace with Azure Service Bus and Cosmos DB – Provides all the advantages of SaaS-based equivalents, and additionally as Azure resources, benefits from being in the Azure Cloud alongside AKS.
Source Code
The Kubernetes resource files and deployment scripts used in this post are all available on GitHub. This is the only project you need to clone to reproduce the AKS example in this post.
git clone \ --branch master --single-branch --depth 1 --no-tags \ https://github.com/garystafford/azure-aks-sb-cosmosdb-demo.git
The Docker images for the three Spring microservices deployed to AKS, Voter, Candidate, and Election, are available on Docker Hub. Optionally, the source code, including Dockerfiles, for the Voter, Candidate, and Election microservices, as well as the Voter Client are available on GitHub, in the kub-aks
branch.
git clone \ --branch kub-aks --single-branch --depth 1 --no-tags \ https://github.com/garystafford/candidate-service.git git clone \ --branch kub-aks --single-branch --depth 1 --no-tags \ https://github.com/garystafford/election-service.git git clone \ --branch kub-aks --single-branch --depth 1 --no-tags \ https://github.com/garystafford/voter-service.git git clone \ --branch kub-aks --single-branch --depth 1 --no-tags \ https://github.com/garystafford/voter-client.git
Azure Service Bus
To demonstrate the capabilities of Azure’s Service Bus, the Voter API’s Spring microservice’s source code has been re-written to work with Azure Service Bus instead of RabbitMQ. A future post will explore the microservice’s messaging code. It is more likely that a large application, written specifically for a technology that is easily portable such as RabbitMQ or MongoDB, would likely remain on that technology, even if the application was lifted and shifted to the Cloud or moved between Cloud Service Providers (CSPs). Something important to keep in mind when choosing modern technologies – portability.
Service Bus is Azure’s reliable cloud Messaging-as-a-Service (MaaS). Service Bus is an original Azure resource offering, available for several years. The core components of the Service Bus messaging infrastructure are queues, topics, and subscriptions. According to Microsoft, ‘the primary difference is that topics support publish/subscribe capabilities that can be used for sophisticated content-based routing and delivery logic, including sending to multiple recipients.’
Since the three Voter API’s microservices are not required to produce messages for more than one other service consumer, Service Bus queues are sufficient, as opposed to a pub/sub model using Service Bus topics.
Cosmos DB
Cosmos DB, Microsoft’s globally distributed, multi-model database, offers throughput, latency, availability, and consistency guarantees with comprehensive service level agreements (SLAs). Ideal for the Voter API, Cosmos DB supports MongoDB’s data models through the MongoDB API, a MongoDB database service built on top of Cosmos DB. The MongoDB API is compatible with existing MongoDB libraries, drivers, tools, and applications. Therefore, there are no code changes required to convert the Voter API from MongoDB to Cosmos DB. I simply had to change the database connection string.
NGINX Ingress Controller
Although the Voter API’s HAProxy-based API Gateway could be deployed to AKS, it is not optimal for Kubernetes. Instead, the Voter API will use an NGINX-based Ingress Controller. NGINX will serve as an API Gateway, as HAProxy did, previously.
According to NGINX, ‘an Ingress is a Kubernetes resource that lets you configure an HTTP load balancer for your Kubernetes services. Such a load balancer usually exposes your services to clients outside of your Kubernetes cluster.’
An Ingress resource requires an Ingress Controller to function. Continuing from NGINX, ‘an Ingress Controller is an application that monitors Ingress resources via the Kubernetes API and updates the configuration of a load balancer in case of any changes. Different load balancers require different Ingress controller implementations. In the case of software load balancers, such as NGINX, an Ingress controller is deployed in a pod along with a load balancer.’
There are currently two NGINX-based Ingress Controllers available, one from Kubernetes and one directly from NGINX. Both being equal, for this post, I chose the Kubernetes version, without RBAC (Kubernetes offers a version with and without RBAC). RBAC should always be used for actual cluster security. There are several advantages of using either version of the NGINX Ingress Controller for Kubernetes, including Layer 4 TCP and UDP and Layer 7 HTTP load balancing, reverse proxying, ease of SSL termination, dynamically-configurable path-based rules, and support for multiple hostnames.
Azure Web App
Lastly, the Voter Client application, not really part of the Voter API, but useful for demonstration purposes, will be converted from a containerized application to an Azure Web App. Since it is not part of the Voter API, separating the Client application from AKS makes better architectural sense. Web Apps are a powerful, richly-featured, yet incredibly simple way to host applications and services on Azure. For more information on using Azure Web Apps, read my recent post, Developing Applications for the Cloud with Azure App Services and MongoDB Atlas.
Revised Component Architecture
Below is a simplified component diagram of the new architecture, including Azure Service Bus, Cosmos DB, and the NGINX Ingress Controller. The new architecture looks similar to the previous architecture, but as you will see, it is actually very different.
Process Flow
To understand the role of each API component, let’s look at one of the event-driven, decoupled process flows, the creation of a new election candidate. In the simplified flow diagram below, an API consumer executes an HTTP POST request containing the new candidate object as JSON. The Candidate microservice receives the HTTP request and creates a new document in the Cosmos DB Voter database. A Spring RepositoryEventHandler
within the Candidate microservice responds to the document creation and publishes a Create Candidate event message, containing the new candidate object as JSON, to the Azure Service Bus Candidate Queue.
Independently, the Voter microservice is listening to the Candidate Queue. Whenever a new message is produced by the Candidate microservice, the Voter microservice retrieves the message off the queue. The Voter microservice then transforms the new candidate object contained in the incoming message to its own candidate data model and creates a new document in its own Voter database.
The same process flows exist between the Election and the Candidate microservices. The Candidate microservice maintains current elections in its database, which are retrieved from the Election queue.
Data Models
It is useful to understand, the Candidate microservice’s candidate domain model is not necessarily identical to the Voter microservice’s candidate domain model. Each microservice may choose to maintain its own representation of a vote, a candidate, and an election. The Voter service transforms the new candidate object in the incoming message based on its own needs. In this case, the Voter microservice is only interested in a subset of the total fields in the Candidate microservice’s model. This is the beauty of decoupling microservices, their domain models, and their datastores.
Other Events
The versions of the Voter API microservices used for this post only support Election Created events and Candidate Created events. They do not handle Delete or Update events, which would be necessary to be fully functional. For example, if a candidate withdraws from an election, the Voter service would need to be notified so no one places votes for that candidate. This would normally happen through a Candidate Delete or Candidate Update event.
Provisioning Azure Service Bus
First, the Azure Service Bus is provisioned. Provisioning the Service Bus may be accomplished using several different methods, including manually using the Azure Portal or programmatically using Azure Resource Manager (ARM) with PowerShell or Terraform. I chose to provision the Azure Service Bus and the two queues using the Azure Portal for expediency. I chose the Basic Service Bus Tier of service, of which there are three tiers, Basic, Standard, and Premium.
The application requires two queues, the candidate.queue
, and the election.queue
.
Provisioning Cosmos DB
Next, Cosmos DB is provisioned. Like Azure Service Bus, Cosmos DB may be provisioned using several methods, including manually using the Azure Portal, programmatically using Azure Resource Manager (ARM) with PowerShell or Terraform, or using the Azure CLI, which was my choice.
az cosmosdb create \ --name cosmosdb_instance_name_goes_here \ --resource-group resource_group_name_goes_here \ --location "East US=0" \ --kind MongoDB
The post’s Cosmos DB instance exists within the single East US Region, with no failover. In a real Production environment, you would configure Cosmos DB with multi-region failover. I chose MongoDB as the type of Cosmos DB database account to create. The allowed values are GlobalDocumentDB, MongoDB, Parse. All other settings were left to the default values.
The three Spring microservices each have their own database. You do not have to create the databases in advance of consuming the Voter API. The databases and the database collections will be automatically created when new documents are first inserted by the microservices. Below, the three databases and their collections have been created and populated with documents.
The GitHub project repository also contains three shell scripts to generate sample vote, candidate, and election documents. The scripts will delete any previous documents from the database collections and generate new sets of sample documents. To use, you will have to update the scripts with your own Voter API URL.
MongoDB Aggregation Pipeline
Each of the three Spring microservices uses Spring Data MongoDB, which takes advantage of MongoDB’s Aggregation Framework. According to MongoDB, ‘the aggregation framework is modeled on the concept of data processing pipelines. Documents enter a multi-stage pipeline that transforms the documents into an aggregated result.’ Below is an example of aggregation from the Candidate microservice’s VoterContoller
class.
Aggregation aggregation = Aggregation.newAggregation( match(Criteria.where("election").is(election)), group("candidate").count().as("votes"), project("votes").and("candidate").previousOperation(), sort(Sort.Direction.DESC, "votes") );
To use MongoDB’s aggregation framework with Cosmos DB, it is currently necessary to activate the MongoDB Aggregation Pipeline Preview Feature of Cosmos DB. The feature can be activated from the Azure Portal, as shown below.
Cosmos DB Emulator
Be warned, Cosmos DB can be very expensive, even without database traffic or any Production-grade bells and whistles. Be careful when spinning up instances on Azure for learning purposes, the cost adds up quickly! In less than ten days, while writing this post, my cost was almost US$100 for the Voter API’s Cosmos DB instance.
I strongly recommend downloading the free Azure Cosmos DB Emulator to develop and test applications from your local machine. Although certainly not as convenient, it will save you the cost of developing for Cosmos DB directly on Azure.
With Cosmos DB, you pay for reserved throughput provisioned and data stored in containers (a collection of documents or a table or a graph). Yes, that’s right, Azure charges you per MongoDB collection, not even per database. Azure Cosmos DB’s current pricing model seems less than ideal for microservice architectures, each with their own database instance.
By default the reserved throughput, billed as Request Units (RU) per second or RU/s, is set to 1,000 RU/s per collection. For development and testing, you can reduce each collection to a minimum of 400 RU/s. The Voter API creates five collections at 1,000 RU/s or 5,000 RU/s total. Reducing this to a total of 2,000 RU/s makes Cosmos DB marginally more affordable to explore.
Building the AKS Cluster
An existing Azure Resource Group is required for AKS. I chose to use the latest available version of Kubernetes, 1.8.2.
# login to azure az login \ --username your_username \ --password your_password # create resource group az group create \ --resource-group resource_group_name_goes_here \ --location eastus # create aks cluster az aks create \ --name cluser_name_goes_here \ --resource-group resource_group_name_goes_here \ --ssh-key-value path_to_your_public_key \ --kubernetes-version 1.8.2 # get credentials to access aks cluster az aks get-credentials \ --name cluser_name_goes_here \ --resource-group resource_group_name_goes_here # display cluster's worker nodes kubectl get nodes --output=wide
By default, AKS will provision a three-node Kubernetes cluster using Azure’s Standard D1 v2 Virtual Machines. According to Microsoft, ‘D series VMs are general purpose VM sizes provide balanced CPU-to-memory ratio. They are ideal for testing and development, small to medium databases, and low to medium traffic web servers.’ Azure D1 v2 VM’s are based on Linux OS images, currently Debian 8 (Jessie), with 1 vCPU and 3.5 GB of memory. By default with AKS, each VM receives 30 GiB of Standard HDD attached storage.
You should always select the type and quantity of the cluster’s VMs and their attached storage, optimized for estimated traffic volumes and the specific workloads you are running. This can be done using the --node-count
, --node-vm-size
, and --node-osdisk-size
arguments with the az aks create
command.
Deployment
The Voter API resources are deployed to its own Kubernetes Namespace, voter-api
. The NGINX Ingress Controller resources are deployed to a different namespace, ingress-nginx
. Separate namespaces help organize individual Kubernetes resources and separate different concerns.
Voter API
First, the voter-api
namespace is created. Then, five required Kubernetes Secrets are created within the namespace. These secrets all contain sensitive information, such as passwords, that should not be shared. There is one secret for each of the three Cosmos DB database connection strings, one secret for the Azure Service Bus connection string, and one secret for the Let’s Encrypt SSL/TLS certificate and private key, used for secure HTTPS access to the Voter API.
Secrets
The Voter API’s secrets are used to populate environment variables within the pod’s containers. The environment variables are then available for use within the containers. Below is a snippet of the Voter pods resource file showing how the Cosmos DB and Service Bus connection strings secrets are used to populate environment variables.
env: - name: AZURE_SERVICE_BUS_CONNECTION_STRING valueFrom: secretKeyRef: name: azure-service-bus key: connection-string - name: SPRING_DATA_MONGODB_URI valueFrom: secretKeyRef: name: azure-cosmosdb-voter key: connection-string
Shown below, the Cosmos DB and Service Bus connection strings secrets have been injected into the Voter container and made available as environment variables to the microservice’s executable JAR file on start-up. As environment variables, the secrets are visible in plain text. Access to containers should be tightly controlled through Kubernetes RBAC and Azure AD, to ensure sensitive information, such as secrets, remain confidential.
Next, the three Kubernetes ReplicaSet resources, corresponding to the three Spring microservices, are created using Deployment controllers. According to Kubernetes, a Deployment that configures a ReplicaSet is now the recommended way to set up replication. The Deployments specify three replicas of each of the three Spring Services, resulting in a total of nine Kubernetes Pods.
Each pod, by default, will be scheduled on a different node if possible. According to Kubernetes, ‘the scheduler will automatically do a reasonable placement (e.g. spread your pods across nodes, not place the pod on a node with insufficient free resources, etc.).’ Note below how each of the three microservice’s three replicas has been scheduled on a different node in the three-node AKS cluster.
Next, the three corresponding Kubernetes ClusterIP-type Services are created. And lastly, the Kubernetes Ingress is created. According to Kubernetes, the Ingress resource is an API object that manages external access to the services in a cluster, typically HTTP. Ingress provides load balancing, SSL termination, and name-based virtual hosting.
The Ingress configuration contains the routing rules used with the NGINX Ingress Controller. Shown below are the routing rules for each of the three microservices within the Voter API. Incoming API requests are routed to the appropriate pod and service port by NGINX.
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: voter-ingress namespace: voter-api annotations: ingress.kubernetes.io/ssl-redirect: "true" spec: tls: - hosts: - api.voter-demo.com secretName: api-voter-demo-secret rules: - http: paths: - path: /candidate backend: serviceName: candidate servicePort: 8080 - path: /election backend: serviceName: election servicePort: 8080 - path: /voter backend: serviceName: voter servicePort: 8080
The screengrab below shows all of the Voter API resources created on AKS.
NGINX Ingress Controller
After completing the deployment of the Voter API, the NGINX Ingress Controller is created. It starts with creating the ingress-nginx
namespace. Next, the NGINX Ingress Controller is created, consisting of the NGINX Ingress Controller, three Kubernetes ConfigMap resources, and a default back-end application. The Controller and backend each have their own Service resources. Like the Voter API, each has three replicas, for a total of six pods. Together, the Ingress resource and NGINX Ingress Controller manage traffic to the Spring microservices.
The screengrab below shows all of the NGINX Ingress Controller resources created on AKS.
The NGINX Ingress Controller Service, shown above, has an external public IP address associated with itself. This is because that Service is of the type, Load Balancer. External requests to the Voter API will be routed through the NGINX Ingress Controller, on this IP address.
kind: Service apiVersion: v1 metadata: name: ingress-nginx namespace: ingress-nginx labels: app: ingress-nginx spec: externalTrafficPolicy: Local type: LoadBalancer selector: app: ingress-nginx ports: - name: http port: 80 targetPort: http - name: https port: 443 targetPort: https
If you are only using HTTPS, not HTTP, then the references to HTTP and port 80 in the Ingress configuration are unnecessary. The NGINX Ingress Controller’s resources are explained in detail in the GitHub documentation, along with further configuration instructions.
DNS
To provide convenient access to the Voter API and the Voter Client, my domain, voter-demo.com
, is associated with the public IP address associated with the Voter API Ingress Controller and with the public IP address associated with the Voter Client Azure Web App. DNS configuration is done through Azure’s DNS Zone resource.
The two TXT
type records might not look as familiar as the SOA
, NS
, and A
type records. The TXT
records are required to associate the domain entries with the Voter Client Azure Web App. Browsing to http://www.voter-demo.com or simply http://voter-demo.com brings up the Voter Client.
The Client sends and receives data via the Voter API, available securely at https://api.voter-demo.com.
Routing API Requests
With the Pods, Services, Ingress, and NGINX Ingress Controller created and configured, as well as the Azure Layer 4 Load Balancer and DNS Zone, HTTP requests from API consumers are properly and securely routed to the appropriate microservices. In the example below, three back-to-back requests are made to the voter/info
API endpoint. HTTP requests are properly routed to one of the three Voter pod replicas using the default round-robin algorithm, as proven by the observing the different hostnames (pod names) and the IP addresses (private pod IPs) in each HTTP response.
Final Architecture
Shown below is the final Voter API Azure architecture. To simplify the diagram, I have deliberately left out the three microservice’s ClusterIP-type Services, the three default back-end application pods, and the default back-end application’s ClusterIP-type Service. All resources shown below are within the single East US Azure region, except DNS, which is a global resource.
Shown below is the new Azure Resource Group created by Azure during the AKS provisioning process. The Resource Group contains the various resources required to support the AKS cluster, NGINX Ingress Controller, and the Voter API. Necessary Azure resources were automatically provisioned when I provisioned AKS and when I created the new Voter API and NGINX resources.
In addition to the Resource Group above, the original Resource Group contains the AKS Container Service resource itself, Service Bus, Cosmos DB, and the DNS Zone resource.
The Voter Client Web App, consisting of the Azure App Service and App Service plan resource, is located in a third, separate Resource Group, not shown here.
Cleaning Up AKS
A nice feature of AKS, running a az aks delete
command will delete all the Azure resources created as part of provisioning AKS, the API, and the Ingress Controller. You will have to delete the Cosmos DB, Service Bus, and DNS Zone resources, separately.
az aks delete \ --name cluser_name_goes_here \ --resource-group resource_group_name_goes_here
Conclusion
Taking advantage of Kubernetes with AKS, and the array of Azure’s enterprise-grade resources, the Voter API was shifted from a simple Docker architecture to a production-ready solution. The Voter API is now easier to manage, horizontally scalable, fault-tolerant, and marginally more secure. It is capable of reliably supporting dozens more microservices, with multiple replicas. The Voter API will handle a high volume of data transactions and event messages.
There is much more that needs to be done to productionalize the Voter API on AKS, including:
- Add multi-region failover of Cosmos DB
- Upgrade to Service Bus Standard or Premium Tier
- Optimized Azure VMs and storage for anticipated traffic volumes and application-specific workloads
- Implement Kubernetes RBAC
- Add Monitoring, logging, and alerting with Envoy or similar
- Secure end-to-end TLS communications with Itsio or similar
- Secure the API with OAuth and Azure AD
- Automate everything with DevOps – AKS provisioning, testing code, creating resources, updating microservices, and managing data
All opinions in this post are my own, and not necessarily the views of my current or past employers or their clients.
Spring Music Revisited: Java-Spring-MongoDB Web App with Docker 1.12
Posted by Gary A. Stafford in Build Automation, Continuous Delivery, DevOps, Enterprise Software Development, Java Development, Software Development on August 7, 2016
Build, test, deploy, and monitor a multi-container, MongoDB-backed, Java Spring web application, using the new Docker 1.12.
Introduction
This post and the post’s example project represent an update to a previous post, Build and Deploy a Java-Spring-MongoDB Application using Docker. This new post incorporates many improvements made in Docker 1.12, including the use of the new Docker Compose v2 YAML format. The post’s project was also updated to use Filebeat with ELK, as opposed to Logspout, which was used previously.
In this post, we will demonstrate how to build, test, deploy, and manage a Java Spring web application, hosted on Apache Tomcat, load-balanced by NGINX, monitored by ELK with Filebeat, and all containerized with Docker.
We will use a sample Java Spring application, Spring Music, available on GitHub from Cloud Foundry. The Spring Music sample record album collection application was originally designed to demonstrate the use of database services on Cloud Foundry, using the Spring Framework. Instead of Cloud Foundry, we will host the Spring Music application locally, using Docker on VirtualBox, and optionally on AWS.
All files necessary to build this project are stored on the docker_v2
branch of the garystafford/spring-music-docker repository on GitHub. The Spring Music source code is stored on the springmusic_v2
branch of the garystafford/spring-music repository, also on GitHub.
Application Architecture
The Java Spring Music application stack contains the following technologies: Java, Spring Framework, AngularJS, Bootstrap, jQuery, NGINX, Apache Tomcat, MongoDB, the ELK Stack, and Filebeat. Testing frameworks include the Spring MVC Test Framework, Mockito, Hamcrest, and JUnit.
A few changes were made to the original Spring Music application to make it work for this demonstration, including:
- Move from Java 1.7 to 1.8 (including newer Tomcat version)
- Add unit tests for Continuous Integration demonstration purposes
- Modify MongoDB configuration class to work with non-local, containerized MongoDB instances
- Add Gradle
warNoStatic
task to build WAR without static assets - Add Gradle
zipStatic
task to ZIP up the application’s static assets for deployment to NGINX - Add Gradle
zipGetVersion
task with a versioning scheme for build artifacts - Add
context.xml
file andMANIFEST.MF
file to the WAR file - Add Log4j
RollingFileAppender
appender to send log entries to Filebeat - Update versions of several dependencies, including Gradle, Spring, and Tomcat
We will use the following technologies to build, publish, deploy, and host the Java Spring Music application: Gradle, git, GitHub, Travis CI, Oracle VirtualBox, Docker, Docker Compose, Docker Machine, Docker Hub, and optionally, Amazon Web Services (AWS).
NGINX
To increase performance, the Spring Music web application’s static content will be hosted by NGINX. The application’s WAR file will be hosted by Apache Tomcat 8.5.4. Requests for non-static content will be proxied through NGINX on the front-end, to a set of three load-balanced Tomcat instances on the back-end. To further increase application performance, NGINX will also be configured for browser caching of the static content. In many enterprise environments, the use of a Java EE application server, like Tomcat, is still not uncommon.
Reverse proxying and caching are configured thought NGINX’s default.conf
file, in the server
configuration section:
server { | |
listen 80; | |
server_name proxy; | |
location ~* \/assets\/(css|images|js|template)\/* { | |
root /usr/share/nginx/; | |
expires max; | |
add_header Pragma public; | |
add_header Cache-Control "public, must-revalidate, proxy-revalidate"; | |
add_header Vary Accept-Encoding; | |
access_log off; | |
} |
The three Tomcat instances will be manually configured for load-balancing using NGINX’s default round-robin load-balancing algorithm. This is configured through the default.conf
file, in the upstream
configuration section:
upstream backend { | |
server music_app_1:8080; | |
server music_app_2:8080; | |
server music_app_3:8080; | |
} |
Client requests are received through port 80
on the NGINX server. NGINX redirects requests, which are not for non-static assets, to one of the three Tomcat instances on port 8080
.
MongoDB
The Spring Music application was designed to work with a number of data stores, including MySQL, Postgres, Oracle, MongoDB, Redis, and H2, an in-memory Java SQL database. Given the choice of both SQL and NoSQL databases, we will select MongoDB.
The Spring Music application, hosted by Tomcat, will store and modify record album data in a single instance of MongoDB. MongoDB will be populated with a collection of album data from a JSON file, when the Spring Music application first creates the MongoDB database instance.
ELK
Lastly, the ELK Stack with Filebeat, will aggregate NGINX, Tomcat, and Java Log4j log entries, providing debugging and analytics to our demonstration. A similar method for aggregating logs, using Logspout instead of Filebeat, can be found in this previous post.
Continuous Integration
In this post’s example, two build artifacts, a WAR file for the application and ZIP file for the static web content, are built automatically by Travis CI, whenever source code changes are pushed to the springmusic_v2
branch of the garystafford/spring-music repository on GitHub.
Following a successful build and a small number of unit tests, Travis CI pushes the build artifacts to the build-artifacts
branch on the same GitHub project. The build-artifacts
branch acts as a pseudo binary repository for the project, much like JFrog’s Artifactory. These artifacts are used later by Docker to build the project’s immutable Docker images and containers.
Build Notifications
Travis CI pushes build notifications to a Slack channel, which eliminates the need to actively monitor Travis CI.
Automation Scripting
The .travis.yaml
file, custom gradle.build
Gradle tasks, and the deploy_travisci.sh
script handles the Travis CI automation described, above.
Travis CI .travis.yaml
file:
language: java | |
jdk: oraclejdk8 | |
before_install: | |
- chmod +x gradlew | |
before_deploy: | |
- chmod ugo+x deploy_travisci.sh | |
script: | |
- "./gradlew clean build" | |
- "./gradlew warNoStatic warCopy zipGetVersion zipStatic" | |
- sh ./deploy_travisci.sh | |
env: | |
global: | |
- GH_REF: github.com/garystafford/spring-music.git | |
- secure: <GH_TOKEN_secure_hash_here> | |
- secure: <COMMIT_AUTHOR_EMAIL_secure_hash_here> | |
notifications: | |
slack: | |
- secure: <SLACK_secure_hash_here> |
Custom gradle.build
tasks:
// new Gradle build tasks | |
task warNoStatic(type: War) { | |
// omit the version from the war file name | |
version = '' | |
exclude '**/assets/**' | |
manifest { | |
attributes | |
'Manifest-Version': '1.0', | |
'Created-By': currentJvm, | |
'Gradle-Version': GradleVersion.current().getVersion(), | |
'Implementation-Title': archivesBaseName + '.war', | |
'Implementation-Version': artifact_version, | |
'Implementation-Vendor': 'Gary A. Stafford' | |
} | |
} | |
task warCopy(type: Copy) { | |
from 'build/libs' | |
into 'build/distributions' | |
include '**/*.war' | |
} | |
task zipGetVersion (type: Task) { | |
ext.versionfile = | |
new File("${projectDir}/src/main/webapp/assets/buildinfo.properties") | |
versionfile.text = 'build.version=' + artifact_version | |
} | |
task zipStatic(type: Zip) { | |
from 'src/main/webapp/assets' | |
appendix = 'static' | |
version = '' | |
} |
The deploy.sh
file:
#!/bin/bash | |
set -e | |
cd build/distributions | |
git init | |
git config user.name "travis-ci" | |
git config user.email "${COMMIT_AUTHOR_EMAIL}" | |
git add . | |
git commit -m "Deploy Travis CI Build #${TRAVIS_BUILD_NUMBER} artifacts to GitHub" | |
git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:build-artifacts > /dev/null 2>&1 |
You can easily replicate the project’s continuous integration automation using your choice of toolchains. GitHub or BitBucket are good choices for distributed version control. For continuous integration and deployment, I recommend Travis CI, Semaphore, Codeship, or Jenkins. Couple those with a good persistent chat application, such as Glider Labs’ Slack or Atlassian’s HipChat.
Building the Docker Environment
Make sure VirtualBox, Docker, Docker Compose, and Docker Machine, are installed and running. At the time of this post, I have the following versions of software installed on my Mac:
- Mac OS X 10.11.6
- VirtualBox 5.0.26
- Docker 1.12.1
- Docker Compose 1.8.0
- Docker Machine 0.8.1
To build the project’s VirtualBox VM, Docker images, and Docker containers, execute the build script, using the following command: sh ./build_project.sh
. A build script is useful when working with CI/CD automation tools, such as Jenkins CI or ThoughtWorks go. However, to understand the build process, I suggest first running the individual commands, locally.
#!/bin/sh | |
set -ex | |
# clone project | |
git clone -b docker_v2 --single-branch \ | |
https://github.com/garystafford/spring-music-docker.git music \ | |
&& cd "$_" | |
# provision VirtualBox VM | |
docker-machine create --driver virtualbox springmusic | |
# set new environment | |
docker-machine env springmusic \ | |
&& eval "$(docker-machine env springmusic)" | |
# mount a named volume on host to store mongo and elk data | |
# ** assumes your project folder is 'music' ** | |
docker volume create --name music_data | |
docker volume create --name music_elk | |
# create bridge network for project | |
# ** assumes your project folder is 'music' ** | |
docker network create -d bridge music_net | |
# build images and orchestrate start-up of containers (in this order) | |
docker-compose -p music up -d elk && sleep 15 \ | |
&& docker-compose -p music up -d mongodb && sleep 15 \ | |
&& docker-compose -p music up -d app \ | |
&& docker-compose scale app=3 && sleep 15 \ | |
&& docker-compose -p music up -d proxy && sleep 15 | |
# optional: configure local DNS resolution for application URL | |
#echo "$(docker-machine ip springmusic) springmusic.com" | sudo tee --append /etc/hosts | |
# run a simple connectivity test of application | |
for i in {1..9}; do curl -I $(docker-machine ip springmusic); done |
Deploying to AWS
By simply changing the Docker Machine driver to AWS EC2 from VirtualBox, and providing your AWS credentials, the springmusic
environment may also be built on AWS.
Build Process
Docker Machine provisions a single VirtualBox springmusic
VM on which host the project’s containers. VirtualBox provides a quick and easy solution that can be run locally for initial development and testing of the application.
Next, the script creates a Docker data volume and project-specific Docker bridge network.
Next, using the project’s individual Dockerfiles, Docker Compose pulls base Docker images from Docker Hub for NGINX, Tomcat, ELK, and MongoDB. Project-specific immutable Docker images are then built for NGINX, Tomcat, and MongoDB. While constructing the project-specific Docker images for NGINX and Tomcat, the latest Spring Music build artifacts are pulled and installed into the corresponding Docker images.
Docker Compose builds and deploys (6) containers onto the VirtualBox VM: (1) NGINX, (3) Tomcat, (1) MongoDB, and (1) ELK.
The NGINX Dockerfile
:
# NGINX image with build artifact | |
FROM nginx:latest | |
MAINTAINER Gary A. Stafford <garystafford@rochester.rr.com> | |
ENV REFRESHED_AT 2016-09-17 | |
ENV GITHUB_REPO https://github.com/garystafford/spring-music/raw/build-artifacts | |
ENV STATIC_FILE spring-music-static.zip | |
RUN apt-get update -qq \ | |
&& apt-get install -qqy curl wget unzip nano \ | |
&& apt-get clean \ | |
\ | |
&& wget -O /tmp/${STATIC_FILE} ${GITHUB_REPO}/${STATIC_FILE} \ | |
&& unzip /tmp/${STATIC_FILE} -d /usr/share/nginx/assets/ | |
COPY default.conf /etc/nginx/conf.d/default.conf | |
# tweak nginx image set-up, remove log symlinks | |
RUN rm /var/log/nginx/access.log /var/log/nginx/error.log | |
# install Filebeat | |
ENV FILEBEAT_VERSION=filebeat_1.2.3_amd64.deb | |
RUN curl -L -O https://download.elastic.co/beats/filebeat/${FILEBEAT_VERSION} \ | |
&& dpkg -i ${FILEBEAT_VERSION} \ | |
&& rm ${FILEBEAT_VERSION} | |
# configure Filebeat | |
ADD filebeat.yml /etc/filebeat/filebeat.yml | |
# CA cert | |
RUN mkdir -p /etc/pki/tls/certs | |
ADD logstash-beats.crt /etc/pki/tls/certs/logstash-beats.crt | |
# start Filebeat | |
ADD ./start.sh /usr/local/bin/start.sh | |
RUN chmod +x /usr/local/bin/start.sh | |
CMD [ "/usr/local/bin/start.sh" ] |
The Tomcat Dockerfile
:
# Apache Tomcat image with build artifact | |
FROM tomcat:8.5.4-jre8 | |
MAINTAINER Gary A. Stafford <garystafford@rochester.rr.com> | |
ENV REFRESHED_AT 2016-09-17 | |
ENV GITHUB_REPO https://github.com/garystafford/spring-music/raw/build-artifacts | |
ENV APP_FILE spring-music.war | |
ENV TERM xterm | |
ENV JAVA_OPTS -Djava.security.egd=file:/dev/./urandom | |
RUN apt-get update -qq \ | |
&& apt-get install -qqy curl wget \ | |
&& apt-get clean \ | |
\ | |
&& touch /var/log/spring-music.log \ | |
&& chmod 666 /var/log/spring-music.log \ | |
\ | |
&& wget -q -O /usr/local/tomcat/webapps/ROOT.war ${GITHUB_REPO}/${APP_FILE} \ | |
&& mv /usr/local/tomcat/webapps/ROOT /usr/local/tomcat/webapps/_ROOT | |
COPY tomcat-users.xml /usr/local/tomcat/conf/tomcat-users.xml | |
# install Filebeat | |
ENV FILEBEAT_VERSION=filebeat_1.2.3_amd64.deb | |
RUN curl -L -O https://download.elastic.co/beats/filebeat/${FILEBEAT_VERSION} \ | |
&& dpkg -i ${FILEBEAT_VERSION} \ | |
&& rm ${FILEBEAT_VERSION} | |
# configure Filebeat | |
ADD filebeat.yml /etc/filebeat/filebeat.yml | |
# CA cert | |
RUN mkdir -p /etc/pki/tls/certs | |
ADD logstash-beats.crt /etc/pki/tls/certs/logstash-beats.crt | |
# start Filebeat | |
ADD ./start.sh /usr/local/bin/start.sh | |
RUN chmod +x /usr/local/bin/start.sh | |
CMD [ "/usr/local/bin/start.sh" ] |
Docker Compose v2 YAML
This post was recently updated for Docker 1.12, and to use Docker Compose v2 YAML file format. The post’s docker-compose.yml
takes advantage of improvements in Docker 1.12 and Docker Compose v2 YAML. Improvements to the YAML file include eliminating the need to link containers and expose ports, and the addition of named networks and volumes.
version: '2' | |
services: | |
proxy: | |
build: nginx/ | |
ports: | |
- 80:80 | |
networks: | |
- net | |
depends_on: | |
- app | |
hostname: proxy | |
container_name: proxy | |
app: | |
build: tomcat/ | |
ports: | |
- 8080 | |
networks: | |
- net | |
depends_on: | |
- mongodb | |
hostname: app | |
mongodb: | |
build: mongodb/ | |
ports: | |
- 27017:27017 | |
networks: | |
- net | |
depends_on: | |
- elk | |
hostname: mongodb | |
container_name: mongodb | |
volumes: | |
- music_data:/data/db | |
- music_data:/data/configdb | |
elk: | |
image: sebp/elk:latest | |
ports: | |
- 5601:5601 | |
- 9200:9200 | |
- 5044:5044 | |
- 5000:5000 | |
networks: | |
- net | |
volumes: | |
- music_elk:/var/lib/elasticsearch | |
hostname: elk | |
container_name: elk | |
volumes: | |
music_data: | |
external: true | |
music_elk: | |
external: true | |
networks: | |
net: | |
driver: bridge |
The Results
Below are the results of building the project.
# Resulting Docker Machine VirtualBox VM: | |
$ docker-machine ls | |
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS | |
springmusic * virtualbox Running tcp://192.168.99.100:2376 v1.12.1 | |
# Resulting external volume: | |
$ docker volume ls | |
DRIVER VOLUME NAME | |
local music_data | |
local music_elk | |
# Resulting bridge network: | |
$ docker network ls | |
NETWORK ID NAME DRIVER SCOPE | |
f564dfa1b440 music_net bridge local | |
# Resulting Docker images - (4) base images and (3) project images: | |
$ docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
music_proxy latest 7a8dd90bcf32 About an hour ago 250.2 MB | |
music_app latest c93c713d03b8 About an hour ago 393 MB | |
music_mongodb latest fbcbbe9d4485 25 hours ago 366.4 MB | |
tomcat 8.5.4-jre8 98cc750770ba 2 days ago 334.5 MB | |
mongo latest 48b8b08dca4d 2 days ago 366.4 MB | |
nginx latest 4efb2fcdb1ab 10 days ago 183.4 MB | |
sebp/elk latest 07a3e78b01f5 13 days ago 884.5 MB | |
# Resulting (6) Docker containers | |
$ docker ps | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
b33922767517 music_proxy "/usr/local/bin/start" 3 hours ago Up 13 minutes 0.0.0.0:80->80/tcp, 443/tcp proxy | |
e16d2372f2df music_app "/usr/local/bin/start" 3 hours ago Up About an hour 0.0.0.0:32770->8080/tcp music_app_3 | |
6b7accea7156 music_app "/usr/local/bin/start" 3 hours ago Up About an hour 0.0.0.0:32769->8080/tcp music_app_2 | |
2e94f766df1b music_app "/usr/local/bin/start" 3 hours ago Up About an hour 0.0.0.0:32768->8080/tcp music_app_1 | |
71f8dc574148 sebp/elk:latest "/usr/local/bin/start" 3 hours ago Up About an hour 0.0.0.0:5000->5000/tcp, 0.0.0.0:5044->5044/tcp, 0.0.0.0:5601->5601/tcp, 0.0.0.0:9200->9200/tcp, 9300/tcp elk | |
f7e7d1af7cca music_mongodb "/entrypoint.sh mongo" 20 hours ago Up About an hour 0.0.0.0:27017->27017/tcp mongodb |
Testing the Application
Below are partial results of the curl test, hitting the NGINX endpoint. Note the different IP addresses in the Upstream-Address
field between requests. This test proves NGINX’s round-robin load-balancing is working across the three Tomcat application instances: music_app_1
, music_app_2
, and music_app_3
.
Also, note the sharp decrease in the Request-Time
between the first three requests and subsequent three requests. The Upstream-Response-Time
to the Tomcat instances doesn’t change, yet the total Request-Time
is much shorter, due to caching of the application’s static assets by NGINX.
for i in {1..6}; do curl -I $(docker-machine ip springmusic);done | |
HTTP/1.1 200 | |
Server: nginx/1.11.4 | |
Date: Sat, 17 Sep 2016 18:33:50 GMT | |
Content-Type: text/html;charset=ISO-8859-1 | |
Content-Length: 2094 | |
Connection: keep-alive | |
Accept-Ranges: bytes | |
ETag: W/"2094-1473924940000" | |
Last-Modified: Thu, 15 Sep 2016 07:35:40 GMT | |
Content-Language: en | |
Request-Time: 0.575 | |
Upstream-Address: 172.18.0.4:8080 | |
Upstream-Response-Time: 1474137230.048 | |
HTTP/1.1 200 | |
Server: nginx/1.11.4 | |
Date: Sat, 17 Sep 2016 18:33:51 GMT | |
Content-Type: text/html;charset=ISO-8859-1 | |
Content-Length: 2094 | |
Connection: keep-alive | |
Accept-Ranges: bytes | |
ETag: W/"2094-1473924940000" | |
Last-Modified: Thu, 15 Sep 2016 07:35:40 GMT | |
Content-Language: en | |
Request-Time: 0.711 | |
Upstream-Address: 172.18.0.5:8080 | |
Upstream-Response-Time: 1474137230.865 | |
HTTP/1.1 200 | |
Server: nginx/1.11.4 | |
Date: Sat, 17 Sep 2016 18:33:52 GMT | |
Content-Type: text/html;charset=ISO-8859-1 | |
Content-Length: 2094 | |
Connection: keep-alive | |
Accept-Ranges: bytes | |
ETag: W/"2094-1473924940000" | |
Last-Modified: Thu, 15 Sep 2016 07:35:40 GMT | |
Content-Language: en | |
Request-Time: 0.326 | |
Upstream-Address: 172.18.0.6:8080 | |
Upstream-Response-Time: 1474137231.812 | |
# assets now cached... | |
HTTP/1.1 200 | |
Server: nginx/1.11.4 | |
Date: Sat, 17 Sep 2016 18:33:53 GMT | |
Content-Type: text/html;charset=ISO-8859-1 | |
Content-Length: 2094 | |
Connection: keep-alive | |
Accept-Ranges: bytes | |
ETag: W/"2094-1473924940000" | |
Last-Modified: Thu, 15 Sep 2016 07:35:40 GMT | |
Content-Language: en | |
Request-Time: 0.012 | |
Upstream-Address: 172.18.0.4:8080 | |
Upstream-Response-Time: 1474137233.111 | |
HTTP/1.1 200 | |
Server: nginx/1.11.4 | |
Date: Sat, 17 Sep 2016 18:33:53 GMT | |
Content-Type: text/html;charset=ISO-8859-1 | |
Content-Length: 2094 | |
Connection: keep-alive | |
Accept-Ranges: bytes | |
ETag: W/"2094-1473924940000" | |
Last-Modified: Thu, 15 Sep 2016 07:35:40 GMT | |
Content-Language: en | |
Request-Time: 0.017 | |
Upstream-Address: 172.18.0.5:8080 | |
Upstream-Response-Time: 1474137233.350 | |
HTTP/1.1 200 | |
Server: nginx/1.11.4 | |
Date: Sat, 17 Sep 2016 18:33:53 GMT | |
Content-Type: text/html;charset=ISO-8859-1 | |
Content-Length: 2094 | |
Connection: keep-alive | |
Accept-Ranges: bytes | |
ETag: W/"2094-1473924940000" | |
Last-Modified: Thu, 15 Sep 2016 07:35:40 GMT | |
Content-Language: en | |
Request-Time: 0.013 | |
Upstream-Address: 172.18.0.6:8080 | |
Upstream-Response-Time: 1474137233.594 |
Spring Music Application Links
Assuming the springmusic
VM is running at 192.168.99.100
, the following links can be used to access various project endpoints. Note the (3) Tomcat instances each map to randomly exposed ports. These ports are not required by NGINX, which maps to port 8080 for each instance. The port is only required if you want access to the Tomcat Web Console. The port, shown below, 32771, is merely used as an example.
- Spring Music Application: 192.168.99.100
- NGINX Status: 192.168.99.100/nginx_status
- Tomcat Web Console – music_app_1*: 192.168.99.100:32771/manager
- Environment Variables – music_app_1: 192.168.99.100:32771/env
- Album List (RESTful endpoint) – music_app_1: 192.168.99.100:32771/albums
- Elasticsearch Info: 192.168.99.100:9200
- Elasticsearch Status: 192.168.99.100:9200/_status?pretty
- Kibana Web Console: 192.168.99.100:5601
* The Tomcat user name is admin
and the password is t0mcat53rv3r
.
Helpful Links
- Cloud Foundry’s Spring Music Example
- Getting Started with Gradle for Java
- Introduction to Gradle
- Spring Framework
- Understanding Nginx HTTP Proxying, Load Balancing, Buffering, and Caching
- Common conversion patterns for log4j’s PatternLayout
- Spring @PropertySource example
- Java log4j logging
TODOs
- Automate the Docker image build and publish processes
- Automate the Docker container build and deploy processes
- Automate post-deployment verification testing of project infrastructure
- Add Docker Swarm multi-host capabilities with overlay networking
- Update Spring Music with latest CF project revisions
- Include scripting example to stand-up project on AWS
- Add Consul and Consul Template for NGINX configuration