Showing posts with label Mesos. Show all posts
Showing posts with label Mesos. Show all posts

Thursday, 20 October 2016

Integrated Continuous Deployment (CD) Solution with Mesos, Zookeeper, Marathon on top of CI

Overview

After development of our RESTful webservices (see post here) and CI exercises (see post here), now we eventually reach the Continuous Deployment (CD) part.
Obviously, we’re going to experiment a Docker-based CD process.

Below components will be engaged to build up our integrated CD solution:

  • The Docker image we built and published previously in our private Docker Registry;
  • Apache Mosos: The DC/OS platform for managing our CPU, memory, storage, and other compute resources within our distributed clusters;
  • Apache ZooKeeper: A highly reliable distributed coordination tool for Apache Mosos cluster;
  • Mesosphere Marathon: The container orchestration platform for Apache Mesos.

What we’re going to focus in this chapter is as highlighted as below:



Table of Contents

Update docker-compose.yml By Adding CD Components

Having the infrastructure setup for CI previously, it’s pretty easy to add more components into our Docker Compose configuration file for our CD solution which includes:
  • ZooKeeper
  • Mesos: Master, Slave
  • Marathon

ZooKeeper

~/docker-compose.yml
…
# Zookeeper
zookeeper:
  image: jplock/zookeeper:3.4.5
  ports:
    - "2181"

Mesos: mesos-master, mesos-slave

~/docker-compose.yml
…
# Mesos Master
mesos-master:
  image: mesosphere/mesos-master:0.28.1-2.0.20.ubuntu1404
  hostname: mesos-master
  links:
    - zookeeper:zookeeper
  environment:
    - MESOS_ZK=zk://zookeeper:2181/mesos
    - MESOS_QUORUM=1
    - MESOS_WORK_DIR=/var/lib/mesos
    - MESOS_LOG_DIR=/var/log
  ports:
    - "5050:5050"

# Mesos Slave
mesos-slave:
  image: mesosphere/mesos-slave:0.28.1-2.0.20.ubuntu1404
  links:
    - zookeeper:zookeeper
    - mesos-master:mesos-master
  environment:
    - MESOS_MASTER=zk://zookeeper:2181/mesos
    - MESOS_EXECUTOR_REGISTRATION_TIMEOUT=5mins
    - MESOS_CONTAINERIZERS=docker,mesos
    - MESOS_ISOLATOR=cgroups/cpu,cgroups/mem
    - MESOS_LOG_DIR=/var/log
  volumes:
    - /var/run/docker.sock:/run/docker.sock
- /usr/bin/docker:/usr/bin/docker

    - /sys:/sys:ro
- mesosslace-stuff:/var/log

    - /lib/x86_64-linux-gnu/libsystemd-journal.so.0:/lib/x86_64-linux-gnu/libsystemd-journal.so.0
    - /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1
    - /lib/x86_64-linux-gnu/libcgmanager.so.0:/lib/x86_64-linux-gnu/libcgmanager.so.0
    - /lib/x86_64-linux-gnu/libnih.so.1:/lib/x86_64-linux-gnu/libnih.so.1
    - /lib/x86_64-linux-gnu/libnih-dbus.so.1:/lib/x86_64-linux-gnu/libnih-dbus.so.1
    - /lib/x86_64-linux-gnu/libgcrypt.so.11:/lib/x86_64-linux-gnu/libgcrypt.so.11
  expose:
    - "5051"

Marathon

~/docker-compose.yml
…
# Marathon
marathon:
  image: mesosphere/marathon
  links:
    - zookeeper:zookeeper
  ports:
    - "8080:8080"
  command: --master zk://zookeeper:2181/mesos --zk zk://zookeeper:2181/marathon

The Updated docker-compose.yml

Now the final updated ~/docker-compose.yml is as below:
# Git Server
gogs:
  image: 'gogs/gogs:latest'
  #restart: always
  hostname: '192.168.56.118'
  ports:
    - '3000:3000'
    - '1022:22'
  volumes:
    - '/data/gogs:/data'

# Jenkins CI Server
jenkins:
  image: devops/jenkinsci
