Присоединение AWS documentDB к приложению Spring Boot

Недавно я попытался использовать новую службу AWS DocumentDB в качестве своей БД в приложении Spring.

Кластер был создан в том же VPC, что и EKS, на котором я развертываю свое приложение. Группы безопасности разрешают соединения между всеми узлами в VPC.

AWS предоставляет для моего кластера БД такой mongo URI:

mongodb://<my-user>:<insertYourPassword>@<my-cluster-endpoint>:27017/?ssl_ca_certs=rds-combined-ca-bundle.pem&replicaSet=rs0

Мой вопрос:

Как заставить мой код Spring работать с таким подключением?

Я попытался добавить в свой application.properties файл следующее:

spring.data.mongodb.uri=mongodb://<my-user>:<insertYourPassword>@<my-cluster-endpoint>:27017/admin?ssl_ca_certs=rds-combined-ca-bundle.pem&replicaSet=rs00
spring.data.mongodb.database=admin
server.ssl.key-store=classpath:rds-combined-ca-bundle.pem

И поместив файл PEM в /src/main/resources

Однако код по-прежнему не может подключиться к кластеру БД.

Я получаю это сообщение как сообщение об ошибке: No server chosen by com.mongodb.client.internal.MongoClientDelegate

За ним следует Exception in monitor thread while connecting to server ...

И, наконец, исключение тайм-аута: com.mongodb.MongoSocketReadTimeoutException: Timeout while receiving message

Это похоже на проблему группы безопасности, но у меня нет проблем с подключением к оболочке mongo из того же EC2, на котором запущено приложение Spring Pod.

Любые идеи?


person TheFooBarWay    schedule 17.01.2019    source источник
comment
какое исключение, поделитесь журналами.   -  person Barath    schedule 17.01.2019
comment
используйте это руководство braytonstafford.com/ 27.03.2018 / для извлечения jks из пакета aws ca.   -  person Barath    schedule 17.01.2019
comment
ты нашел решение? Я с той же проблемой   -  person Emre    schedule 29.01.2019
comment
К сожалению, на данный момент мне пришлось отказаться от этого побочного проекта. Будет обновляться, если что-то изменится, и решение будет найдено. Приношу свои извинения всем, кто вмешался, как только позволит время, я перейду к предложениям.   -  person TheFooBarWay    schedule 30.01.2019
comment
@TheFooBarWay есть ли у вас какое-нибудь решение для этого. Мой вариант использования такой же: приложение с весенней загрузкой, работающее в AWS EKS и имеющее доступ к базе данных AWS Document DB. Кластер EKS и база данных документов находятся в одном VPC. У меня такая же ошибка, как и у вас.   -  person user5921551    schedule 01.07.2021


Ответы (5)


Как упоминалось в документации,

По умолчанию вы получаете доступ к ресурсам Amazon DocumentDB (с совместимостью с MongoDB) из инстанса Amazon EC2 в том же Amazon VPC, что и ресурсы Amazon DocumentDB. Однако предположим, что ваш вариант использования требует, чтобы вы или ваше приложение обращались к вашим ресурсам Amazon DocumentDB извне Amazon VPC кластера. В этом случае вы можете использовать SSH-туннелирование (также известное как «перенаправление портов») для доступа к своим ресурсам Amazon DocumentDB.

Подключение извне VPC

Кластер Amazon DocumentDB должен работать в виртуальном частном облаке (VPC) по умолчанию. Для взаимодействия с кластером Amazon DocumentDB необходимо запустить инстанс Amazon Elastic Compute Cloud (Amazon EC2) в VPC по умолчанию в том же регионе AWS, где вы создали кластер Amazon DocumentDB.

Следуйте инструкциям по подключению к кластеру Кластер AWS DocumentDB

Ссылка на GitHub: spring-boot-aws-documentdb

Обновление:

Чтобы подключиться через SSL, используйте приведенную ниже логику, установив SSL_CERTIFICATE, указывающий на промежуточный сертификат для региона aws.

