官术网_书友最值得收藏!

Setting up a development environment for microservices

One of the greatest benefits of using Docker and its container ecosystem is that you don't need to install anything else on your machine. For example, if you need a MySQL database, you don't need to install anything on your local dev; it is easier to spin a container with the version you want and start using it.

This way of developing is more flexible, therefore we will be working with Docker containers throughout the whole book. In this section, we will learn how to build a basic Docker environment; it will be our foundation and we will be improving and adapting this base to each of our microservices in the subsequent chapters.

To simplify the folder structure of our project, we will have some root folders on our development machine:

  • Docker: This folder will contain all the Docker environment
  • Source: This folder will have the source of each of our microservices

Note that this structure is flexible and can be changed and adapted to your specific requirements without any problems.

All the required files are available on our GitHub repository, at https://github.com/php-microservices/docker, on the master branch with the chapter-02 tag.

Let's dig deeper into the Docker setup. Open your docker folder and create a file called docker-compose.yml with the following content:

    version: '2'
    services:

These two lines indicate that we are using the latest syntax for Docker compose and they define a list of services that we will be spinning every time we do a docker-compose up. All our services will be added after the services declaration.

Autodiscovery service

Autodiscovery is a mechanism in which we don't specify the endpoints of each of our microservices. Each one of our services use a shared registry in which they say that they are available. When a microservice needs to know the location of another microservice, it can consult our autodiscovery registry to know the required endpoint.

For our application, we will be using an autodiscovery mechanism to ensure that our microservices can be scaled easily and if a node is not healthy, we stop sending requests to it. Our choice for this purpose is to use Consul (by HashiCorp), a very small application that we can add to our project. The main role for our Consul container is to keep everything in order, keeping a list of the available and healthy services.

Let's start the project by opening your docker-compose.yml file with your favorite IDE/editor and adding the next piece of code just after the services: line:

    version: '2'
    services:
        autodiscovery:
            build: ./autodiscovery/
            mem_limit: 128m
            expose:
                - 53
                - 8300
                - 8301
                - 8302
                - 8400
                - 8500
            ports:
                - 8500:8500
            dns:
                - 127.0.0.1

In a Docker compose file, the syntax is very easy to understand and always follows the same flow. The first line defines a container type (it is like a class name for devs); in our case it is autodiscovery, and inside this container type we can specify several options to adapt the container to our requirements.

With build: ./autodiscovery/, we are telling Docker where it can find a Dockerfile that describes what we want in our container in detail.

The mem_limit: 128m sentence will limit the memory consumption of any container of the autodiscovery type to not more than 128 Mb. Note that this instruction is optional.

Each container needs different ports open and, by default, when you spin a container, none of them are open. For this reason, you need to specify which ports you want open for each container. For example, a container with a web server will need the port 80 open but for a container that runs MySQL, the required port may be 3306. In our case, we are opening the ports 53, 8300, 8301, 8302, 8400, and 8500 for each one of our autodiscovery containers.

If you try to reach the container on one of the opened ports, it will not work. The container ecosystem resides in a separate network and you can only access it if you create a bridge between your environment and the Docker network. Our autodiscovery container runs Consul and it has a nice web UI on port 8500. We want to be able to use this UI; so, when we use ports, we are mapping our local 8500 port to the container 8500 port.

Now, it's time to create a new folder called autodiscovery in the same path of your docker-compose.yml file. Inside this new folder, place a file called Dockerfile with the following line:

    FROM consul:v0.7.0 

This small sentence inside the Dockerfile indicates that we are using a Docker consul image with tag v0.7.0. This image will be fetched from the official Docker hub, a repository for container images.

At this point, doing a $ docker-compose up will spin up a Consul machine, give it a try. Since we didn't specify the -d option, the Docker engine will output all the logs to your terminal. You can stop your container with a simple CTRL+C. When you add the -d option, the Docker compose runs as a daemon and returns the prompt; you can do a $ docker-compose stop to stop the containers.

Microservice base core - NGINX and PHP-FPM

PHP-FPM is an alternative to the old way of executing PHP in our web server. The main benefit of using PHP-FPM is its small memory footprint and the high performance under hight loads. The best web server you can find nowadays to run your PHP-FPM is NGINX, a very light web server and reverse proxy used in the most important projects.

