Статьи

Внедрение сервисов Kubernetes в управляемые CDI-компоненты с использованием Fabric8

В Куберне я больше всего люблю способ обнаружения услуг. Зачем?

Главным образом потому, что пользовательский код не должен иметь дело с регистрацией, поиском служб, а также потому, что нет никаких сетевых сюрпризов (если вы когда-либо пробовали подход на основе реестра, вы поймете, о чем я говорю) .

В этом посте будет рассказано, как вы можете использовать Fabric8  для внедрения   сервисов Kubernetes в Java с использованием CDI.

Kubernetes Услуги

Покрытие углубленного  Kubernetes  Услуга выходит за рамки этого поста, но я попытаюсь дать очень краткий обзор их.

В Kubernetes приложения упакованы в контейнеры Docker . Обычно хорошая идея разбить приложение на отдельные части, чтобы у вас было несколько   контейнеров Docker, которые, скорее всего, должны взаимодействовать друг с другом. Некоторые контейнеры могут быть собраны вместе, помещая их в один и тот же  модуль , в то время как другие могут быть удалены и им нужен способ общения друг с другом. Вот где  Сервисы  попадают в картину.

Контейнер может связываться с одним или несколькими портами, предоставляя одну или несколько «услуг» другим контейнерам. Например:

  • Сервер базы данных.
  • Брокер сообщений.
  • Служба отдыха.

Вопрос в том, « Как другие контейнеры знают, как получить доступ к этим службам? »

Таким образом,  Kubernetes  позволяет «ярлыку» каждый  Pod  и использовать эти метки для «выберите»  Боб ,  которые обеспечивают логическую услугу. Эти метки являются простыми ключами, парами значений.

