Showing posts with label service. Show all posts
Showing posts with label service. Show all posts

Wednesday, February 14, 2018

Kubernetes getting started: Minikube

0. Intro: What is Kubernetes?

From wiki:
Kubernetes (commonly referred to as "K8s"[3]) is an open-source system for automating deployment, scaling and management of containerized applications[4] that was originally designed by Google and now maintained by the Cloud Native Computing Foundation. It aims to provide a "platform for automating deployment, scaling, and operations of application containers across clusters of hosts".[3] It works with a range of container tools, including Docker.

In shorts, kubernetes is a tool which allows you to upload in it many docker images, link them by services, scale and configure them.


1. Minikube:  simple image of kubernetes "to play". 

The simplest way to get familiar with kubernetes is Minikube. Let's install it.


1.1 Virtual box installation. 

It also can be running using HyperV, but I prefer Virtual box. It can be download from here.

1.2 Docker installation.

Theoretically, this step is optional, because minikube has it's own docker. So we can use public docker images without docker installation.  But it case of development, if we have to create new images - docker is needed. 

1.3 Minikube installation. 

It can be downloaded from here

1.4. Kubectl installation. 

It can be downloaded from here. Also don't miss the steps regarding putting it into system PATH variable. For example for linux it's a " sudo mv ./kubectl /usr/local/bin/kubectl"

Now let's check if everything is ok with minikube and kubectl:
$ minikube status
minikube: 
cluster: 
kubectl: 


$ kubectl version
Client Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.3", GitCommit:"d2835416544f298c919e2ead3be3d0864b52323b", GitTreeState:"clean", BuildDate:"2018-02-07T12:22:21Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"linux/amd64"}
The connection to the server localhost:8080 was refused - did you specify the right host or port?

They should produce similar output to listed above.


2. Minikube: warming up

First of all, we have to set it up: it should download needed ISO image and put it into VirtualBox.
For this, theoretically we have just to run:
$ minikube start
But if for some reason it's not working, you can try to point it to vm-driver in explicit way:
$ minikube start --vm-driver="virtualbox"

For me, this process failed with error:
The vboxdrv kernel module is not loaded. Either there is no module\n         available for the current kernel (4.9.0-5-amd64) or it failed to\n         load. Please recompile the kernel module and install it by\n\n           sudo /sbin/vboxconfig\n\n 

When I executed:
# /sbin/vboxconfig

If also failed, with complains regarding linux headers. 

So I installed headers:
# apt-get install linux-headers-amd64 linux-headers-4.9.0-5-amd64

Executed again: 
# /sbin/vboxconfig

And again:
$ minikube start
demien:minikube$ minikube start
Starting local Kubernetes v1.9.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Downloading localkube binary
 162.41 MB / 162.41 MB [============================================] 100.00% 0s
 65 B / 65 B [======================================================] 100.00% 0s
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.

Loading cached images from config file.

- finally! :)

To make sure it works we can run:
$ minikube status
minikube: Running
cluster: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.100


3. Minikube: Simple operations

3.1. Dashboard. 

To open dashboard:
$ minikube dashboard
Opening kubernetes dashboard in default browser...

Your browser should show something like this: 


From dashboard you can configure cluster pods, deployments, replicas, servers.


3.2 Minikube: terminal

We can open terminal on minikube by running: 
$ minikube ssh
In example below I checked if my home folder is mounted to kubernetes:




4. Kubectl: simple operations

More "high-level" operations, like listed above should be executed by "minikube" command, but "low-lovel" operations belongs to "kubectl". 

4.1. Context swithing

First of all it's better to make sure kubectl is connected to minikube by running: 
$ kubectl config use-context minikube
Switched to context "minikube".

4.2. Deployment

To deploy docker image to kubernetes, we have 2 options: deploy from command line, or by creation and upload of deployment descriptor file.  

First option is faster, but for only simple situations: 
$ kubectl run hello-nginx --image=nginx --port=80 --replicas=2
deployment "hello-nginx" created

