将AWS documentDB附加到Spring Boot应用程序

The*_*Way 4 spring amazon-web-services aws-documentdb-mongoapi

我最近尝试在Spring应用程序中将新的AWS DocumentDB服务用作数据库。

群集已在与我在其上部署应用程序的EKS相同的VPC中创建。安全组允许VPC中所有节点之间的连接。

AWS为我的数据库集群公开了这样的mongo URI:

mongodb://<my-user>:<insertYourPassword>@<my-cluster-endpoint>:27017/?ssl_ca_certs=rds-combined-ca-bundle.pem&replicaSet=rs0
Run Code Online (Sandbox Code Playgroud)

我的问题:

如何使我的Spring代码与这种连接一起工作?

我尝试将followig添加到我的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
Run Code Online (Sandbox Code Playgroud)

并将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

它看起来像是一个安全组问题,但我没有问题,可以从运行Spring应用程序Pod的同一EC2中连接mongo shell。

有任何想法吗?

Her*_*del 7

@Sunny Pelletier提供的答案对我有用,它在我们的 Java 设置中混合了@Frank的答案。

因此,对我来说,我想要一个适用于我们本地 docker 设置以及任何具有活动配置文件和通过 CDK 在我们的环境中设置的其他环境变量的 AWS 环境的解决方案。

我首先从一个简单的配置 POJO 开始,在范例之外设置我的属性spring.data.mongo.*。您不必这样做,只需让 Spring 处理它,就像通常创建MongoClient.

我的默认本地开发application.yml和相应的配置类。

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

Run Code Online (Sandbox Code Playgroud)
@Data
@Configuration
@ConfigurationProperties(prefix = "mongo")
public class MongoConnectConfig {

    private int port;

    private String host;

    private String user;

    private String database;

    private String password;

}

Run Code Online (Sandbox Code Playgroud)

然后,我创建了两个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()
        );
    }
}

Run Code Online (Sandbox Code Playgroud)

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()
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,我们将其放入文件夹rds-combined-ca-bundle.pemsrc/main/resources/certs/

附注:

  • 再说一遍,我相信您应该能够摆脱使用默认spring.data*属性的困扰,并且您MongoClient应该使用它们。
  • 忽略@SneakyThrows这里,我只是为了代码简洁的目的而这样做,按照您认为合适的方式处理您检查的异常。
  • 我想我们可以明白为什么 Kotlin 语法可以被认为是“更干净”了吧?:)


Bar*_*ath 5

如文档中所述,

根据设计,您可以从与Amazon DocumentDB资源相同的Amazon VPC中的Amazon EC2实例访问Amazon DocumentDB资源(具有MongoDB兼容性)。但是,假设您的用例要求您或您的应用程序从集群的Amazon VPC外部访问Amazon DocumentDB资源。在这种情况下,您可以使用SSH隧道(也称为“端口转发”)访问您的Amazon DocumentDB资源。

从外部VPC连接

您的Amazon DocumentDB集群应在默认的虚拟私有云(VPC)中运行。要与Amazon DocumentDB集群进行交互,您必须在创建Amazon DocumentDB集群的同一AWS区域中将Amazon Elastic Compute Cloud(Amazon EC2)实例启动到默认VPC中。

按照指南连接到集群 AWS DocumentDB集群

GitHub参考spring-boot-aws-documentdb

更新

要通过SSL连接,请通过设置指向AWS区域特定中间证书的SSL_CERTIFICATE使用以下逻辑。

可以从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;
    }


    }
Run Code Online (Sandbox Code Playgroud)

连接输出

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)]
Run Code Online (Sandbox Code Playgroud)

  • 它正在工作,但所有其他 https 调用甚至 aws sdk 调用都失败。获取 PKIX 路径构建失败:sun.security.provider.certpath.SunCertPathBuilderException:无法找到请求的有效证书路径。任何想法? (2认同)