Его можно загрузить с сертификатов SSL и скопировать его. в базовый каталог. В качестве альтернативы вы можете указать абсолютный путь к переменной SSL_CERTIFICATE.

     private static final String SSL_CERTIFICATE = "rds-ca-2015-us-east-1.pem";
     private static final String KEY_STORE_TYPE = "JKS";
     private static final String KEY_STORE_PROVIDER = "SUN";
     private static final String KEY_STORE_FILE_PREFIX = "sys-connect-via-ssl-test-cacerts";
     private static final String KEY_STORE_FILE_SUFFIX = ".jks";
     private static final String DEFAULT_KEY_STORE_PASSWORD = "changeit";

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


    protected static class SSLContextHelper{
    /**
     * This method sets the SSL properties which specify the key store file, its type and password:
     * @throws Exception
     */
    private static void setSslProperties()  {

        try {
            System.setProperty("javax.net.ssl.trustStore", createKeyStoreFile());
        } catch (Exception e) {

            e.printStackTrace();
        }
        System.setProperty("javax.net.ssl.trustStoreType", KEY_STORE_TYPE);
        System.setProperty("javax.net.ssl.trustStorePassword", DEFAULT_KEY_STORE_PASSWORD);
    }


    private static String createKeyStoreFile() throws Exception {
        return createKeyStoreFile(createCertificate()).getPath();
    }

    /**
     *  This method generates the SSL certificate
     * @return
     * @throws Exception
     */
    private static X509Certificate createCertificate() throws Exception {
        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        URL url = new File(SSL_CERTIFICATE).toURI().toURL();
        if (url == null) {
            throw new Exception();
        }
        try (InputStream certInputStream = url.openStream()) {
            return (X509Certificate) certFactory.generateCertificate(certInputStream);
        }
    }

    /**
     * This method creates the Key Store File
     * @param rootX509Certificate - the SSL certificate to be stored in the KeyStore
     * @return
     * @throws Exception
     */
    private static File createKeyStoreFile(X509Certificate rootX509Certificate) throws Exception {
        File keyStoreFile = File.createTempFile(KEY_STORE_FILE_PREFIX, KEY_STORE_FILE_SUFFIX);
        try (FileOutputStream fos = new FileOutputStream(keyStoreFile.getPath())) {
            KeyStore ks = KeyStore.getInstance(KEY_STORE_TYPE, KEY_STORE_PROVIDER);
            ks.load(null);
            ks.setCertificateEntry("rootCaCertificate", rootX509Certificate);
            ks.store(fos, DEFAULT_KEY_STORE_PASSWORD.toCharArray());
        }
        return keyStoreFile;
    }


    }

выход подключения:

019-01-17 13:33:22.316  INFO 3598 --- [onaws.com:27017] org.mongodb.driver.cluster               : Canonical address mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017 does not match server address.  Removing mongodb.cluster-cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017 from client view of cluster
2019-01-17 13:33:22.401  INFO 3598 --- [onaws.com:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2}] to mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017
2019-01-17 13:33:22.403  INFO 3598 --- [onaws.com:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017, type=REPLICA_SET_PRIMARY, state=CONNECTED, ok=true, version=ServerVersion{versionList=[3, 6, 0]}, minWireVersion=0, maxWireVersion=6, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=null, roundTripTimeNanos=2132149, setName='rs0', canonicalAddress=mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017, hosts=[mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017], passives=[], arbiters=[], primary='mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017', tagSet=TagSet{[]}, electionId=7fffffff0000000000000001, setVersion=null, lastWriteDate=Thu Jan 17 13:33:21 UTC 2019, lastUpdateTimeNanos=516261208876}
2019-01-17 13:33:22.406  INFO 3598 --- [onaws.com:27017] org.mongodb.driver.cluster               : Discovered replica set primary mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017
2019-01-17 13:33:22.595  INFO 3598 --- [           main] com.barath.app.CustomerService           : Saving the customer with customer details com.barath.app.Customer@6c130c45
2019-01-17 13:33:22.912  INFO 3598 --- [           main] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:3}] to mongodb.cktoiipu3bbd.us-east-1.docdb.amazonaws.com:27017
2019-01-17 13:33:23.936  INFO 3598 --- [           main] pertySourcedRequestMappingHandlerMapping : Mapped URL path [/v2/api-docs] onto method [public org.springframework.http.ResponseEntity<springfox.documentation.spring.web.json.Json> springfox.documentation.swagger2.web.Swagger2Controller.getDocumentation(java.lang.String,javax.servlet.http.HttpServletRequest)]
person Barath    schedule 17.01.2019
comment
Он работал, но все остальные вызовы https не работают даже при вызове aws sdk. Ошибка построения пути PKIX: sun.security.provider.certpath.SunCertPathBuilderException: невозможно найти действительный путь сертификации для запрошенного. Любая идея? - person mmr25; 23.12.2020
comment
@ mmr25 у вас есть проблемы с путём cer, перейдите по этой ссылке stackoverflow.com/questions/21076179/ - person GvSharma; 21.06.2021