Вот пример того, как мы можем «пометить» модуль, указав метку с именем ключа и значением mysql .

    {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

И вот пример того, как мы можем определить  Сервис,  который предоставляет порт mysql. Селектор службы использует пару ключ / значение, которую мы указали выше, чтобы определить, какие из них предоставляют услугу.

{
      "kind": "Service",
      "apiVersion": "v1beta3",
      "metadata": {
        "name": "mysql"
      },
      "spec": {
        "ports": [
          {
            "name": "mysql",
            "protocol": "TCP",
            "port": 3306,
            "targetPort": 3306
          }
        ],
        "selector": {
          "name": "mysql"
        }
     }
}

Информация  Сервиса  передавалась каждому контейнеру в качестве переменных среды  Kubernetes . Для каждого создаваемого  контейнера Kubernetes  будет следить за тем, чтобы соответствующие переменные среды передавались для ВСЕХ служб, видимых для контейнера.

Для службы mysql из приведенного выше примера переменными среды будут:

  • MYSQL_SERVICE_HOST
  • MYSQL_SERVICE_PORT

Fabric8  предоставляет расширение CDI, которое можно использовать для упрощения разработки   приложений Kubernetes путем внедрения   ресурсов Kubernetes .

Начало работы с расширением CDI Fabric8

Для использования расширения cdi первым шагом является добавление зависимости в проект.

<dependency>
    <groupId>io.fabric8</groupId>
    <artifactId>fabric8-cdi</artifactId>
    <version>2.1.11</version>
</dependency>

Следующий шаг — решить, какой сервис вы хотите внедрить в какое поле, а затем добавьте аннотацию @ServiceName к нему.

import javax.inject.Inject;
import io.fabric8.annotations.ServiceName;

public class MysqlExample {

    private static final DB = "mydb";
    private static final TCP_PROTO = "tcp";
    private static final JDBC_PROTO = "jdbc:mysql";
    
    private final Connection connection;
    
    public MysqlExample(@Inject @ServiceName("mysql") String serivceUrl) {
         Class.forName("com.mysql.jdbc.Driver");
         return DriverManager.getConnection(toJdbcUrl(serivceUrl));
    }
    
    private static String toJdbcUrl(String url) {
        return url.replaceFirst(TCP_PROTO, JDBC_PROTO) +
               "/" +
               DB;
    }
    
    //More stuff
}

В приведенном выше примере у нас есть класс, которому требуется соединение JDBC с базой данных mysql, доступной через  Kubernetes  Services .

Внедренный serivceUrl будет иметь вид: [tcp | udp]: // [хост]: [порт]. Который является идеальным URL, но это не правильный JDBC URL. Поэтому нам нужна утилита для преобразования этого. Это цель  toJdbcUrl.

Несмотря на то, что можно определить протокол при определении службы, можно указать только основные транспортные протоколы, такие как TCP или UDP, а не что-то вроде http, jdbc и т. Д.

Аннотация @Protocol

Необходимость найти и заменить значения «tcp» или «udp» протоколом приложения — это вонючий процесс, и он очень быстро стареет. Чтобы удалить этот шаблон,  Fabric8  предоставляет аннотацию @Protocol. Эта аннотация позволяет вам выбрать тот протокол приложения, который вы хотите в своем введенном URL службы. В предыдущем примере это «jdbc: mysql». Таким образом, код может выглядеть так:

import javax.inject.Inject;
import io.fabric8.annotations.Protocol;
import io.fabric8.annotations.ServiceName;

public class MysqlExampleWithProtocol {

    private static final DB = "mydb";
    private final Connection connection;
    
    public MysqlExampleWithProtocol(@Inject @Protocol("jdbc:mysql") @ServiceName("mysql") String serivceUrl) {
         Class.forName("com.mysql.jdbc.Driver");
         return DriverManager.getConnection(serivceUrl + "/" + DB);
    }
    
    //More stuff
}

Несомненно, это намного чище. Тем не менее, он не включает в себя информацию о фактической базе данных или каких-либо параметрах, которые обычно передаются как часть URL JDBC, поэтому здесь есть место для улучшения.

Можно было бы ожидать, что в том же духе будут доступны аннотации @Path или @Parameter, но обе эти вещи относятся к данным конфигурации и не подходят для жесткого кодирования в коде. Более того, расширение CDI Fabric8 не стремится стать структурой преобразования URL. Таким образом, вместо этого он поднимается на ступеньку выше, позволяя вам непосредственно создавать экземпляр клиента для доступа к любому заданному сервису и вставлять его в источник.

Создание клиентов для сервисов с использованием аннотации @Factory

В предыдущем примере мы увидели, как мы можем получить URL для сервиса и создать соединение JDBC с ним. Любой проект, которому требуется соединение JDBC, может скопировать этот фрагмент, и он будет отлично работать, если пользователь помнит, что ему нужно установить фактическое имя базы данных.

Разве не было бы замечательно, если бы вместо копирования и вставки этого фрагмента его можно было бы скомпоновать и повторно использовать? Здесь начинается заводская аннотация. Вы можете аннотировать с помощью @Factory любой метод, который принимает в качестве аргумента URL-адрес службы и возвращает объект, созданный с помощью URL-адреса (например, клиент для службы). Так что для предыдущего примера у нас может быть MysqlConnectionFactory:

import java.sql.Connection;
import io.fabric8.annotations.Factory;
import io.fabric8.annotations.ServiceName;

public class MysqlConnectionFactory {
     @Factory
     @ServiceName
     public Connection createConnection(@ServiceName @Protocol("jdbc:mysql") String url) {
          Class.forName("com.mysql.jdbc.Driver");
          return DriverManager.getConnection(serivceUrl + "/" + DB); 
     }
}

Затем вместо введения URL-адреса можно напрямую подключить соединение, как показано ниже.

import java.sql.Connection;
import javax.inject.Inject;
import io.fabric8.annotations.ServiceName;

public class MysqlExampleWithFactory {
    
    private Connection connection;
    
    public MysqlExampleWithProtocol(@Inject @ServiceName("mysql") Connection connection) {
         this.connection = connection;
    }
    //More stuff
}

Что здесь происходит?

При запуске приложения CDI расширение Fabric8 будет получать события обо всех аннотированных методах. Он будет отслеживать все доступные фабрики, поэтому для любой точки ввода, отличной от String, аннотированной @ServiceName, он создаст источник, который под капотом использует соответствующий @Factory.

В приведенном выше примере сначала MysqlConnectionFactory будет зарегистрирован, и когда экземпляр Connection с квалификатором @ServiceName будет обнаружен, будет создан источник, делегирующий MysqlConnectionFactory (все квалификаторы будут соблюдены) .

Это потрясающе, но также и упрощенно . Зачем?

Потому что редко такая фабрика требует только ссылки на сервис. В большинстве случаев требуются другие параметры конфигурации, такие как:

  • Информация аутентификации
  • Тайм-ауты соединения
  • Больше ….

Использование @Factory с @Configuration

В следующем разделе мы увидим фабрики, которые используют данные конфигурации. Я собираюсь использовать пример mysql jdbc и добавить поддержку для указания настраиваемых учетных данных. Но перед этим я собираюсь задать риторический вопрос?

«Как вы можете настроить приложение в контейнере?» 

Кратчайший возможный ответ — «Использование переменных среды».

Итак, в этом примере я предполагаю, что учетные данные передаются в контейнер, который должен получить доступ к mysql, используя следующие переменные среды:

  • MYSQL_USERNAME
  • MYSQL_PASSWORD

Теперь нам нужно посмотреть, как наш @Factory может их использовать.

Если раньше вы хотели использовать переменные окружения внутри CDI, скорее всего, вы использовали Apache DeltaSpike . Этот проект, среди прочего, предоставляет аннотацию @ConfigProperty, которая позволяет вам вводить переменную среды в bean-компонент CDI (на самом деле он делает больше, чем это) .

import org.apache.deltaspike.core.api.config.ConfigProperty;
import javax.inject.Inject;

public class MysqlConfiguration {
      @Inject
      @ConfigProperty(name = "USERNAME", defaultValue = "admin")
      private String username;
      
      @Inject
      @ConfigProperty(name = "PASSWORD", defaultValue = "admin")
      private String password;
      
      @Inject
      @ConfigProperty(name = "DATABASE_NAME", defaultValue = "mydb")
      private String databaseName;
      
      public String getUsername() {
            return username;
      }
      
      public String getPassword() {
            return password;
      }
      public String getDatabaseName() {
            return databaseName;
      }
            
}

Этот bean-компонент может быть объединен с методом @Factory, чтобы мы могли передать конфигурацию самой фабрике.

Но что, если бы у нас было несколько  серверов баз данных, настроенных с другим набором учетных данных, или несколько баз данных? В этом случае мы могли бы использовать имя службы в качестве префикса и позволить  Fabric8  выяснить, какие переменные среды следует искать для каждого экземпляра @Configuration.

import javax.inject.Inject;
import io.fabric8.annotations.ServiceName;
import io.fabric8.annotations.Factory;
import io.fabric8.annotations.Protocol;
import io.fabric8.annotations.Configuration;

public class MysqlExampleWithFactoryAndConfiguration {
     
     @Factory
     @ServiceName
     public Connection createConnection(@ServiceName @Protocol("jdbc:mysql") String url, @Configuration MysqlConfiguration conf) {
          Class.forName("com.mysql.jdbc.Driver");
          return DriverManager.getConnection(serivceUrl + "/" + conf.getDatabaseName(), 
                                                           conf.getUsername(), 
                                                           conf.getPassword()); 
     }
}

Теперь у нас есть многократно используемый компонент, который можно использовать с любой базой данных mysql, работающей внутри kubernetes, и он полностью настраивается.

В  расширении Fabric8 CDI есть дополнительные функции  , но поскольку этот пост уже слишком длинный, они будут рассмотрены в следующих публикациях.