#  image: jenkinsci/jenkins
#  links:
#    - marathon:marathon
  volumes:
    - /data/jenkins:/var/jenkins_home

    - /opt/jdk/java_home:/opt/jdk/java_home
    - /opt/maven:/opt/maven
    - /data/mvn_repo:/data/mvn_repo

    - /var/run/docker.sock:/var/run/docker.sock
    - /usr/bin/docker:/usr/bin/docker
    - /etc/default/docker:/etc/default/docker

    - /lib/x86_64-linux-gnu/libsystemd-journal.so.0:/lib/x86_64-linux-gnu/libsystemd-journal.so.0
    - /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1
    - /lib/x86_64-linux-gnu/libcgmanager.so.0:/lib/x86_64-linux-gnu/libcgmanager.so.0
    - /lib/x86_64-linux-gnu/libnih.so.1:/lib/x86_64-linux-gnu/libnih.so.1
    - /lib/x86_64-linux-gnu/libnih-dbus.so.1:/lib/x86_64-linux-gnu/libnih-dbus.so.1
    - /lib/x86_64-linux-gnu/libgcrypt.so.11:/lib/x86_64-linux-gnu/libgcrypt.so.11
  ports:
- "8081:8080"

# Private Docker Registry
docker-registry:
  image: registry
  volumes:
    - /data/registry:/var/lib/registry
  ports:
    - "5000:5000"

# Zookeeper
zookeeper:
  image: jplock/zookeeper:3.4.5
  ports:
    - "2181"

# Mesos Master
mesos-master:
  image: mesosphere/mesos-master:0.28.1-2.0.20.ubuntu1404
  hostname: mesos-master
  links:
    - zookeeper:zookeeper
  environment:
    - MESOS_ZK=zk://zookeeper:2181/mesos
    - MESOS_QUORUM=1
    - MESOS_WORK_DIR=/var/lib/mesos
    - MESOS_LOG_DIR=/var/log
  ports:
    - "5050:5050"

# Mesos Slave
mesos-slave:
  image: mesosphere/mesos-slave:0.28.1-2.0.20.ubuntu1404
  links:
    - zookeeper:zookeeper
    - mesos-master:mesos-master
  environment:
    - MESOS_MASTER=zk://zookeeper:2181/mesos
    - MESOS_EXECUTOR_REGISTRATION_TIMEOUT=5mins
    - MESOS_CONTAINERIZERS=docker,mesos
    - MESOS_ISOLATOR=cgroups/cpu,cgroups/mem
    - MESOS_LOG_DIR=/var/log
  volumes:
    - /var/run/docker.sock:/run/docker.sock
     - /usr/bin/docker:/usr/bin/docker

    - /sys:/sys:ro
    - mesosslace-stuff:/var/log

    - /lib/x86_64-linux-gnu/libsystemd-journal.so.0:/lib/x86_64-linux-gnu/libsystemd-journal.so.0
    - /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1
    - /lib/x86_64-linux-gnu/libcgmanager.so.0:/lib/x86_64-linux-gnu/libcgmanager.so.0
    - /lib/x86_64-linux-gnu/libnih.so.1:/lib/x86_64-linux-gnu/libnih.so.1
    - /lib/x86_64-linux-gnu/libnih-dbus.so.1:/lib/x86_64-linux-gnu/libnih-dbus.so.1
    - /lib/x86_64-linux-gnu/libgcrypt.so.11:/lib/x86_64-linux-gnu/libgcrypt.so.11
  expose:
    - "5051"

# Marathon
marathon:
  image: mesosphere/marathon
  links:
    - zookeeper:zookeeper
  ports:
    - "8080:8080"
  command: --master zk://zookeeper:2181/mesos --zk zk://zookeeper:2181/marathon

Spin Up the Docker Containers

$ docker-compose up

Verify Our Components

Jenkins: http://192.168.56.118:8081/
Marathon: http://192.168.56.118:8080/

Deployment By Using Mesos + Marathon

Marathon provides RESTful APIs for managing the behaviors we're going to load into the platform, such as deleting an application, deploying an application etc.
So the idea we're going to illustrate is to:
  1. Prepare the payload, or the Marathon deployment file;
  2. Wrap the interactions with Marathon as simple deployment scripts which can eventually be "plugged" into Jenkins as one more build steps to kick off the CD pipeline.