For second option,  we have to create a deployment descriptor like this: 

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: hello-nginx
spec:
  selector:
    matchLabels:
      run: hello-nginx
  replicas: 2 # tells deployment to run 2 pods matching the template
  template: # create pods using pod definition in this template
    metadata:
      # unlike pod-nginx.yaml, the name is not included in the meta data as a unique name is
      # generated from the deployment name
      labels:
        run: hello-nginx
    spec:
      containers:
      - name: hello-nginx
        image: nginx
        ports:
        - containerPort: 80

Most important things are at the end:
- image - name of the docker image which should be pulled from docker public registry
- containerPort - port which will be exposed outside

After creation we have to deploy this descriptor by running(deployment-demo.yml - file name):
$ kubectl apply -f ./deployment-demo.yml 
deployment "nginx-deployment" created

Now we can check in dashboard: in should be one deployment :




and it should be 2 pods, because we defined "2 replicas" in descriptor:


 

Pod in kubernetes is like a "logical host": it's a running docker image. We defined 1 image with replica=2, so kubernetes is running this image twice. 

4.3 Service

Uploaded docker image is up and running now. But  we can't use is "outside" so far. For this we need the service, which will expose running container port outside. 
For service creation we have 2 the same options: from command line and from descriptor file. 

From command line:

$ kubectl expose deployment hello-nginx --type=NodePort
service "hello-nginx" exposed
 
From descriptor file(with name service-demo.yml): 

apiVersion: v1
kind: Service
metadata:
  name: hello-nginx
  labels:
    run: hello-nginx
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080
    protocol: TCP
  selector:
    run: hello-nginx

File upload: 
$ kubectl apply -f ./service-demo.yml 
service "hello-nginx" created


Now we can check "service" menu: beside standard "kubernetes" service it should be our "hello-nginx": 


Also we can see the "outside" port here: 30080 - actually we defined it in descriptor. 
Now we can open this port using kubernetes ip and result should be: 




5. The end. 

No dubs, kubernetes is a great tool "to rule them all": to manage and configure running containers in a cluster. More tutorial can be found on official site: https://kubernetes.io/docs/tutorials/

Sunday, January 1, 2017

Micro services with spring boot

1. Into

From wikipedia:
Microservices is a specialisation of an implementation approach for service-oriented architectures (SOA) used to build flexible, independently deployable software systems. Services in a microservice architecture (MSA)[1] are processes that communicate with each other over a network in order to fulfill a goal. These services use technology-agnostic protocols.[2][3][4] The microservices approach is a first realisation of SOA that followed the introduction of DevOps and is becoming more popular for building continuously deployed systems.[5][6]

Sometimes, when application is very big, it has a complicated logic. Component A is using component B, which is using C, which depends on D.... In such systems when we a changing, for example component D - it's hard to predict which components will be affected. Also, developers with just joined the project, have to spend a lot of time to understand how everything works.

Monolith architecture:

Microservice approach is about splitting big application into smaller units(services) which are independent, but may communicate with each others.

Microservice architecture:


2. Types of MicroServices

When we spitted big application into several smaller services, we got set of REGULAR services. But Also, for better and transparent communication we may need several INFRASTRUCTURE services.


Examples of INFRASTRUCTURE services:
 - config server: sometimes it's better to have all configuration settings "in one place": in one dedicated server. All our REGULAR services will be reading them from such server.
- discovery server: our REGULAR services can be switched to work on another port, or moved to another DNS name how can we handle that? For  such purposes we may have a DISCOVERY server: every REGULAR service have to register himself on such server by his "nick-name" for example as "user-service" and other service will be able to get his DNS and port number by asking for this nick-name from DISCOVERY service.
- edge server(edge service): interface on the "edge of the cloud". We may have some security checks in our infrastructure(in REGULAR services). But there are may services, should we "copy+paste" this security checks to all our REGULAR services? It's better to have one "proxy" server on the "edge of the cloud". Clients will be accessing this EDGE server and it will forwarding them to our REGULAR services.

Communication diagram:


Description:
Client want get some data from User service by it endpoint "/user/getDetails". For that, client just need to know DNS and port of our EDGE server(localhost:8080) and User service nick-name("user-service"). So, it can call EDGE server by URL: localhost:8080/user-service/user/getDetail - and that is it. EDGE server will call DISCOVERY server with request like "give me URI for user-service", DISCOVERY server will return URI: localhost:9003, and after that EDGE server will call localhost:9003/user/getDetails URI and return result to Client.





