Affichage des articles dont le libellé est Cloud. Afficher tous les articles
Affichage des articles dont le libellé est Cloud. Afficher tous les articles

samedi 28 juin 2014

Server provisioning with Vagrant, CoreOS and Docker

In this article, we'll see how easy it is to build a custum Linux image an to deploy it in cloud using modern DevOps tools like Vagrant, CoreOS and Docker and the Amazon Web Services cloud provider.

Setup Vagrant and CoreOS

To build a custom Linux image for Docker we need to run Docker on Linux... ;-) CoreOS is a minimalist Linux distribution that ships with Docker and a few other services. As we want to setup our custom Linux image for Docker locally, let use CoreOS with Vagrant. Vagrant is about provisionning virtual development environment and it's exactly what we need to launch CoreOS quickly on Windows, Mac OS X or Linux. To get all this stuff to work together just follow the Running CoreOS on Vagrant documentation on the CoreOs website.
git clone https://github.com/coreos/coreos-vagrant.git
cd coreos-vagrant
vagrant up
Bringing machine 'core-01' up with 'virtualbox' provider...
==> core-01: Checking if box 'coreos-beta' is up to date...
==> core-01: Clearing any previously set network interfaces...
==> core-01: Preparing network interfaces based on configuration...
    core-01: Adapter 1: nat
    core-01: Adapter 2: hostonly
==> core-01: Forwarding ports...
    core-01: 22 => 2222 (adapter 1)
==> core-01: Running 'pre-boot' VM customizations...
==> core-01: Booting VM...
==> core-01: Waiting for machine to boot. This may take a few minutes...
    core-01: SSH address: 127.0.0.1:2222
    core-01: SSH username: core
    core-01: SSH auth method: private key
    core-01: Warning: Connection timeout. Retrying...
    core-01: Warning: Connection timeout. Retrying...
    core-01: Warning: Connection timeout. Retrying...
    core-01: Warning: Connection timeout. Retrying...
==> core-01: Machine booted and ready!
==> core-01: Setting hostname...
==> core-01: Configuring and enabling network interfaces...
==> core-01: Machine already provisioned. Run `vagrant provision` or use the `--provision`
==> core-01: to force provisioning. Provisioners marked to run always will still run.
You can now SSH to your virtual environnement :
CoreOS (beta)
core@core-01 ~ $

Create a custom Debian image