Marathon Deployment File

Firstly we need to create the Marathon deployment file, which is in JSON format, for scheduling our application on Marathon.
Let’s name it marathon-app-springboot-jersey-swagger.json and put it under /data/jenkins/workspace/springboot-jersey-swagger (which will be eventually mapped to Jenkins container’s path of “/var/jenkins_home/workspace/springboot-jersey-swagger”, or "${WORKSPACE}/" so that it’s reachable in Jenkins).
$ sudo touch /data/jenkins/workspace/springboot-jersey-swagger/marathon-app-springboot-jersey-swagger.json
$ sudo vim /data/jenkins/workspace/springboot-jersey-swagger/marathon-app-springboot-jersey-swagger.json

/data/jenkins/workspace/springboot-jersey-swagger/marathon-app-springboot-jersey-swagger.json
{
    "id": "springboot-jersey-swagger", 
    "container": {
      "docker": {
        "image": "192.168.56.118:5000/devops/springboot-jersey-swagger:latest",
        "network": "BRIDGE",
        "portMappings": [
          {"containerPort": 8000, "servicePort": 8000}
        ]
      }
    },
    "cpus": 0.2,
    "mem": 256.0,
    "instances": 1
}

Deployment Scripts

Once we have the Marathon deployment file to publish we can compile the deployment scripts that will remove the currently running application and redeploy it using the new image. 
There are better upgrade strategies out of the box but, I won’t discuss them here.
Let’s put it under /data/jenkins/workspace/springboot-jersey-swagger (which will be eventually mapped to Jenkins container’s below path so that it’s reachable in Jenkins) and don’t forget to grant it with execution permission.
$ sudo touch /data/jenkins/workspace/springboot-jersey-swagger/deploy-app-springboot-jersey-swagger.sh
$ sudo chmod +x /data/jenkins/workspace/springboot-jersey-swagger/deploy-app-springboot-jersey-swagger.sh
$ sudo vim /data/jenkins/workspace/springboot-jersey-swagger/deploy-app-springboot-jersey-swagger.sh

#!/bin/bash

# We can make it much more flexible if we expose some of below as parameters
APP="springboot-jersey-swagger"
MARATHON="192.168.56.118:8080"
PAYLOAD="${WORKSPACE}/marathon-app-springboot-jersey-swagger.json"
CONTENT_TYPE="Content-Type: application/json"

# Delete the old application
# Note: again, this can be enhanced for a better upgrade strategy
curl -X DELETE -H "${CONTENT_TYPE}" http://${MARATHON}/v2/apps/${APP}

# Wait for a while
sleep 2

# Post the application to Marathon
curl -X POST -H "${CONTENT_TYPE}" http://${MARATHON}/v2/apps -d@${PAYLOAD}

Plug It In By Adding One More Build Step In Jenkins

Simply add one more build step after the Maven step.
It's a "Execute Shell" build step and the commands are really simple:
cd ${WORKSPACE}
./deploy-app-springboot-jersey-swagger.sh
Save it.

Trigger the Process Again

By default, any code check-in will automatically trigger the build and deployment process.
But for testing purpose, we can click the “Build Now” to manually trigger it.
The Console Output is really verbose but it can help you understand the whole process. Check it out here.

Once the process is done in Jenkins (we can monitor the progress by viewing the Console Output), we can see our application exist in “Deployments” tab and quickly move into “Application” tab of Marathon which means that the deployment is done.
Note: if an application keeps pending in deployment because of “waiting for resource offer”, it’s most likely because of no sufficient resource (e.g. Mem) set in Mesos. In this case, we may need to add more Mesos Slave or fine tune the JSON deployment file with fewer resources.



One may notice that our application is under “Unknown”. Why? It’s because we haven’t set up our health check mechanism yet so Marathon doesn't really know whether the application is healthy or not.

Verify Our App