3. Our application structure

For test microservice-based application we are going to create an infrastructure for internet shop where people can login, find some interesting items, put them into cart and create an order.
We will have set of REGULAR services:
- user service
- item service
- cart service

And INFRASTRUCTURE services:
- config server
- discovery server
- egde server(edge serice)

4. Config server

Structure:

It's a simple SpringBoot application with several config files for our REGULAR services.
pom.xml: 

<?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>

   <groupId>com.demien.services</groupId>
   <artifactId>config-server</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>config-server</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.4.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-config-server</artifactId>
      </dependency>
   </dependencies>

   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR2</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

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


</project>


ConfigServerApplication.java:


package com.demien.services;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

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

Beside regular SpringBoot annotation @SpringBootApplication we have in addition @EnableConfigServer which makes all stuff related with configs working(makes files in /config directory accessible by corresponding servers).

application.properties:
spring.profiles.active=native
server.port=8888

- here we are defining server port - it has to be static. profile=native is needed, because config files are located in filesystem(by default - GIT repository).

Configuration files for REGULAR services are almost the same, I'm configuring just a port number. But in real life we can put much more settings.

cart-service.properties:
server.port=${PORT:9001}

item-service.properties:
server.port=${PORT:9002}

user-service.properties:
server.port=${PORT:9003}

Now we can start our service by running mvn spring-boot:run
And check how it works by opening in a browser URL: http://localhost:8888/item-service/default
Result should be something like this:
{"name":"item-service",
 "profiles":["default"],
 "label":null,
 "version":null,
 "state":null,
 "propertySources":[
    {"name":"file:config/item-service.properties",
     "source":{"server.port":"${PORT:9002}"}
    },    
    {"name":"file:./config/item-service.properties",
     "source":{"server.port":"${PORT:9002}"}
    }
 ]
}

5. Discovery server

We will use most popular implementation of Discovery server: EUREKA by Netflix. To turn spring boot application to EUREKA server we need just one annotation.

Project structure:



pom.xml

<?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>

   <groupId>com.demien.services</groupId>
   <artifactId>eureka-server</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>eureka-server</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.4.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>

      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-config</artifactId>
      </dependency>

      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-eureka-server</artifactId>
      </dependency>
   </dependencies>

   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR3</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

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


</project>

EurekaServerApplication.java

package com.demien.services;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

- as I mentioned before, we need just one additional annotation to turn our application to Eureka server: @EnableEurekaServer.

bootstrap.properties


spring.application.name=eureka-serverspring.cloud.config.uri=http://localhost:8888server.port=8761eureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseeureka.instance.hostname=localhosteureka.instance.prefer-ip-address=true

Now we can start our application the same way mvn spring-boot:run
And open URL: http://localhost:8761/
Result should be something like this:



Not a one REGULAR service is running now, so list of registered service is empty now(no instances available).


6. Edge server(edge service)

Edge-server is just a "proxy". Spring boot is providing implementation of  ZUUL proxy by Netflix.
In similar way to DISCOVERY server, we just need one annotation.

Project structure:



pom.xml

<?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>

   <groupId>com.demien.services</groupId>
   <artifactId>edge-service</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>edge-service</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.4.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-eureka</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-zuul</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
   </dependencies>

   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR3</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

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


</project>


EdgeServiceApp.java

package com.demien.services;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class EdgeServiceApp {

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

}

We just added one annotation @EnableZuulProxy and this is it! Now all requests to EDGE server in format /server-name/server-resource will be forwarder to corresponding server using DISCOVERY server.

bootstrap.properties
spring.application.name=edge-servicespring.cloud.config.url=http:/localhost:8888

7. Regular services

Regular services are not so interesting - it's just a regular SpringBoot applications and they looks similar to EDGE service. Later I'll show some code fragments of one of them. Of course all source can be downloaded from link on the bottom. 

As I mentioned before, there are 3 regular services: CartService, ItemService, UserService. All of them have to be registered in EUREKA, so then all these services are running EUREKA server will show all of them together with EDGE service. 


8. Regular services : Cart Service

Regular services are not interesting - it's just a regular SpringBoot applications, so I will show only most interesting files of one them: Cart Service. This service is communicating with the others: UserService and ItemService.