Since our application will be using an autodiscovery pattern, we need an easy way of dealing with the service registering, deregistering, and health check. One of the simplest and fastest applications you can use is ContainerPilot, a small micro-orchestration application created by Joyent that works with your favorite container scheduler, in our case Docker compose. This small app is being executed as PID 1 and forks the application we want to run inside the container.

We will be working with ContainerPilot because it relieves the developer of dealing with the autodiscovery, so we need to have the latest version on each container we will be using.

Let's start defining our base php-fpm container. Open the docker-compose.yml and add a new service for the php-fpm:

    microservice_base_fpm: 
      build: ./microservices/base/php-fpm/ 
    links: 
      - autodiscovery 
    expose: 
      - 9000 
    environment: 
      - BACKEND=microservice_base_nginx 
      - CONSUL=autodiscovery 

In the preceding code, we are defining a new service and one interesting attribute is links. This attribute defines which other containers our service can see or connect. In our example, we want to link this type of container to any autodiscovery container. Without this explicit definition, our fpm container won't see the autodiscovery service.

Now, create the microservices/base/php-fpm/Dockerfile file on your IDE/editor with the following content:

    FROM php:7-fpm 
 
    RUN apt-get update && apt-get -y install \ 
      git g++ libcurl4-gnutls-dev libicu-dev libmcrypt-dev 
      libpq-dev libxml2-dev 
      unzip zlib1g-dev \ 
      && git clone -b php7 
      https://github.com/phpredis/phpredis.git 
      /usr/src/php/ext/redis \
      && docker-php-ext-install curl intl json mbstring 
      mcrypt pdo pdo_pgsql 
      redis xml \ 
      && apt-get autoremove && apt-get autoclean \ 
      && rm -rf /var/lib/apt/lists/* 
 
    RUN echo 'date.timezone="Europe/Madrid"' >>  
      /usr/local/etc/php/conf.d/date.ini 
    RUN echo 'session.save_path = "/tmp"' >>  
      /usr/local/etc/php/conf.d/session.ini 
 
    ENV CONSUL_TEMPLATE_VERSION 0.16.0 
    ENV CONSUL_TEMPLATE_SHA1  
    064b0b492bb7ca3663811d297436a4bbf3226de706d2b76adade7021cd22e156 
 
    RUN curl --retry 7 -Lso /tmp/consul-template.zip \ 
      "https://releases.hashicorp.com/
      consul-template/${CONSUL_TEMPLATE_VERSION}/
      consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip" \ 
    && echo "${CONSUL_TEMPLATE_SHA1}  /tmp/consul-template.zip" 
    | sha256sum -c \ 
    && unzip /tmp/consul-template.zip -d /usr/local/bin \ 
    && rm /tmp/consul-template.zip 
 
    ENV CONTAINERPILOT_VERSION 2.4.3 
    ENV CONTAINERPILOT_SHA1 2c469a0e79a7ac801f1c032c2515dd0278134790 
    ENV CONTAINERPILOT file:///etc/containerpilot.json 
 
    RUN curl --retry 7 -Lso /tmp/containerpilot.tar.gz \ 
      "https://github.com/joyent/containerpilot/releases/download/
      ${CONTAINERPILOT_VERSION}/containerpilot-
      ${CONTAINERPILOT_VERSION}.tar.gz" 
      \ 
      && echo "${CONTAINERPILOT_SHA1}  /tmp/containerpilot.tar.gz" 
      | sha1sum -c \ 
      && tar zxf /tmp/containerpilot.tar.gz -C /usr/local/bin \ 
      && rm /tmp/containerpilot.tar.gz 
 
    COPY config/ /etc 
    COPY scripts/ /usr/local/bin 
 
    RUN chmod +x /usr/local/bin/reload.sh 
 
    CMD [ "/usr/local/bin/containerpilot", "php-fpm", "--nodaemonize"] 

What we have done on this file is tell Docker how it needs to create our php-fpm container. The first line declares the official version we want to use as a foundation for our container, in this case php7 fpm. Once the image is downloaded, the first RUN line will add all the extra PHP packages we will be using.

The two RUN sentences will add bespoke PHP configurations; feel free to adapt these lines to your requirements.

Once all the PHP tasks are done, it is time to install a small application on the container that will help us to deal with templates--consul-template. This application is used to build configuration templates on the fly using the information we have stored on our Consul service.

As we said before, we are using ContainerPilot. So, after the consul-template installation, we are telling Docker how to install this application.

At this point, Docker finishes installing all the required packages and copies some configuration and shell scripts needed by ContainerPilot

The last line starts ContainerPilot as PID 1 and forks php-fpm.

Now, let's explain the configuration file required by ContainerPilot. Open your IDE/editor and create the microservices/base/php-fpm/config/containerpilot.json file with the following content:

    { 
      "consul": "{{ if .CONSUL_AGENT }}localhost{{ else }}{{ .CONSUL }}
      {{ end }}:8500", 
      "preStart": "/usr/local/bin/reload.sh preStart", 
      "logging": {"level": "DEBUG"}, 
      "services": [ 
        { 
          "name": "microservice_base_fpm", 
          "port": 80, 
          "health": "/usr/local/sbin/php-fpm -t", 
          "poll": 10, 
          "ttl": 25, 
          "interfaces": ["eth1", "eth0"] 
        } 
      ], 
      "backends": [ 
        { 
          "name": "{{ .BACKEND }}", 
          "poll": 7, 
          "onChange": "/usr/local/bin/reload.sh" 
        } 
      ], 
      "coprocesses": [{{ if .CONSUL_AGENT }} 
        { 
          "command": ["/usr/local/bin/consul", "agent", 
            "-data-dir=/var/lib/consul", 
            "-config-dir=/etc/consul", 
            "-rejoin", 
            "-retry-join", "{{ .CONSUL }}", 
            "-retry-max", "10", 
          "-retry-interval", "10s"], 
          "restarts": "unlimited" 
        }
      {{ end }}] 
    } 

This JSON configuration file is very easy to understand. First, it defines where we can find our Consul container and which command we want to run on the ContainerPilot preStart event. In services, you can define all the services you want to declare that the current container is running. On the backends, you can define all the services you are listening for changes. In our case, we are listening for changes to services called microservice_base_nginx (the BACKEND variable is defined on the docker-compose.yml). If something changes on Consul on these services, we will execute the onChange command in the container.

For a more information about ContainerPilot, you can visit the official page, that is, https://www.joyent.com/containerpilot.

It's time to create the microservices/base/php-fpm/scripts/reload.sh file with the following content:

    #!/bin/bash 
 
    SERVICE_NAME=${SERVICE_NAME:-php-fpm} 
    CONSUL=${CONSUL:-consul} 
    preStart() { 
      echo "php-fpm preStart" 
    } 
 
    onChange() { 
      echo "php-fpm onChange" 
    } 
 
    help() { 
      echo "Usage: ./reload.sh preStart  
      => first-run configuration for php-fpm" 
      echo "      ./reload.sh onChange  
      => [default] update php-fom config on 
      upstream changes" 
    } 
 
    until 
      cmd=$1 
      if [ -z "$cmd" ]; then 
             onChange 
      fi 
      shift 1 
      $cmd "$@" 
      [ "$?" -ne 127 ] 
    do 
      onChange 
      exit 
    done 

Here, we created a dummy script, but it is up to you to adapt it to your requirements. For example, it can be changed to run execute consul-template and rebuild the NGINX configuration once ContainerPilot fires the script. We will be explaining a more complex script later.

We have our base php-fpm container ready, but our basic environment can't be complete without a web server. We will be using NGINX, a very light and powerful reverse proxy and web server.

The way we will build our NGINX server is very similar to the php-fpm, so we will only explain the differences.

Tip

Remember that all the files are available in our GitHub repository.

We will add a new service definition for NGINX to the docker-compose.yml file and link it to our autodiscovery service and also to our php-fpm:

    microservice_base_nginx: 
      build: ./microservices/base/nginx/ 
      links: 
        - autodiscovery 
        - microservice_base_fpm 
      environment: 
        - BACKEND=microservice_base_fpm 
        - CONSUL=autodiscovery 
      ports: 
        - 8080:80 

In our microservices/base/nginx/config/containerpilot.json, we now have a new option telemetry. This config setting allows us to specify a remote telemetry service used to collect stats from our service. Having this kind of service included in our environment allows us to see how our containers are performing:

    "telemetry": { 
      "port": 9090, 
      "sensors": [ 
        { 
          "name": "nginx_connections_unhandled_total", 
          "help": "Number of accepted connnections that were not handled", 
          "type": "gauge", 
          "poll": 5, 
          "check": ["/usr/local/bin/sensor.sh", "unhandled"] 
        }, 
        { 
          "name": "nginx_connections_load", 
          "help": "Ratio of active connections (less waiting) to 
          the maximum  
          worker connections", 
          "type": "gauge", 
          "poll": 5, 
          "check": ["/usr/local/bin/sensor.sh", "connections_load"] 
        }
      ]
    } 

As you can see, we use a bespoke bash script to obtain the container stats, and the content of our microservices/base/nginx/scripts/sensor.sh script is as follows:

    #!/bin/bash 
    set -e 
 
    help() { 
      echo 'Make requests to the Nginx stub_status endpoint and 
      pull out metrics' 
      echo 'for the telemetry service. Refer to the Nginx docs 
      for details:' 
      echo 'http://nginx.org/en/docs/http/ngx_http_stub_status_module.html' 
    } 
 
    unhandled() { 
      local accepts=$(curl -s --fail localhost/nginx-health | awk 'FNR == 3 
      {print $1}') 
      local handled=$(curl -s --fail localhost/nginx-health | awk 'FNR == 3 
      {print $2}') 
      echo $(expr ${accepts} - ${handled}) 
    } 
 
    connections_load() { 
      local scraped=$(curl -s --fail localhost/nginx-health) 
      local active=$(echo ${scraped} 
      | awk '/Active connections/{print $3}') 
      local waiting=$(echo ${scraped} | awk '/Reading/{print $6}') 
      local workers=$(echo $(cat /etc/nginx/nginx.conf | perl -n -e'/
      worker_connections *(\d+)/ && print $1') ) 
      echo $(echo "scale=4; (${active} - ${waiting}) / ${workers}" | bc) 
    } 
 
    cmd=$1 
    if [ ! -z "$cmd" ]; then 
      shift 1 
      $cmd "$@" 
      exit 
    fi 
 
    help 

This bash script gets some nginx stats that we will be sending to our telemetry server with ContainerPilot.

Our microservices/base/nginx/scripts/reload.sh is a little more complex than the one we created before for php-fpm:

    #!/bin/bash 
   
    SERVICE_NAME=${SERVICE_NAME:-nginx} 
    CONSUL=${CONSUL:-consul} 
 
    preStart() { 
      consul-template \ 
            -once \ 
            -dedup \ 
            -consul ${CONSUL}:8500 \ 
            -template "/etc/nginx/nginx.conf.ctmpl:/etc/nginx/nginx.conf" 
    } 
 
    onChange() { 
      consul-template \ 
            -once \ 
            -dedup \ 
            -consul ${CONSUL}:8500 \ 
            -template "/etc/nginx/nginx.conf.ctmpl:/etc/nginx/
            nginx.conf:nginx -s reload" 
    } 
 
    help() { 
      echo "Usage: ./reload.sh preStart  
      => first-run configuration for Nginx" 
      echo "      ./reload.sh onChange  => [default] update Nginx config on 
      upstream changes" 
    } 
 
    until 
      cmd=$1 
      if [ -z "$cmd" ]; then 
             onChange 
      fi 
      shift 1 
      $cmd "$@" 
      [ "$?" -ne 127 ] 
    do 
       onChange 
       exit 
    done 

As you can see, we use consul-template to rebuild our NGINX config on the startup or when ContainerPilot detects a change in the list of backend services we will be monitoring. This behavior allows us to stop sending requests to unhealthy nodes.

At this point, we have our base environment ready and we are ready to test it with a simple $ docker-compose up. We will be using all these pieces to create bigger and more complex services. In the upcoming chapters, we will be adding the telemetry service or a data storage among others.

主站蜘蛛池模板: 天等县| 永嘉县| 开平市| 天峻县| 云阳县| 中宁县| 德兴市| 大竹县| 贞丰县| 孙吴县| 衡阳县| 电白县| 甘孜县| 花垣县| 驻马店市| 镇原县| 宁河县| 卓资县| 乐安县| 偃师市| 嘉禾县| 梧州市| 梧州市| 松滋市| 南汇区| 布尔津县| 开平市| 南平市| 车致| 嵊州市| 靖西县| 江油市| 汶上县| 周至县| 阳西县| 盘山县| 香格里拉县| 城步| 宁远县| 井陉县| 扬州市|