Once application is deployed by Mesos + Marathon, there will be new Docker container(s) spun up automatically.
But how can we access our application deployed by Mesos + Marathon? The port configuration part in Marathon may be a bit confusing and we will figure it out later. In current stage we have to see what port our app is exposed, by below command:
$ docker ps
CONTAINER ID        IMAGE                                                         COMMAND                  CREATED             STATUS              PORTS                                          NAMES
a020940ae0b5        192.168.56.118:5000/devops/springboot-jersey-swagger:latest   "java -jar /springboo"   8 minutes ago       Up 8 minutes        0.0.0.0:31489->8000/tcp                        mesos-e51e55d3-cd64-4369-948f-aaa433f12c1e-S0.b0e0d351-4da0-4af5-be1b-5e7dab876d06
...

As what I highlighted in red, the port exposed to host is 31489 so our application URL will be:
http://{the docker host ip}:31489, or http://192.168.56.118:31489 in my case.

Yes, once you reach here, our application's continuous deployment is now part of our integrated CI + CD pipeline. We did it!

Conclusion

Now it's time to recap our DevOps story.
I update a bit on the diagram which may be better for me to illustrate the process and components we have engaged.

The Development As Usual

Firstly, as usual, we develop our RESTful webservices by following Microservices architecture with components like Java, Spring Boot, Jersey, Swagger, Maven and more.
Our source code is hosted by Gogs, which is our private Git Repository.
We develop, do unit test, check in code -- everything is just as usual.

Related posts:

The Continuous Integration (CI) Comes To Help

Jenkins, acted as our CI Server, comes to help which detects changes in our Git Repository and triggers our integrated CI + CD pipeline.
For CI part, Jenkins engages Maven to run through tests, both Unit Test and SIT if any and build our typical Spring Boot artifact, an all-in-one fat Jar file. Meanwhile, with the Docker Maven plugin, docker-maven-plugin, Maven can take one step further to help build our desired Docker image where our application Jar file is being hosted.
The Docker image will be published to our private Docker Registry and our CI part ends.

Related posts:

The Final Show in Continuous Deployment (CD)

Apache Mesos, together with Marathon and ZooKeeper founds the CD world.
The trigger point is still in Jenkins -- by adding one more build step -- once CI part is done, Jenkins then executes our Shell scripts where the interactions with Marathon are wrapped.
By posting a predefined JSON payload, or the deployment file, to Marathon, it will deploy our application to Mesos platform. As a result, Docker container(s) will be spun up automatically with specific resources (e.g. CPU, RAM) allocated for our application.
Accordingly, the web services offered by our application are up and running.
That's our whole story of "from code to online services".

Future Improvements?

Yes, of course. Anyway, it's still a DevOps prototype.
Along the way, for simplicity purposes, there are some topics or concerns that haven't been properly addressed yet which include but are not limited to:
  • Using multi-server cluster to make it "real";
  • Introducing security and ACL to bring in necessary control;
  • Applying HAProxy setup for the service clustering;
  • Enabling auto scaling;
  • Adding health check mechanism for our application into Marathon;
  • Fine-tuning the application upgrade strategy;
  • Engaging Consul for automatic service discovery;
  • Having a centralized monitoring mechanism;
  • Trying it out with AWS or Azure;
  • And more

Final Words

Continuous Delivery (= Continuous Integration + Continuous Deployment) is a big topic and has variable solutions in the market now, be it commercial or open source.
I started working on this set-up mainly to make my hands dirty and also help developers and administrators to learn how to use a series of open source components to build up a streamlined CI+CD pipeline so that we can sense the beauty of continuous delivery.
As mentioned in previous chapter of "Future Improvements", a lot more things are still pending for exploring. But that's also the reason why architects like me love it the way that IT rocks, isn't it?

Enjoy DevOps and appreciate your comments if any!


Monday, 20 June 2016

From Code To Online Services: My experiments of DevOps - Development of RESTful Web Services by Spring Boot, Jersey, Swagger


1. The Stack of Frameworks / Specs


Well, even any application can be built and used for this experiment, but why not build an interesting one: a real world RESTful service by saying hello to someone? Alright, that’s just another statement for a hello world application;)

Here we go:

  • Sprint Boot: The fundamental framework for the services.
  • Jersey: The RI of JAX-RS specs.
  • Swagger: The API documentation specs and tooling.
  • For testing, we’re going to use 
    • Spring Boot Test Framework (spring-boot-starter-test) with Junit, Hamcrest
    • Rest Assured