Once logged into CoreOS, we can see that Docker is available and that no image is available yet :
core@core-01 ~ $ docker images
REPOSITORY          TAG                 IMAGE ID
Let start a Debian Linux with Docker container :
core@core-01 ~ $ docker run -t -i debian:7.4 cat /etc/*-release | grep NAME
PRETTY_NAME="Debian GNU/Linux 7 (wheezy)"
NAME="Debian GNU/Linux"
Now we have the Debian Docker image available localy :
core@core-01 ~ $ docker images
REPOSITORY          TAG                 IMAGE ID
debian              7.4                 e565fbbc6033
As we can see, java is unavailable :
core@core-01 ~ $ docker run -t -i debian:7.4 java -version
2014/06/27 14:01:58 exec: "java": executable file not found in $PATH
So, how can we build a custom debian:7.4 based image with java pre-installed ? Nothing simpler than creating a Dockerfile with the necessary command to install the jdk on a debian distribution :
core@core-01 ~ $ touch DockerFile
core@core-01 ~ $ vi DockerFile
FROM debian:7.4
MAINTAINER Antoine Vianey 

RUN apt-get update
RUN apt-get -f install
RUN export DEBIAN_FRONTEND=noninteractive
RUN apt-get -y install --no-install-recommends openjdk-7-jdk
The docker build command builds the image for us... The -t option allow us to specify the repository + tag that will be associated with the resulting image. Read the Dockerfile reference guide for a full description of what you can do with dockerfile.
core@core-01 ~ $ sudo docker build -t avianey/ojdk7-deb .
If every step runs fine the final image is available under the specified tag. If a step fails, interdmediate steps images are kept to speed up the rebuild after having corrected the failing Dockerfile step.
core@core-01 ~ $ docker images
REPOSITORY          TAG                 IMAGE ID
avianey/ojdk7-deb   latest              54e73307f07e
debian              7.4                 e565fbbc6033
Check that java is installed :
core@core-01 ~ $ docker run -t -i avianey/ojdk7-deb java -version
java version "1.7.0_55"
OpenJDK Runtime Environment (IcedTea 2.4.7) (7u55-2.4.7-1~deb7u1)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
Everything is fine, we can tag our custom image :
core@core-01 ~ $ sudo docker tag 54e73307f07e avianey/ojdk7-deb:v1
core@core-01 ~ $ docker images
REPOSITORY          TAG                 IMAGE ID
avianey/ojdk7-deb   latest              54e73307f07e
avianey/ojdk7-deb   v1                  54e73307f07e
debian              7.4                 e565fbbc6033

Use the image in the cloud

Now that we have our custom image setup, we need to push it in the Docker Hub or a private Docker repository to use it from the cloud :
core@core-01 ~ $ sudo docker push avianey/ojdk7-deb
The push refers to a repository [avianey/ojdk7-deb] (len: 2)
Sending image list
...
Pushing repository avianey/ojdk7-deb (2 tags)
...
Pushing tag for rev [54e73307f07e] on {https://registry-1.docker.io/v1/repositories/avianey/ojdk7-deb/tags/latest}
Pushing tag for rev [54e73307f07e] on {https://registry-1.docker.io/v1/repositories/avianey/ojdk7-deb/tags/v1}
Everyone can now start a Docker container running the avianey/ojdk7-deb:v1 image. That what we are going to do with a CoreOS Amazon Machine Image :
Using username "core".
Authenticating with public key "imported-openssh-key"
CoreOS (beta)
core@ip-172-31-25-84 ~ $ docker run -t -i avianey/ojdk7-deb:v1 java -version
Unable to find image 'avianey/ojdk7-deb:v1' locally
Pulling repository avianey/ojdk7-deb
54e73307f07e: Download complete
511136ea3c5a: Download complete
405cce5cd17d: Download complete
e565fbbc6033: Download complete
782079a3e1cf: Download complete
3be917c2a827: Download complete
11311a0d3731: Download complete
b4754115da81: Download complete
java version "1.7.0_55"
OpenJDK Runtime Environment (IcedTea 2.4.7) (7u55-2.4.7-1~deb7u1)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
That's it, enjoy !

mardi 12 octobre 2010

Android JAX-RS Client : partie serveur avec Jersey et App Engine

En 2008, Google a ouvert son offre de cloud computing au monde Java. Depuis, le Google appEngine permet à n'importe quelle personne de déployer une application web Java scalable et à haute disponibilité ! Dans ce tutorial, nous allons voir comment exposer un service REST en JSON dans le cloud au moyen de l'appEngine et de l'api Jersey JAX-RS. Le webservice exposé consistera en un simple CRUD d'objets de type User.

Téléchargement et installation des outils

Pour suivre ce tutorial, vous aurez besoin des outils et api suivantes :
Une fois téléchargées et installée, créez un nouveau Web Application Project :


Ajoutez les librairies Jersey suivante dans le répertoire war/WEB-INF :
  • asm.jar
  • commons-validator.jar
  • jackson-core-asl.jar
  • jackson-jaxrs.jar
  • jackson-mapper-asl.jar
  • jackson-xc.jar
  • jersey-client.jar
  • jersey-core.jar
  • jersey-json.jar
  • jettison.jar
  • jsr311-api.jar


Mise en place du modèle de données

Dans le répertoire src/META-INF du projet, nous allons remplacer le fichier de configuration JDO créé par défaut par un fichier de configuration JPA persistence.xml :

<?xml version="1.0" encoding="UTF-8" ?>
<persistence version="1.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
                        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">

    <persistence-unit name="transactions-optional">
        <provider>
                org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider
        </provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

</persistence>

Nous nous contenterons de la configuration JPA minimale.
Notre entité persisté sera une classe User :

package net.androgames.blog.sample.rest.server;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.datanucleus.jpa.annotations.Extension;


@Entity
public class User implements Serializable {

    /**
     * 1 : Version initiale
     */
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Extension(vendorName="datanucleus", key="gae.encoded-pk", value="true")
    private String id;
    
    private String nom;
    private String prenom;
    
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getNom() {
        return nom;
    }
    public void setNom(String nom) {
        this.nom = nom;
    }
    public String getPrenom() {
        return prenom;
    }
    public void setPrenom(String prenom) {
        this.prenom = prenom;
    }
    
}