First of all, for communication we need a RestTemplate:

package com.demien.services.cart;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}


And now, we can use this RestTemplate for communication with the others services:

package com.demien.services.cart.controller;

import com.demien.services.cart.domain.CartItem;
import com.demien.services.cart.repository.CartItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.websocket.server.PathParam;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.List;

@EnableDiscoveryClient@RestController@RequestMapping(value = "/cart")
public class CartController {

    public final static String ITEM_SERVICE_PATH="http://item-service/item";
    public final static String USER_SERVICE_PATH="http://user-service/user";
    public final static String USER_BY_TOKEN=USER_SERVICE_PATH+"/byToken";

    @Autowired
    private CartItemRepository cartItemRepository;

    @Autowired
    private RestTemplate restTemplate;


    @RequestMapping(method = RequestMethod.GET, value="/{tokenId}")
    public List<CartItem> getCardItemsByToken(@PathParam("tokenId") String tokenId) {
        return cartItemRepository.getCardItemsByToken(tokenId);
    }

    @RequestMapping(method = RequestMethod.POST)
    public void addUserItem(@RequestParam String tokenId, @RequestParam String itemId, @RequestParam String amount) {
        CartItem cartItem =new CartItem(itemId, Integer.parseInt(amount));
        cartItemRepository.addCardItemByToken(tokenId, cartItem);
    }

    @RequestMapping(value = "/order", method = RequestMethod.POST)
    public String getCartOrderByToken(@RequestParam("tokenId") String tokenId) {
        if (tokenId == null) {
            throw new RuntimeException("TokenId is null");
        }
        StringBuilder result=new StringBuilder();
        Object userResponse = getUserDetailsByToken(tokenId);
        String userName = (String)getValueFromResponse(userResponse, "name");
        String userAddress = (String)getValueFromResponse(userResponse, "address");

        result.append("User:"+userName+"\n");
        result.append("Address:"+userAddress+"\n");

        List<CartItem> cartItems = cartItemRepository.getCardItemsByToken(tokenId);
        BigDecimal total=BigDecimal.ZERO;
        int index=0;
        for (CartItem cartItem:cartItems) {
            index++;
            Object itemResponse = getItemDetails(cartItem.getItemId());
            String itemName = (String)getValueFromResponse(itemResponse, "itemName");
            BigDecimal price = new BigDecimal( (Double) getValueFromResponse(itemResponse, "price"));
            BigDecimal itemTotal = price.multiply(new BigDecimal(cartItem.getAmount()));
            total = total.add(itemTotal);
            result.append("  "+index+". item:"+itemName+", price:"+price+", amount:"+cartItem.getAmount()+", itemTotal:"+itemTotal +"\n");
        }
        result.append("Total:"+total);

        return result.toString();
    }

    public Object getValueFromResponse(Object response, String value) {
        return ((LinkedHashMap)response).get(value);

    }

    public Object getItemDetails(String itemId) {
        return restTemplate.getForObject(ITEM_SERVICE_PATH+"/"+itemId, Object.class);
    }

    public Object getUserDetailsByToken(String tokenId) {
        return restTemplate.getForObject(USER_BY_TOKEN+"/" + tokenId, Object.class);
    }


}


And we need to define "nick-name" of our service in boottrap.properties:
spring.application.name=cart-servicespring.cloud.config.uri=http://localhost:8888ribbon.http.client.enabled=true

As you can see, all we need to know for communication with another services is "nick-names"  :
    public final static String ITEM_SERVICE_PATH="http://item-service/item";
    public final static String USER_SERVICE_PATH="http://user-service/user";
We don't need to know the exact server name and port, just service name from DISCOVERY service.

9. Working with regular services. 

We have an EDGE service which is running on localhost:8080, so to call any service we want we need to call EDGE service by pattern: localhost:8080/service-name/service-resource

For example, to call endpoint /cart/order from Cart-service described above, we have to call:
http://localhost:8080/cart-service/cart/order

"cart-service" is a "nick-name" defined in service bootstrap.properties file:
spring.application.name=cart-service

To login, using UserService, we have to call:
http://localhost:8080/user-service/user/login

To get list of all items form ItemService:
http://localhost:8080/item-service/item/getAll

10. The end. 

All source code can be downloaded from here.