Я могу подтвердить, что решение, предоставленное @Barath, позволяет защитить TLS-соединение AWS DocumentDB внутри самого приложения Java. Это намного более чистый подход по сравнению с подходом, описанным AWS на https://docs.aws.amazon.com/documentdb/latest/developerguide/connect_programmatically.html, который требует, чтобы вы запускали сценарий на своем сервере, что является более сложным и трудным для автоматического развертывания и т. д.

Для дальнейшей настройки самого соединения в приложении Spring я использовал следующий класс @Configuration, который позволяет подключаться к локальной MongoDB для тестирования во время разработки, и один AWS, развернутый с настройками из файла свойств.

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
    
@Configuration
@EnableMongoRepositories(basePackages = "YOUR.PACKAGE.WITH.repository")
public class MongoDbConfig extends AbstractMongoClientConfiguration {
    
    @Value("${spring.profiles.active}")
    private String activeProfile;
    
    @Value("${mongodb.host:localhost}")
    private String dbUri;
    @Value("${mongodb.port:27017}")
    private int dbPort;
    @Value("${mongodb.database.name:YOUR_DOCUMENTDB_NAME}")
    private String dbName;
    @Value("${mongodb.username:}")
    private String dbUser;
    @Value("${mongodb.password:}")
    private String dbPassword;
    
    @Override
    public String getDatabaseName() {
        return dbName;
    }
    
    @Override
    public MongoClient mongoClient() {
        ConnectionString connectionString = new ConnectionString(getConnectionString());
        MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
               .applyConnectionString(connectionString)
               .build();
        return MongoClients.create(mongoClientSettings);
    }
    
    private String getConnectionString() {
        if (activeProfile.contains("local")) {
            return String.format("mongodb://%s:%s/%s", dbUri, dbPort, dbName);
        }
        return String.format("mongodb://%s:%s@%s:%s/%s?ssl=true&replicaSet=rs0&readpreference=secondaryPreferred&retrywrites=false",
                dbUser, dbPassword, dbUri, dbPort, dbName);
    }
}
person Frank    schedule 08.03.2021

На самом деле я столкнулся с той же проблемой, что и вы, но теперь AWS использует rds-combined-ca-bundle.pem, который объединяет множество сертификатов в один.

Если вы не хотите создавать хранилище доверенных сертификатов, используя устаревшую документацию, вы можете сделать это самостоятельно и добавить rds-combined-ca-bundle.pem в свое приложение, генерирующее хранилище ключей во время выполнения.

Мне удалось заставить это работать с этим образцом кода. Это было протестировано с spring:2.4, mongo-driver: 4.1.1 и documentDB с использованием mongo 4.0 совместимости.

val endOfCertificateDelimiter = "-----END CERTIFICATE-----"

// rds-combined-ca-bundle.pem contains more than one certificate. We need to add them all to the trust-store independantly. 

val allCertificates = ClassPathResource("certificates/rds-combined-ca-bundle.pem").file.readText()
    .split(endOfCertificateDelimiter)
    .filter { it.isNotBlank() }
    .map { it + endOfCertificateDelimiter }

val certificateFactory = CertificateFactory.getInstance("X.509")
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null) // This allows us to use an in-memory key-store

allCertificates.forEachIndexed { index, certificate ->
    val caCert = certificateFactory.generateCertificate(certificate.byteInputStream()) as X509Certificate
    keyStore.setCertificateEntry("AWS-certificate-$index", caCert)
}

val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)

val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustManagerFactory.trustManagers, null)

builder.applyToSslSettings {
    it.enabled(true)
        .context(sslContext)
}
person Sunny Pelletier    schedule 22.03.2021

Ответ, предоставленный @Sunny Pelletier, работал у меня с мэшапом @ Frank в нашей настройке Java.

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

Сначала я начал с простого POJO конфигурации, чтобы настроить свои свойства вне парадигмы spring.data.mongo.*. Вам не обязательно этого делать, и вы можете просто позволить Spring обработать это, как обычно, для создания файла MongoClient.

Мой локальный разработчик по умолчанию application.yml и соответствующий класс конфигурации.

mongo:
  user: mongo
  password: mongo
  host: localhost
  port: 27017
  database: my-service

@Data
@Configuration
@ConfigurationProperties(prefix = "mongo")
public class MongoConnectConfig {

    private int port;

    private String host;

    private String user;

    private String database;

    private String password;

}

Затем я создал два AbstractMongoClientConfiguration дочерних класса; один для местного и один для неместного. Ключевым моментом здесь является то, что я не создавал свой собственный MongoClient. Причина в том, что мне нужны все хорошие вещи для инициализации Spring Boot, которые вы получаете с фреймворком. Например, авторегистрация всех конвертеров и тому подобное.