Mise en place de Jersey et CRUD simple

Pour exposer les fonctionnalités de création, suppression, modification et récupération en REST, nous allons utiliser l'api Jersey. Comme la plupart des framework utilisés dans une application web, sa configuration s'effectue en déclarant une Servlet spécifique dans le fichier web.xml :

<?xml version="1.0" encoding="utf-8"?>
<web-app 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
    version="2.5">

    <servlet>
        <servlet-name>Jersey</servlet-name>
        <servlet-class>
            com.sun.jersey.spi.container.servlet.ServletContainer
        </servlet-class>
        <!-- Packages a analyser -->
        <init-param>
            <param-name>com.sun.jersey.config.property.packages</param-name>
            <param-value>net.androgames.blog.sample.rest.server</param-value>
        </init-param>
        <!-- Mapping JSON POJO -->
        <init-param>
            <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>Jersey</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    
    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>
    
</web-app>

La sérialisation / désérialisation des POJO par Jersey sera effectuée par la classe com.sun.jersey.api.json.POJOMappingFeature. Jersey va ainsi supporter la sérialisation / désérialisation au format JSON. L'implémentation de notre CRUD est la suivante :

package net.androgames.blog.sample.rest.server;

import java.util.List;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

import com.sun.jersey.api.NotFoundException;

@Path("/user")
@Produces("application/json")
@Consumes("application/json")
public class UserService {
    
    private static final Logger log = Logger.getLogger(UserService.class.getName());
    
    private static final EntityManagerFactory ENTITY_MANAGER = 
        Persistence.createEntityManagerFactory("transactions-optional");
    
    public static EntityManager getEntityManager() {
        return ENTITY_MANAGER.createEntityManager();
    }

    /**
     * Mise a jour d'un utilisateur par son id
     * @param id
     * @param user
     * @return
     */
    @POST
    @Path("{id}")
    public User update(
            @PathParam("id") String id, 
            User user) {
        log.info("Mise a jour du user d'id : " + id);
        
        if (user == null) {
            throw new IllegalArgumentException();
        }
        
        EntityManager em = getEntityManager();
        User persistedUser = em.getReference(User.class, id);
        
        if (persistedUser == null) {
            throw new NotFoundException();
        }
        
        persistedUser.setNom(user.getNom());
        persistedUser.setPrenom(user.getPrenom());

        em.getTransaction().begin();
        em.merge(persistedUser);
        em.getTransaction().commit();
        
        return persistedUser;
    }

    /**
     * Recupere un utilisateur par son id
     * @param deviceId
     * @return
     */
    @GET
    @Path("{id}")
    public User get(@PathParam("id") String id) {
        log.info("Recuperation du user d'id : " + id);
        
        EntityManager em = getEntityManager();
        User persistedUser = em.getReference(User.class, id);
        
        if (persistedUser == null) {
            throw new NotFoundException();
        }
        
        return persistedUser;
    }

    /**
     * Recuperation de la liste des utilisateurs
     * @param deviceId
     * @return
     */
    @GET
    @SuppressWarnings("unchecked")
    public List<User> list() {
        log.info("Recuperation des utilisateurs");
        
        EntityManager em = getEntityManager();
        List<User> users = em.createQuery("SELECT u FROM User u").getResultList();
        
        return users;
    }

    /**
     * Supprime un utilisateur par son id
     * @param deviceId
     * @return
     */
    @DELETE
    @Path("{id}")
    public String delete(@PathParam("id") String id) {
        log.info("Suppression du user d'id : " + id);
        
        EntityManager em = getEntityManager();
        User persistedUser = em.getReference(User.class, id);
        
        if (persistedUser == null) {
            throw new NotFoundException();
        }

        em.getTransaction().begin();
        em.remove(persistedUser);
        em.getTransaction().commit();
        
        return id;
    }

    /**
     * Ajoute un utilisateur
     * @param deviceId
     * @return
     */
    @PUT
    public User add(User user) {
        log.info("Ajout d'un utilisateur");
        
        EntityManager em = getEntityManager();
        em.getTransaction().begin();
        em.persist(user);
        em.getTransaction().commit();
        
        return user;
    }
    
}