2. Git Server: Gogs


Even we can setup Git Server later, but as version control is important, please do it as early as possible.
Note: I did try GitLab but: 1) it’s too slow running in my laptop; and 2) I was facing several permission issues being thrown from embedded Nginx…so I changed my mind and had Gogs instead as it’s really “a painless self-hosted Git service” as advertised.

2.1 Spin Up the Docker Container

Login to the Ubuntu VM, our logical host and execute below commands:

1
2
3
4
5
$ cd ~
$ mkdir cd_demo
$ cd cd_demo
$ touch docker-compose.yml
$ vim docker-compose.yml

1
2
3
4
5
6
7
Gogs:
  image: 'gogs/gogs:latest'
  ports:
    - '3000:3000'
    - '1022:22'
  volumes:
    - '/data/gogs:/data'

Note: it’s a good practice to mount a local path (here is /data) for the container so all valuable data like settings, database will not be gone even we upgrade our container. 

$ docker-compose up

Now, we have spun up our first Docker container for Gogs, our Git server.

2.2 Configuration

In just a few seconds, the Gogs container should have been up and running and we can access the app by: http://192.168.56.118:3000

It would guide us for the “Install Steps For First-time Run”.
Below were my inputs, for your reference:
  • Database Settings
    • Database Type: SQLite3
    • Path: /data/db/gogs.db
  • Application General Settings
    • Application Name: Gogs: Go Git Service
    • Repository Root Path: /data/git/gogs-repositories
    • Run User: git
    • Domain: 192.168.56.118
    • SSH Port: 22
    • HTTP Port: 3000
    • Application URL: http://192.168.56.118:3000/
    • Log Path: data/log
  • Optional Settings
    • Admin Account Settings: omitted
Once done, Gogs will persistent the settings and redirect to the home page.

2.3 Create Our First Repository

All these can be done via Gogs UIs:
Gogs - New Repository
Gogs - New Repository for Spring Boot, Jersey, Swagger










































2.4 Clone the Git Repository to Kick Off Development

By command line, it’s very simple and straightforward.
Open Git Bash and key in:

$ git clone http://192.168.56.118:3000/bright/springboot-jersey-swagger.git

Or we can do it by using Eclipse if you prefer to.

3. Create Maven-based Project Structure

Since development part is my strongest part and code says more than words, let’s make it short but clear during the experiment.
Apparently, as we follow Maven’s best practices to build the project so the folder structure is exactly Maven-driven:
---------------------------------------------
/
   LICENSE
   pom.xml
   README.md
├───src
   ├───main
      ├───java
         └───bright
             └───zheng
                 └───poc
                     └───api
                            ApiApplication.java
                         ├───config
                                JerseyConfig.java
                         ├───model
                                Hello.java
                         └───resources
                                 HelloResource.java
      └───resources
             application.yml
             bootstrap.yml
             log4j.xml
          ├───static
   └───test
       └───java
           └───bright
               └───zheng
                   └───poc
                       └───api
                               ApplicationTests.java
---------------------------------------------

Meanwhile, I’ll highlight some important files with necessary comments so that we can easily understand how to develop a RESTful application by using abovementioned frameworks.

3.1 pom.xml

/pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
       <modelVersion>4.0.0</modelVersion>

       <parent>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-parent</artifactId>
              <version>1.3.5.RELEASE</version>
              <relativePath />
       </parent>

       <groupId>bright.zheng.poc.api</groupId>
       <artifactId>springboot-jersey-swagger</artifactId>
       <version>1.0.0-SNAPSHOT</version>
       <packaging>jar</packaging>

       <name>springboot-jersey-swagger-docker</name>
       <description>POC - Restful API by Spring Boot, Jersey, Swagger</description>


       <properties>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
              <java.version>1.8</java.version>
             
              <!-- project related properties -->
              <start-class>bright.zheng.poc.api.ApiApplication</start-class>
              <swagger-jersey2-jaxrs>1.5.3</swagger-jersey2-jaxrs>
              <rest-assured>3.0.0</rest-assured>
       </properties>

       <dependencies>
              <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-starter-actuator</artifactId>
              </dependency>
              <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-starter-jersey</artifactId>
              </dependency>
              <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
              <dependency>
                     <groupId>io.swagger</groupId>
                     <artifactId>swagger-jersey2-jaxrs</artifactId>
                     <version>${swagger-jersey2-jaxrs}</version>
              </dependency>

              <!-- testing related dependencies -->
              <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-starter-test</artifactId>
                     <scope>test</scope>
              </dependency>
              <dependency>
                     <groupId>io.rest-assured</groupId>
                     <artifactId>rest-assured</artifactId>
                     <version>${rest-assured}</version>
                     <scope>test</scope>
              </dependency>
       </dependencies>

       <build>
              <plugins>
                     <plugin>
                           <groupId>org.springframework.boot</groupId>
                           <artifactId>spring-boot-maven-plugin</artifactId>
                     </plugin>
              </plugins>
       </build>
</project>

3.2 ApiApplication.java


\src\main\java\bright\zheng\poc\api\ApiApplication.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package bright.zheng.poc.api;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

/**
 * This is a typical boostrap class for Spring Boot based application
 * 
 * @author bright.zheng
 *
 */
@SpringBootApplication
public class ApiApplication
extends SpringBootServletInitializer {  
       @Override
       protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
            return builder.sources(ApiApplication.class);
       }

       public static void main(String[] args) {
              SpringApplication.run(ApiApplication.class, args);
       }
}

3.3 HelloResource.java

\src\main\java\bright\zheng\poc\api\resources\HelloResource.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package bright.zheng.poc.api.resources;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import bright.zheng.poc.api.model.Hello;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

@Component
@Path("/v1/hello")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Api(value = "Hello API - say hello to the world", produces = "application/json")
public class HelloResource {

       private static final Logger LOGGER = LoggerFactory.getLogger(HelloResource.class);

       @GET                                     //JAX-RS Annotation
       @Path("/{name}")     //JAX-RS Annotation
       @ApiOperation(                           //Swagger Annotation
                     value = "Say hello by providing the name by URI, via standard json header",
                     response = Hello.class) 
       @ApiResponses(value = {           //Swagger Annotation
              @ApiResponse(code = 200, message = "Resource found"),
           @ApiResponse(code = 404, message = "Resource not found")
       })
       public Response sayHelloByGet(@ApiParam @PathParam("name") String name) {
              LOGGER.info("v1/hello/{} - {}", name, MediaType.APPLICATION_JSON);
              return this.constructHelloResponse(name, MediaType.APPLICATION_JSON);
       }
      
       ///////////////////////////////////////////////////////////////

       private Response constructHelloResponse(String name, String via) {
              //for testing how we handle 404
              if ("404".equals(name)) {
                     return Response.status(Status.NOT_FOUND).build();
              }
              Hello result = new Hello();
              result.setMsg(String.format("Hello %s - %s", name, via));
              return Response.status(Status.OK).entity(result).build();
       }
}



3.4 JerseyConfig.java

\src\main\java\bright\zheng\poc\api\config\JerseyConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package bright.zheng.poc.api.config;

import javax.annotation.PostConstruct;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.wadl.internal.WadlResource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import bright.zheng.poc.api.resources.HelloResource;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.jaxrs.listing.SwaggerSerializers;

/**
 * Jersey Configuration
 *
 * <p>
 *
 * The endpoint will expose not only the real APIs,
 * but also two important documents:
 * <li> - swagger spec: /swagger.json</li>
 * <li> - WADL spec: /application.wadl</li>
 *
 * </p>
 *
 * @author bright.zheng
 *
 */
@Component
public class JerseyConfig extends ResourceConfig {

       @Value("${spring.jersey.application-path:/api}")
       private String apiPath;

       public JerseyConfig() {
              this.registerEndpoints();
       }
      
       @PostConstruct
       public void init() {
              this.configureSwagger();
       }

       private void registerEndpoints() {
              this.register(HelloResource.class);
              this.register(WadlResource.class);
       }