Вместо этого я использовал крючок настройки, предоставленный AbstractMongoClientConfiguration.configureClientSettings(MongoClientSettings.Builder builder), чтобы затем объединить пользовательские настройки, такие как часть .pem.

Другая часть заключается в том, что я использовал профили для включения / отключения конфигураций, чтобы сделать их удобными для местных разработчиков; мы не используем никакие профили, кроме default, для локальной разработки, поэтому проще приступить к настройке, не зная с самого начала.

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@Slf4j
@Configuration
@RequiredArgsConstructor
@Profile({"!dev && !qa && !prod"})
@EnableMongoRepositories(basePackages = "co.my.data.repositories")
public class LocalDevMongoConfig extends AbstractMongoClientConfiguration {
    
    private final MongoConnectConfig config;
    
    @Override
    public String getDatabaseName() {
        return config.getDatabase();
    }
    
    @Override
    protected void configureClientSettings(MongoClientSettings.Builder builder) {
        log.info("Applying Local Dev MongoDB Configuration");
        builder.applyConnectionString(new ConnectionString(getConnectionString()));
    }

    //mongodb://${mongo.user}:${mongo.password}@${mongo.host}:${mongo.port}/${mongo.database}?authSource=admin
    private String getConnectionString() {
        return String.format("mongodb://%s:%s@%s:%s/%s?authSource=admin",
                config.getUser(),
                config.getPassword(),
                config.getHost(),
                config.getPort(),
                config.getDatabase()
        );
    }
}


import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.stream.Collectors;

@Slf4j
@Configuration
@RequiredArgsConstructor
@Profile({"dev || qa || prod"})
@EnableMongoRepositories(basePackages = "co.my.data.repositories")
public class DocumentDbMongoConfig extends AbstractMongoClientConfiguration {

    private final MongoConnectConfig config;

    @Override
    public String getDatabaseName() {
        return config.getDatabase();
    }

    @SneakyThrows
    @Override
    protected void configureClientSettings(MongoClientSettings.Builder builder) {
        log.info("Applying AWS DocumentDB Configuration");
        builder.applyConnectionString(new ConnectionString(getConnectionString()));
        var endOfCertificateDelimiter = "-----END CERTIFICATE-----";
        File resource = new ClassPathResource("certs/rds-combined-ca-bundle.pem").getFile();
        String pemContents = new String(Files.readAllBytes(resource.toPath()));
        var allCertificates = Arrays.stream(pemContents
                .split(endOfCertificateDelimiter))
                .filter(line -> !line.isBlank())
                .map(line -> line + endOfCertificateDelimiter)
                .collect(Collectors.toUnmodifiableList());


        var certificateFactory = CertificateFactory.getInstance("X.509");
        var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        // This allows us to use an in-memory key-store
        keyStore.load(null);

        for (int i = 0; i < allCertificates.size(); i++) {
            var certString = allCertificates.get(i);
            var caCert = certificateFactory.generateCertificate(new ByteArrayInputStream(certString.getBytes()));
            keyStore.setCertificateEntry(String.format("AWS-certificate-%s", i), caCert);
        }

        var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        var sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

        builder.applyToSslSettings(ssl -> {
            ssl.enabled(true).context(sslContext);
        });
    }

    /**
     * Partly based on the AWS Console "Connectivity & security " section in the DocumentDB Cluster View.
     *   Since we register the pem above, we don't need to add the ssl & sslCAFile piece
     *   mongodb://${user}:${password}@${host}:${port}/?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false
     */
    private String getConnectionString() {
        return String.format("mongodb://%s:%s@%s:%s/%s?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false",
                config.getUser(),
                config.getPassword(),
                config.getHost(),
                config.getPort(),
                config.getDatabase()
        );
    }
}

Наконец, мы помещаем rds-combined-ca-bundle.pem в папку src/main/resources/certs/.

Примечания на стороне:

  • Опять же, я считаю, что вы можете обойтись без использования свойств spring.data* по умолчанию, и ваш MongoClient должен был их использовать.
  • Не обращайте внимания на @SneakyThrows здесь, я сделал это для краткости кода, обрабатывайте отмеченные исключения так, как считаете нужным.
  • Думаю, мы можем понять, почему синтаксис Kotlin можно считать более чистым, да? :)
person Hermann Steidel    schedule 15.07.2021

Простое решение - вы можете удалить опцию TLS (SSL) в AWS, а затем удалить «ssl_ca_certs = rds -comdated-ca-bundle.pem» из строки подключения. Но если приложению требуется подключение к базе данных SSL, вы можете использовать AWS Гид

person Chamara Maduranga    schedule 19.03.2020