Jersey se configure au moyen d'annotations :
@Path
Cette annotation permet de spécifier le chemin d'accès à la ressource relativement au chemin paramétré dans le fichier web.xml pour la Servlet Jersey. Utilisée sur la déclaration d'une classe, elle s'applique à toutes ses méthodes. Utilisée sur une méthode, elle s'applique relativement au Path définit pour la classe.
@PathParam
Elle permet de récupérer une partie du Path en paramètre d'une méthode. Dans notre exemple, nous déclarons le Path "/user/{id}" pour la méthode get. Ainsi définit, notre Path nous permet de récupérer la partie mappée par "{id}" grâce à l'annotation PathParam.
@PUT, @POST, @GET, @DELETE
Ces annotations permettent de spécifier la méthode HTTP à mapper sur une méthode de la classe UserService. Seules les requêtes du type spécifié seront transmises à la méthode. Cela permet d'utiliser le même chemin "/user" pour la création d'un utilisateur (PUT), la récupération de tous les utilisateurs (GET) et la suppression d'un utilisateur (DELETE). La récupération d'un utilisateur (GET) et la imse à jour d'un utilisateur (POST) partagent le chemin "/user/{id}".
@Produce
Cette annotation permet de préciser le (ou les) type MIME que Jersey peut utiliser pour sérialiser les retours des méthodes annotées PUT, GET, POST, DELETE, ... Dans notre exemple, nous avons spécifier une sérialisation en JSON pour l'ensemble des méthodes de la classe. Il est également possible de spécifier le type MIME par version, ou d'en spécifier plusieurs séparés par des virgules. Dans ce cas, le type MIME utilisé pour la sérialisation correspond au premier type MIME rencontré dans le header HTTP Accept.
@Consume
Tout comme l'annotation Produce, cette annotation permet de définir un type MIME et, plus précisément, le type MIME qui sera utiliser par Jersey pour désérialiser les valeurs à passer en paramètre aux méthodes exposées. Dans notre exemple, nous attendons donc pour les méthodes add et update une requête HTTP nous envoyant un bean User sérialisé en JSON. Il est également possible de spécifier le type MIME par méthode ou d'en préciser plus d'un.

Test des services avec l'API de test Jersey


Jersey offre la possibilité de recetter rapidement un webservice REST en utilisant la classe Client de son API. L'exemple ci dessous montre comment tester rapidement l'ajout, la mise à jour, la récupération et la suppression d'un utilisateur.

package net.androgames.blog.sample.rest.server;

import javax.ws.rs.core.MediaType;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;

public class TestUserService {

    private static final String URL = "http://localhost:8888/user";
    
    /**
     * 
     * @param args
     */
    public static void main(String[] args) {
        
        // creation d'un client Jersey
        Client c = Client.create();
        WebResource r;
        User user;
        
        // test d'insertion d'un utilisateur
        r = c.resource(URL);
        user = new User();
        user.setNom("Vianey");
        user.setPrenom("");
        user = r.type(MediaType.APPLICATION_JSON_TYPE)
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .put(User.class, user);
        
        System.out.println("User enregistré avec l'id : " + user.getId());
        
        // test de mise a jour de l'utilisateur
        r = c.resource(URL + "/" + user.getId());
        user = new User();
        user.setNom("Vianey");
        user.setPrenom("Antoine");
        user = r.type(MediaType.APPLICATION_JSON_TYPE)
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .post(User.class, user);
        
        System.out.println("User mise a jour : " + user.getPrenom() + " " + user.getNom());
        
        // test de recuperation de l'utilisateur
        r = c.resource(URL + "/" + user.getId());
        user = r.type(MediaType.APPLICATION_JSON_TYPE)
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .get(User.class);
        
        System.out.println("User récupéré : " + user.getPrenom() + " " + user.getNom());
        
        // test de suppression des utilisateurs
        r = c.resource(URL + "/" + user.getId());
        r.type(MediaType.APPLICATION_JSON_TYPE)
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .delete();
        
    }

}

Pour le test de récupération de la liste des utilisateurs, je n'ai malheureusement pas trouver de méthode élégante pour désérialiser directement le type List en JAX-RS comme cela est fait avec le type User... Une solution répandue consiste à retourner un wrapper encapsulant la liste des utilisateurs. Dans le prochain tutorial, nous aborderons nous verrons comment coder un client Android pour ces webservices avec l'utilisation d'une librairie nous permettant de désérialiser de manière élégante notre liste d'utilisateur. A la prochaine !
Fork me on GitHub