       private void configureSwagger() {
              this.register(ApiListingResource.class);
              this.register(SwaggerSerializers.class);

              BeanConfig config = new BeanConfig();
              config.setTitle("POC - Restful API by Spring Boot, Jersey, Swagger");
              config.setVersion("v1");
              config.setContact("Bright Zheng");
              config.setSchemes(new String[] { "http", "https" });
              config.setBasePath(this.apiPath);
              config.setResourcePackage("bright.zheng.poc.api.resources");
              config.setPrettyPrint(true);
              config.setScan(true);
       }

}


4. Swagger UI Integration

It would be great to have Swagger UI integration for automatic API documentation.
Simply do below steps:


5. Testing

Programmers write unit tests, of course. Here we go:

5.1 ApplicationTests.java

\src\test\java\bright\zheng\poc\api\ApplicationTests.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package bright.zheng.poc.api;

import static io.restassured.RestAssured.expect;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import io.restassured.RestAssured;

/**
 * Application level testing
 *
 * @author bright.zheng
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ApiApplication.class)
@WebIntegrationTest({"server.port=0"})
public class ApplicationTests {
      
    @Value("${local.server.port}")
    private int port;
      
    @Before
    public void setupURL() {
        RestAssured.baseURI = "http://localhost";
        RestAssured.port = port;
        RestAssured.basePath = "/";
    }
 
    /**
     *
     * {"app":{"name":"springboot-jersey-swagger"},"build":{"version":"1.0.0-SNAPSHOT"}}
     *
     * @throws Exception
     */
    @Test
    public void springroot_info() throws Exception {
       expect().
              body("app.name", equalTo("springboot-jersey-swagger")).
              body("build.version", equalTo("1.0.0-SNAPSHOT")).
           when().
              get("/info");
    }
   
    /**
     * {"msg":"Hello Bright - application/json"}
     *
     * @throws Exception
     */
    @Test
    public void hello_get() {
       expect().
              body("msg", containsString("Hello Bright")).
        when().
              get("/api/v1/hello/Bright");
    }

}



6. Run It


6.1 Start It Up


By Maven command line in Windows, it would be:
1
2
3
> cd /d D:\development\workspaces\java\POC\springboot-jersey-swagger
> mvn package
> java -jar target/springboot-jersey-swagger-1.0.0-SNAPSHOT.jar



By Eclipse, simply open ApiApplication.java and click Run menu -> Run As -> Java Application, it would be booted up within seconds.

6.2 Take a Look at the API Documentation UI provided by Swagger

Simply open browser and key in: http://localhost:8000/





























We can even try the services. Simply key in the parameter (name here) and click the “Try it out!” button, we can see everything in the same page including: 

    • Curl command if we want to reproduce it on Curl
    • URL we just requested
    • Response Code
    • Response Body
    • Response Headers

    7. Commit and Push to Git


    Commit and push our first draft of Hello Service to our local Git Server.

    By Git Bash:
    1
    2
    3
    4
    $ git status
    $ git add –A
    $ git commit –m “first draft of Hello Service”
    $ git push
    


    Note: I also synced the code to GitHub, follow below steps:
    • Create a GitHub repository: itstarting/springboot-jersey-swagger
    • Execute commands like:
    1
    2
    3
    4
    5
    6
    7
    8
    $ cd /D/development/workspaces/java/POC/springboot-jersey-swagger
    $ git remote add github https://github.com/brightzheng100/springboot-jersey-swagger.git
    $ git remote -v
    github  https://github.com/brightzheng100/springboot-jersey-swagger.git (fetch)
    github  https://github.com/brightzheng100/springboot-jersey-swagger.git (push)
    origin  http://192.168.56.118:3000/bright/springboot-jersey-swagger.git (fetch)
    origin  http://192.168.56.118:3000/bright/springboot-jersey-swagger.git (push) 
    $ git push -u github master
    

    By Eclipse it's very simple: 
    • Right click project -> team -> Commit…
    • Choose all items in “Unstaged Changes”, right click -> Add to Index, which will add all items into “Staged Changes”
    • Key in comments in “Commit Message” and click “Commit and Push” button and eventually we have below result.


    Well, the development works are done!



    For complete source code, please visit my GitHub. Enjoy coding!