Статьи

Учебник по Amazon Elastic Beanstalk — Руководство ULTIMATE (Скачать PDF)

ПРИМЕЧАНИЕ ДЛЯ РЕДАКЦИИ: AWS Elastic Beanstalk — это сервис оркестрации, предлагаемый Amazon Web Services для развертывания инфраструктуры, которая управляет различными сервисами AWS, включая EC2, S3, Simple Notification Service (SNS), CloudWatch, автоматическое масштабирование и Elastic Load Balancers.

Elastic Beanstalk обеспечивает дополнительный уровень абстракции над пустым сервером и ОС; вместо этого пользователи видят предварительно созданную комбинацию ОС и платформы.

Для развертывания требуется определить ряд компонентов: «приложение» в качестве логического контейнера для проекта, «версия», представляющая собой развертываемую сборку исполняемого файла приложения, «шаблон конфигурации», который содержит информацию о конфигурации для среды Beanstalk. и для продукта. (Источник: Википедия )

Теперь мы предоставляем исчерпывающее руководство, чтобы вы могли разрабатывать свои собственные приложения на основе Amazon Elastic Beanstalk. Мы охватываем широкий спектр тем, от развертывания и настройки до интеграции с Java и взаимодействия с командной строкой. С помощью этого руководства вы сможете запустить и запустить собственные проекты за минимальное время. Наслаждайтесь!

1. Введение

Amazon Elastic Beanstalk — это сервис, который позволяет вам использовать набор существующих сервисов Amazon AWS для размещения вашего приложения. В отличие от более общих служб, таких как EC2, вам не нужно предоставлять образ машины, которая развернута в облаке, но вы предоставляете только готовое приложение, которое размещено в предопределенной среде на Amazon AWS.

Amazon позволяет выбирать между различными предопределенными средами и настраивает все необходимое для запуска приложения от вашего имени. Поэтому этот сервис подходит вам, если вы хотите сконцентрироваться на самом приложении, а не на основной операционной системе и сервере. Тем не менее, поскольку Elastic Beanstalk использует скрытые существующие службы, такие как EC2, S3, CodeCommit или Route 53, вы имеете полный контроль над своим приложением.

2. Концепции

Amazon Elastic Beanstalk определяет набор терминов, которые используются в сервисе и поэтому должны быть понятны с самого начала. Приложение — это набор компонентов, который включает в себя не только различные версии приложения, но также конфигурации для сред, в которых развернуто приложение. Версия приложения — это развертываемый артефакт, который помечен и хранится в корзине Amazon S3. Поэтому он может быть восстановлен в более поздний момент времени. Среда — это набор ресурсов Amazon AWS, который используется для запуска определенной версии приложения. Различные среды могут быть настроены, и версия приложения может работать в разных средах.

Разные среды могут существовать одновременно, а также могут обслуживать разные версии одного и того же приложения. Среда состоит из двух уровней: среды веб-сервера и рабочей среды . В то время как среда веб-сервера обслуживает HTTP-запросы, рабочая среда читает сообщения из очереди и обрабатывает их. Таким образом, приложения могут использовать шаблон «рабочая очередь», чтобы отделить бизнес-логику от обслуживания HTTP-запроса. Конфигурация среды включает в себя настройки среды. Применение этой конфигурации позволит Amazon AWS создавать соответствующие ресурсы. Существующие шаблоны могут использоваться для создания конфигураций и поэтому называются шаблонами конфигурации .

2.1 Среда веб-сервера

«Среда веб-сервера» предоставляет все ресурсы для запуска вашего приложения. Он состоит из одного или нескольких веб-серверов, на которых развернуто ваше приложение. Эти веб-серверы размещаются на компьютерах EC2, работающих в облаке Amazon AWS. Поскольку эти машины находятся за Elastic Load Balancer, к ним можно обратиться по имени CNAME, например myapp.us-west-2.elasticbeanstalk.com . Это CNAME связывается с внутренним URL-адресом балансировки нагрузки с использованием службы Route 53. Этот сервис Amazon обеспечивает высокую доступность благодаря использованию Системы доменных имен (DNS). Машины EC2 вашей среды могут быть частью группы «Автоматическое масштабирование».

Это означает, что Amazon автоматически увеличивает количество экземпляров, если увеличивается нагрузка, и, с другой стороны, уменьшает количество машин, по крайней мере, до одной машины, если нагрузка исчезает. «Диспетчер хостов», работающий на каждой машине, выполняет задачу развертывания приложения и сбора метрик и событий. Он также контролирует сервер приложений и при необходимости поворачивает файлы журнала. Если одному компоненту среды требуется исправление или обновление, «Администратор хоста» может выполнить это обновление от вашего имени. «Группы безопасности» определяют правила брандмауэра для набора экземпляров. Основная группа безопасности в среде веб-сервера обеспечивает доступ к порту 80 (HTTP) вашего приложения. Если вам нужно использовать базу данных, вы можете определять другие группы безопасности с более детальным контролем.

2.2 Рабочая среда

«Рабочая среда» предоставляет все ресурсы для запуска рабочих приложений, которые используют сообщения из очереди Amazon SQS. Поэтому он не только обеспечивает компьютеры EC2, на которых выполняется ваше рабочее приложение, но и очередь SQS, которую можно использовать для передачи сообщений из «среды веб-сервера» в «рабочую среду» и обратно. Демон, работающий в каждом экземпляре «Рабочей среды», извлекает запросы из очереди и доставляет их в ваше приложение.

2.3 Рассмотрение проекта

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

Второй способ — увеличить количество запущенных сервисов (горизонтальное масштабирование), так как это позволяет добавлять больше машин при необходимости. За этим вторым способом следует Amazon Elastic Beanstalk, поскольку тщательный мониторинг доступных экземпляров EC2 позволяет при необходимости настраивать новые экземпляры в группе автоматического масштабирования. Но это также означает, что приложение разработано и написано для возможности масштабирования. Вместо создания монолитного приложения, которому требуется все больше и больше оборудования, горизонтальное масштабирование означает распределение нагрузки по произвольному количеству небольших служб, которые как можно меньше сохраняют состояние, так что новые службы могут быть добавлены во время выполнения при необходимости.

Балансировщик нагрузки перед средой веб-сервера будет распределять входящие запросы среди доступных служб, требуя, чтобы каждая служба могла обрабатывать одну из них. Другим важным моментом при разработке приложения для AWS Elastic Beanstalk является безопасность. Данные, передаваемые в и из среды веб-сервера, могут быть зашифрованы с использованием SSL. Поэтому необходимо приобрести действительный сертификат в одном из внешних сертификационных органов (например, VeriSign или Entrust). Обратите внимание, что шифрование SSL заканчивается на балансировщике нагрузки среды и что трафик между ним и веб-серверами обычно не шифруется.

Чтобы иметь возможность запускать и останавливать дополнительные экземпляры, когда это необходимо, ваше приложение не должно хранить никаких данных в локальном хранилище каждого узла, поскольку эти данные будут удалены после закрытия экземпляра и его не будет при запуске другого экземпляра. , Следовательно, нужно заботиться о постоянном хранении. Amazon AWS предлагает различные сервисы, которые можно использовать из вашего приложения для хранения состояния:

  • Amazon S3: этот сервис можно использовать для хранения произвольных объемов данных в облаке.
  • Amazon Elastic File System: EFS может быть смонтирована на вашем экземпляре EC2 и использоваться как обычная файловая система.
  • Amazon Elastic Block Store: тома EBS подключены к экземплярам EC2 и могут использоваться в сочетании с файловыми системами или базами данных.
  • Amazon DynamoDB: эта служба предоставляет базу данных NoSQL в облаке Amazon.
  • Сервис реляционных баз данных Amazon: RDS управляет шестью различными механизмами реляционных баз данных (Amazon Aurora, PostgreSQL, MySQL, MariaDB, Oracle и Microsoft SQL Server), которые могут использоваться вашим приложением.

Если пользователи по всему миру испытывают разное время ожидания, вы можете использовать Amazon CloudFront, чтобы он автоматически распределял ваше приложение и хранилище по всему миру и направлял пользователя на ближайший доступный сайт. И последнее, но не менее важное: вы можете позволить Amazon обновлять и исправлять вашу среду, если это станет необходимым. Эти обновления выполняются в виде «скользящих пакетов», т. Е. Amazon берет первую партию экземпляров EC2 и завершает их.

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

3. Веб-приложение Java

Теперь, когда мы узнали много нового о концепциях Amazon Elastic Beanstalk, нам нужно запачкать руки и разработать небольшое приложение, предоставляющее простой интерфейс REST с использованием Apache Tomcat.

3.1 Простой REST-API

Для начала мы создадим новый проект maven со следующей командной строкой:

1
mvn archetype:generate -DgroupId=com.javacodegeeks.ultimate.aws.eb -DartifactId=tomcat-web-service -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

После этого у нас есть новый каталог с именем tomcat-web-service со следующей структурой:

1
2
3
4
5
6
7
|-- pom.xml
`-- src
    |-- main
    |   `-- webapp
    |       `-- index.jsp
    |       `-- WEB-INF
    |           `-- web.xml

Архетип уже создал для нас web.xml и файл index.jsp . Последний может быть использован для последующего простого тестирования первой версии в облаке, поэтому мы пока не удаляем страницу JSP. Файл web.xml нуждается в некотором редактировании:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 
<web-app>
    <display-name>tutorial-webapp</display-name>
 
    <servlet>
        <servlet-name>RestServlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>com.javacodegeeks.ultimate.aws.eb</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>RestServlet</servlet-name>
        <url-pattern>/tutorial-service/*</url-pattern>
    </servlet-mapping>
</web-app>

С помощью display-name элемента XML мы определяем, как приложение будет помечено внутри сервера Apache Tomcat. Элементы servlet и servlet-mapping определяют класс Servlet, который прослушивает запросы, и шаблон URL, который он должен прослушивать. В нашем случае мы используем шаблон /tutorial-service/* , то есть все URL, которые выглядят как http://://tutorial-service/* будут обрабатываться этим сервлетом.

Имя контекста определяется через имя военного архива, который мы разворачиваем в tomcat. Параметр jersey.config.server.provider.packages сообщает реализации JAX-B, которую мы собираемся использовать для реализации REST-API, какие пакеты Java следует сканировать на наличие аннотаций. Чтобы сделать это, нам нужно создать следующую структуру каталогов в нашем проекте maven: src/main/java/com/javacodegeeks/ultimate/aws/eb . Чтобы сообщить maven, какую версию реализации JAX-B мы хотим использовать, мы копируем следующий блок информации о зависимостях в ваш файл pom.xml :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <jersey.version>2.26</jersey.version>
    <junit.version>4.12</junit.version>
    <commons-logging>1.1.3</commons-logging>
    <log4j.version>1.2.17</log4j.version>
    <javax-ws-rs-api.version>2.1</javax-ws-rs-api.version>
    <aws-sdk.version>1.11.106</aws-sdk.version>
    <db.dynamodb.local-endpoint>false</db.dynamodb.local-endpoint>
</properties>
 
<dependencies>
    <dependency>
        <groupId>javax.ws.rs</groupId>
        <artifactId>javax.ws.rs-api</artifactId>
        <version>${javax-ws-rs-api.version}</version>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>${commons-logging}</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.inject</groupId>
        <artifactId>jersey-hk2</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.core</groupId>
        <artifactId>jersey-client</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>${jersey.version}</version>
    </dependency>
</dependencies>
 
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-bom</artifactId>
            <version>${aws-sdk.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Помимо простого контракта API ( javax.ws.rs-api ), мы определяем использовать jersey-container-servlet качестве реализации JAX-B и jersey-hk2 для внедрения зависимостей для jersey. Поскольку в Amazon SDK используется регистрация commons-logging мы делаем то же самое. В качестве службы регистрации мы выбрали классическую реализацию log4j . junit и jersey-client в настоящее время используются только для наших интеграционных тестов.

Наличие интеграционных тестов, которые можно выполнить локально, значительно облегчает разработку, поскольку нам не нужно каждый раз загружать новую версию приложения в облако AWS. aws-java-sdk-bom в настоящее время не требуется, но, поскольку мы собираемся использовать SDK на следующих шагах, мы уже включили его здесь. Раздел build pom.xml снова немного pom.xml :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<build>
    <finalName>tomcat-web-service</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.12.1</version>
            <configuration>
                <excludes>
                    <exclude>**/*IntegrationTest*</exclude>
                </excludes>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>2.12.4</version>
            <configuration>
                <includes>
                    <include>**/*IntegrationTest*</include>
                </includes>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.2</version>
            <configuration>
                <url>http://localhost:8080/manager</url>
                <server>localhost</server>
                <path>/${project.build.finalName}</path>
                <contextFile>${project.basedir}/src/test/tomcat7-maven-plugin/context.xml</contextFile>
            </configuration>
            <executions>
                <execution>
                    <id>start-tomcat</id>
                    <phase>pre-integration-test</phase>
                    <goals>
                        <goal>run-war</goal>
                    </goals>
                    <configuration>
                        <fork>true</fork>
                    </configuration>
                </execution>
                <execution>
                    <id>stop-tomcat</id>
                    <phase>post-integration-test</phase>
                    <goals>
                        <goal>shutdown</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

maven-compiler-plugin используется для определения исходной и целевой версии сборки. Это необходимо, поскольку для предоставляемых сервисов установлена ​​определенная версия Java, и поэтому мы должны скомпилировать артефакт, который может быть выполнен в целевой среде. Плагин surefire и failsafe используются для выполнения локальных тестов junit и интеграции. Наконец, tomcat7-maven-plugin позволяет нам запускать и останавливать встроенный сервер Apache Tomcat в сборке для выполнения интеграционных тестов.

Этот шаг сокращает время между разработкой и тестированием, поскольку нам не нужно каждый раз запускать или перезагружать внешне установленный сервер. Так как мы не собираемся использовать какие-либо специфические функции Tomcat 8, tomcat7-maven-plugin должно быть достаточно. Как уже указывается в конфигурации tomcat7-maven-plugin , нам нужен специальный файл context.xml для интеграционных тестов tomcat. Поэтому следующее содержимое помещается в файл, расположенный в src/test/tomcat7-maven-plugin/context.xml :

1
2
3
4
<?xml version='1.0' encoding='utf-8'?>
<Context>
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>

Подготовив все, как описано выше, мы готовы развивать наш первый класс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.javacodegeeks.ultimate.aws.eb;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
 
@Path("/tutorial")
public class TutorialResource {
 
    private static final Log LOGGER = LogFactory.getLog(TutorialResource.class);
 
    @GET
    @Produces("text/json")
    @Path("/list-all-courses")
    public Response listAllCourses() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Listing all courses.");
        }
        List tutorials = new ArrayList<>();
        tutorials.add(new Tutorial("Linus Meyer", "Linux"));
        tutorials.add(new Tutorial("Bill Famous", "Microsoft"));
        tutorials.add(new Tutorial("Josh Hotspot", "Java"));
        return Response.status(200).entity(tutorials).build();
    }
}

Класс аннотируется @Path чтобы сообщить платформе JAX-RS, что все относительные пути URL, используемые в самом классе, должны иметь префикс /tutorial . Для простоты у нас в настоящее время есть только один метод: listAllCourses() . Его часть URL-адреса обозначена другой аннотацией @Path на уровне метода. То, что при вызове этого ресурса REST будет возвращаться строка JSON, указывается с аннотацией @Produces и медиа-типом text/json .

Наконец, мы сообщаем платформе, что этот метод является GET-запросом, используя выделенную аннотацию @GET . В настоящее время у нас нет постоянного хранилища, поэтому мы не можем загрузить данные из источника данных. Поэтому мы жестко связываем список курсов и возвращаем список объектов Tutorial . Класс Tutorial — это простой объект значений:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class Tutorial {
 
    private String author;
    private String title;
 
    public Tutorial() {
 
    }
 
    public Tutorial(String author, String title) {
        this.author = author;
        this.title = title;
    }
 
    public String getAuthor() {
        return author;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setAuthor(String author) {
        this.author = author;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        Tutorial tutorial = (Tutorial) o;
 
        if (author != null ? !author.equals(tutorial.author) : tutorial.author != null) return false;
        if (title != null ? !title.equals(tutorial.title) : tutorial.title != null) return false;
 
        return true;
    }
 
    @Override
    public int hashCode() {
        int result = author != null ? author.hashCode() : 0;
        result = 31 * result + (title != null ? title.hashCode() : 0);
        return result;
    }
 
    @Override
    public String toString() {
        return "Tutorial{" +
                "author='" + author + '\'' +
                ", title='" + title + '\'' +
                '}';
    }
}

Нам нужен конструктор по умолчанию, чтобы библиотека JSON могла десериализовать объекты типа Tutorial из строкового представления. Методы getter и setter позволяют библиотеке JSON устанавливать соответствующие значения атрибутов. В этом примере мы также реализуем методы equals() и hashCode() как мы хотим сравнить экземпляры Tutorial позже в наших интеграционных тестах. Как упоминалось ранее, сборка и развертывание приложения уже в AWS — это вариант, но потребуется некоторое время, чтобы развертывание стало доступным.

Поэтому мы просто пишем интеграционный тест, который проверяет, что наша реализация работает, как и ожидалось, чтобы сохранить ненужные загрузки в облако AWS (и сэкономить время). В pom.xml выше файле pom.xml мы настроили, чтобы интеграционные тесты назывались с IntegrationTest в имени класса. Следовательно, мы создаем класс в src/test/java со следующим содержимым:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class TutorialIntegrationTest {
 
    @Test
    public void testListAllCourses() {
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target("http://localhost:8080")
            .path("/tomcat-web-service/tomcat-web-service/tutorial/list-all-courses");
        Response response = target.request().get();
        assertThat(response.getStatus(), is(200));
        List tutorials = response.readEntity(new GenericType>(){});
        assertThat(tutorials.size(), is(3));
        assertThat(tutorials, hasItem(new Tutorial("Linus Meyer", "Linux")));
        assertThat(tutorials, hasItem(new Tutorial("Bill Famous", "Microsoft")));
        assertThat(tutorials, hasItem(new Tutorial("Josh Hotspot", "Java")));
    }
}

В первой строке метода testListAllCourses() создается новый клиент REST, а во второй строке testListAllCourses() хост, порт и путь на сервере. Для наших локальных тестов подходит localhost . Tomcat запускается по умолчанию на порту 8080. URL-адрес состоит из первой части имени нашего файла военных действий, который развернут в Tomcat. Мы определили это в файле pom.xml используя XML-элемент finalName .

GET-запросы выдаются с использованием метода get() для объекта, возвращаемого request() . Если все в порядке, веб-сервис должен вернуть код состояния 200. В этом случае он также должен возвращать список объектов Tutorial . Метод readEntity() позволяет нам определить это, используя экземпляр GenericType с двумя универсальными типами. Один для List и один внутренний тип для Tutorial . Полученный список должен содержать ровно три записи, по одной записи для каждого курса. Чтобы проверить все локально, мы запускаем следующую команду на локальном терминале:

1
mvn clean verify

Поскольку этап verify наступает после этапа integration-test , этот вызов также запустит ранее написанные интеграционные тесты:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[INFO] --- tomcat7-maven-plugin:2.2:run-war (start-tomcat) @ tomcat-web-service ---
[INFO] Running war on http://localhost:8080/tomcat-web-service
[INFO] Creating Tomcat server configuration at D:\development\glassfish\glassfish-5.1\awseb\awseb\tomcat-web-service\target\tomcat
[INFO] create webapp with contextPath: /tomcat-web-service
[...]
[INFO]
[INFO] --- maven-failsafe-plugin:2.12.4:integration-test (default) @ tomcat-web-service ---
[INFO] Failsafe report directory: D:\development\glassfish\glassfish-5.1\awseb\awseb\tomcat-web-service\target\failsafe-reports
 
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.javacodegeeks.ultimate.aws.eb.TutorialIntegrationTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.443 sec
 
Results :
 
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
 
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO]
[INFO] --- tomcat7-maven-plugin:2.2:shutdown (stop-tomcat) @ tomcat-web-service ---
[...]
[INFO]

Мы видим, что сервер Tomcat запускается до и после интеграционных тестов. В конце сборки файл tomcat-web-service.war находится в target директории нашего проекта. Это приложение, которое мы собираемся загрузить в Amazon Elastic Beanstalk сейчас.

3.2 Развертывание с использованием веб-интерфейса

Если вы еще не создали учетную запись Amazon AWS, сделайте это сейчас, указав в своем браузере следующий URL-адрес и нажав ссылку «Создать учетную запись AWS». На следующих этапах вам нужно будет предоставить типичную личную информацию и действующую кредитную карту. Последнее необходимо для того, чтобы Amazon выставил счет за использованные вами ресурсы. Когда вы создаете новую учетную запись, вы имеете право на «бесплатный уровень».

В течение первых 12 месяцев вы можете использовать до 750 часов вычислительной мощности в EC2, 5 ГБ в стандартной памяти на S3. Этого более чем достаточно для нашего урока. После того, как вы настроили рабочую учетную запись AWS, вы можете создать свое первое приложение. Поэтому укажите вашему браузеру следующий URL-адрес и введите название приложения, а также необязательное описание:

информация о приложении

На следующей странице спрашивается, собираемся ли мы настраивать среду веб-сервера или рабочую среду. Наше примерное приложение REST лучше всего подходит для среды первого типа; следовательно, мы нажимаем «Создать веб-сервер».

Amazon Elastic Beanstalk новая среда

Для среды веб-сервера мы должны установить конфигурацию и тип. Мы выбираем «Tomcat» в качестве предопределенной конфигурации и «Единственный экземпляр» в качестве типа. Таким образом, Amazon предоставляет нам экземпляр EC2 с установленным сервером Apache Tomcat. Мы не выбираем автоматическое масштабирование в этой точке, потому что для нашего примера достаточно одного экземпляра.

Тип среды Amazon Elastic Beanstalk

На следующей странице запрашивается указание версии приложения. Поэтому мы выбираем второй вариант и выбираем файл войны, который наша сборка maven создала ранее.

Версия приложения Amazon Elastic Beanstalk

В качестве альтернативы мы могли бы также предоставить ссылку на артефакт, который мы загрузили ранее в Amazon S3, или выбрать одно из примеров приложений Amazon.

Amazon Elastic Beanstalk информация об окружающей среде

Как упоминалось ранее, наше приложение получает собственный CNAME. Поэтому CNAME может быть предоставлена ​​на следующей странице. «Проверить наличие» позволяет нам убедиться, что имя все еще свободно. Если бы мы планировали использовать реляционную базу данных или виртуальную частную облачную сеть. В нашем простом примере нам не нужны оба ресурса, поэтому мы просто нажимаем «Далее»

Amazon Elastic Beanstalk дополнительные ресурсы

В разделе «Подробности конфигурации» мы можем выбрать тип сервера. Для наших экспериментов достаточно экземпляра t2.micro, но если вы предпочитаете, вы можете выбрать более крупный экземпляр. Документация EC2 описывает доступные типы экземпляров более подробно . Остальные поля ввода можно оставить как есть, поскольку у нас нет особых требований к диску экземпляров или отчетам о работоспособности. Пара ключей EC2 также не требуется.

Детали конфигурации Amazon Elastic Beanstalk

Теги среды можно использовать для идентификации сред в отчетах о распределении затрат или для общего управления средами и разрешениями. Для нашего первого примера приложения теги не нужны, но вы можете предоставить их.

Теги среды Amazon Elastic Beanstalk

Страница «Разрешения» позволяет определить профиль экземпляра и роль службы. Профиль экземпляра — это роль IAM, которая используется вашим приложением для связи с другими сервисами AWS, а роль сервиса — для мониторинга среды.

Разрешения Amazon Elastic Beanstalk

Наконец, на странице обзора представлена ​​вся информация для проверки. Если вы удовлетворены своим выбором, вы можете нажать «Запустить» и позволить Amazon AWS создать все ресурсы для вас. После завершения процесса вы можете увидеть новую среду в консоли:

Среда панели инструментов веб-службы Amazon Elastic Beanstalk tomcat

Щелчок по этой среде приводит к следующей панели инструментов:

Обзор Amazon Elastic Beanstalk

Здесь вы можете увидеть все события вашей среды, работающей версии и конфигурации. Теперь, когда приложение запущено и работает, мы можем указать нашему браузеру следующий URL:

Amazon Elastic Beanstalk URL-адрес кота

Как и ожидалось, браузер отображает массив JSON с тремя элементами Tutorial .

3.3 Развертывание с использованием CLI

Использование веб-консоли достаточно, если вам не приходится часто ее использовать. Если вы стремитесь к автоматизации, вы можете использовать инструмент awccli для создания сред из интерфейса командной строки (CLI). Для этого сначала нужно установить python и pip . Оба веб-сайта содержат информацию о том, как установить и настроить эти два инструмента для общих операционных систем. После того, как вы запустили python и pip, вы можете просто установить awscli используя следующую команду:

1
pip install awsebcli --upgrade --user

Чтобы иметь возможность напрямую запускать команду eb , вы должны включить ее в свой путь. В системах Windows вам нужно добавить следующий путь к PATH среды PATH : %USERPROFILE%\AppData\Roaming\Python \Python36\scripts . В системах на базе Linux и MacOS вам не нужно изменять какие-либо переменные среды. Первый шаг — инициализировать настройки по умолчанию, вызвав команду eb init в корневой папке вашего проекта maven:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
~/eb $ eb init
Select a default region
1) us-east-1 : US East (N. Virginia)
2) us-west-1 : US West (N. California)
3) us-west-2 : US West (Oregon)
4) eu-west-1 : EU (Ireland)
5) eu-central-1 : EU (Frankfurt)
6) ap-south-1 : Asia Pacific (Mumbai)
7) ap-southeast-1 : Asia Pacific (Singapore)
8) ap-southeast-2 : Asia Pacific (Sydney)
9) ap-northeast-1 : Asia Pacific (Tokyo)
10) ap-northeast-2 : Asia Pacific (Seoul)
11) sa-east-1 : South America (Sao Paulo)
12) cn-north-1 : China (Beijing)
13) us-east-2 : US East (Columbus)
14) ca-central-1 : Canada (Central)
15) eu-west-2 : EU (London)
(default is 3): 5

Затем вы должны предоставить свои учетные данные AWS:

1
2
3
4
You have not yet set up your credentials or your credentials are incorrect
You must provide your credentials.
(aws-access-id):
(aws-secret-key):

Следующий вопрос касается создания новой версии приложения, каждый раз, когда вы собираетесь развернуть новую версию своего программного обеспечения. Поскольку мы хотим создать новую версию, мы выбираем 1:

1
2
3
Select an application to use
1) [ Create new Application ]
(default is 1): 1

Теперь вы можете ввести название приложения:

1
2
3
Enter Application Name
(default is "eb"): tomcat-web-service
Application eb has been created.

Как и в веб-консоли, мы должны выбрать предварительно настроенную платформу для нашего приложения. Здесь мы выбираем Tomcat, а не Java (!), Иначе нашему приложению придется открывать собственный порт и обслуживать запросы.

01
02
03
04
05
06
07
08
09
10
11
12
13
Select a platform.
1) Node.js
2) PHP
3) Python
4) Ruby
5) Tomcat
6) IIS
7) Docker
8) Multi-container Docker
9) GlassFish
10) Go
11) Java
(default is 1): 5

Наконец, вы должны сказать, хотите ли вы использовать SSH для подключения к вашим экземплярам EC2. В этом простом примере мы не хотим делать это:

1
2
Do you want to set up SSH for your instances?
(y/n): n

Поскольку инструмент eb не знает, где находится наш артефакт, мы должны указать его, используя файл YAML, хранящийся в .elasticbeanstalk/config.yml со следующим содержимым:

1
2
deploy:
  artifact: target/tomcat-web-service.war

Теперь вы можете развернуть артефакт с помощью следующей команды:

1
eb deploy --staged

3.4 Использование DynamoDB

До сих пор наше приложение не хранит никаких данных. Как указывалось ранее, мы потеряем все состояние на локальном компьютере, когда текущий компьютер отключится, потому что механизм автоматического масштабирования больше не нуждается в нем. Следовательно, мы должны хранить состояние приложения во внешнем хранилище данных. Один вариант, который часто оказывается полезным, — это база данных Amazon NoSQL «DynamoDB». В DynamoDB данные хранятся в таблицах, как в реляционных базах данных.

Таблица представляет собой набор элементов, каждый элемент состоит из одного или нескольких атрибутов. Атрибут является парой ключ / значение. В отличие от реляционных баз данных здесь нет схемы, т. Е. Каждый элемент может иметь разные атрибуты. Например, вы можете создать таблицу для хранения событий. Каждое событие является элементом, а данные, описывающие конкретное событие, хранятся с использованием таких атрибутов, как время его создания, тип события и т. Д. Каждый элемент имеет хеш-ключ и, необязательно, дополнительный ключ сортировки. Хэш-ключ используется для распределения элементов в больших таблицах по различным разделам. Если таблица имеет ключ сортировки, она используется для сортировки элементов с одинаковым хэш-ключом.

Это облегчает сортировку всех элементов с определенным хеш-ключом. Секционирование позволяет распределять данные по различным машинам и, следовательно, является механизмом для горизонтального масштабирования приложения. Зная основы DynamoDB, мы можем расширить наше приложение-образец другим ресурсом, который позволяет нам создавать и удалять таблицы, необходимые для нашего приложения. Мы сделаем это с помощью Amazon AWS SKD; следовательно, мы должны определить зависимость от его части DynamoDB:

1
2
com.amazonaws
    aws-java-sdk-dynamodb

Конкретная версия выводится из aws-java-sdk-bom , которую мы определили в dependencyManagement в самом начале этого урока. Далее мы создаем новый класс DbResource :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Path("/db")
public class DbResource {
 
    private static final Log LOGGER = LogFactory.getLog(DbResource.class);
 
    @GET
    @Produces("text/json")
    @Path("/list-all-tables")
    public Response listAllTables() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Listing all tables.");
        }
        AmazonDynamoDB dynamoDB = createClient();
        ListTablesResult listTablesResult = dynamoDB.listTables();
        List tableNames = listTablesResult.getTableNames();
        return Response.status(200).entity(tableNames).build();
    }
 
    public AmazonDynamoDB createClient() {
        String property = System.getProperty("tutorial.dynamodb.local-endpoint");
        Boolean localEndpoint = Boolean.valueOf(property);
        if (!localEndpoint) {
            return AmazonDynamoDBClientBuilder
                    .standard()
                    .withRegion(Regions.EU_CENTRAL_1)
                    .build();
        } else {
            return AmazonDynamoDBClientBuilder.standard()
                    .withEndpointConfiguration(
                            new AwsClientBuilder.EndpointConfiguration("http://localhost:8000", "eu-central-1"))
                    .withCredentials(new AWSCredentialsProvider() {
                        @Override
                        public AWSCredentials getCredentials() {
                            return new AWSCredentials() {
                                @Override
                                public String getAWSAccessKeyId() {
                                    return "dummy";
                                }
 
                                @Override
                                public String getAWSSecretKey() {
                                    return "dummy";
                                }
                            };
                        }
 
                        @Override
                        public void refresh() {
 
                        }
                    })
                    .build();
        }
    }
}

Этот класс предоставляет REST URL /db/list-all-tables который возвращает список всех существующих таблиц в нашем экземпляре DynamoDB. Поэтому он создает экземпляр AmazonDynamoDB , который возвращается методом createClient() . Код этого метода запрашивает системное свойство tutorial.dynamodb.local-endpoint чтобы узнать, использовать ли локально работающий DynamoDB с заданной конечной точкой и фиктивными учетными данными или получить эту информацию с помощью DefaultAWSCredentialsProviderChain . Эта конкретная реализация выполняет поиск учетных данных AWS в следующем порядке:

  • Переменные среды: AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY
  • Свойства системы Java: aws.accessKeyId и aws.secretKey
  • Файл профиля хранится в домашнем каталоге пользователя
  • Учетные данные, предоставленные контейнером EC2
  • Учетные данные, предоставляемые службой метаданных EC2

Поскольку наш веб-сервис будет развернут на экземпляре EC2, учетные данные предоставляются средой AWS и поэтому не должны храниться в нашем приложении. В случае, если мы используем локальную конечную точку, мы можем использовать фиктивные значения и регион по нашему выбору. Как установить и запустить локальный экземпляр DynamoDB, объясняется здесь . Системное свойство может быть установлено автоматически при запуске сервера Tomcat, поместив следующий фрагмент внутри configuration подключаемого tomcat7-maven-plugin :

1
true

Чтобы протестировать описанную выше функциональность перед ее развертыванием в облаке, мы пишем следующий небольшой интеграционный тест:

01
02
03
04
05
06
07
08
09
10
@Test
public void testListAllTables() {
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target("http://localhost:8080")
        .path("/tomcat-web-service/tomcat-web-service/db/list-all-tables");
    Response response = target.request().get();
    assertThat(response.getStatus(), is(200));
    List tutorials = response.readEntity(new GenericType>(){});
    assertThat(tutorials.size(), is(0));
}

Тестовый код просто вызывает новый URL и проверяет, что возвращается пустой список строк. Точно так же мы можем написать код для создания и удаления таблиц:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public Response removeAllTables() {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Removing all tables.");
    }
    AmazonDynamoDB dynamoDB = createClient();
    ListTablesResult listTablesResult = dynamoDB.listTables();
    List tableNames = listTablesResult.getTableNames();
    for (String table : tableNames) {
        dynamoDB.deleteTable(table);
    }
    return Response.status(200).entity(tableNames).build();
}
 
@GET
@Produces("text/json")
@Path("/create-all-tables")
public Response createAllTables() {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Creating all tables.");
    }
    AmazonDynamoDB dynamoDB = createClient();
    List tableNames = new ArrayList();
    createTable(dynamoDB);
    return Response.status(200).entity(tableNames).build();
}
 
private void createTable(AmazonDynamoDB amazonDynamoDB) {
    List attributeDefinitions = new ArrayList();
    attributeDefinitions.add(new AttributeDefinition().withAttributeName("tutorial").withAttributeType("S"));
    List keySchema = new ArrayList();
    keySchema.add(new KeySchemaElement().withAttributeName("tutorial").withKeyType(KeyType.HASH));
    CreateTableRequest request = new CreateTableRequest()
            .withTableName("tutorials")
            .withKeySchema(keySchema)
            .withAttributeDefinitions(attributeDefinitions)
            .withProvisionedThroughput(
                    new ProvisionedThroughput()
                            .withReadCapacityUnits(1L)
                            .withWriteCapacityUnits(1L));
    DynamoDB dynamoDB = new DynamoDB(amazonDynamoDB);
    Table table = dynamoDB.createTable(request);
    try {
        table.waitForActive();
    } catch (InterruptedException e) {
        LOGGER.error("Failed to wait for table to become active: " + e.getLocalizedMessage(), e);
    }
}

Хотя код для удаления существующих таблиц более или менее createAllTables() метод createAllTables() определяет таблицу для хранения учебных пособий. Атрибут tutorial используется в качестве хэш-ключа. Для нашего примера приложения достаточно емкости чтения и записи, равной 1 единице емкости. Для реальных приложений это может быть недостаточно. Наконец, код ожидает готовности таблиц. Обратите внимание, что нам не нужно указывать все атрибуты при создании таблицы. Теперь мы можем изменить наш существующий метод listAllCourses() чтобы использовать новую таблицу tutorials :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@GET
@Produces("text/json")
@Path("/remove-all-tables")
@GET
@Produces("text/json")
@Path("/list-all-courses")
public Response listAllCourses(@QueryParam("author") String authorQuery) {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Listing all courses.");
    }
    List tutorials = new ArrayList<>();
    AmazonDynamoDB amazonDynamoDB = DbResource.createClient();
     
    Map expValues = new HashMap<>();
    expValues.put(":a", new AttributeValue(authorQuery));
     
    QueryRequest queryRequest = new QueryRequest("tutorials");
    queryRequest.setKeyConditionExpression("author = :a");
    queryRequest.setExpressionAttributeValues(expValues);
     
    QueryResult queryResult = amazonDynamoDB.query(queryRequest);
     
    List> items = queryResult.getItems();
    for (Map item : items) {
        AttributeValue author = item.get("author");
        AttributeValue title = item.get("title");
        Tutorial tutorial = new Tutorial();
        tutorial.setAuthor(author.getS());
        tutorial.setTitle(title.getS());
        tutorials.add(tutorial);
    }
    return Response.status(200).entity(tutorials).build();
}

Сначала мы получаем ссылку на AmazonDynamoDBклиента. Его метод query()использует экземпляр QueryRequestдля выполнения запроса к запрошенной таблице. В этом простом примере мы хотим найти все учебные пособия, предоставленные данным автором. Автор передается в качестве параметра запроса в URL. В JAX-RS это означает, что мы указываем аргумент для метода, listAllCourses()который аннотируется @QueryParam("author").

Значение, переданное аннотации, обозначает имя параметра запроса в URL, которое должно быть передано как Stringв метод. Условие для запроса записывается следующим образом : author = :a. Строка :aявляется атрибутом, для которого мы должны предоставить значение. Это делается путем создания HashMapи ввода значения ключа :aв него. Это HashMapтогда установлено на QueryRequest.

Результатом вызова queryResult.getItems()является список, который содержит экземпляры карты. Каждая карта представляет элемент, его пары ключ / значение хранятся внутри карты. Следовательно, мы перебираем этот список и преобразуем каждый элемент в экземпляр нашего класса Tutorialи возвращаем список руководств вызывающей стороне метода REST. Преобразование объектов Java в строки JSON выполняется с помощью фреймворка. Теперь мы можем вызвать следующие URL для создания таблиц и перечисления всех авторов:

1
2
3
http://.eu-central-1.elasticbeanstalk.com/tomcat-web-service/db/remove-all-tables
http://.eu-central-1.elasticbeanstalk.com/tomcat-web-service/db/create-all-tables
http://.eu-central-1.elasticbeanstalk.com/tomcat-web-service/tutorial/list-all-courses?author=test

В URL-адресах над строкой <your-app>и регионом замените CNAME и регион вашего приложения. Удаление всех таблиц, конечно, не обязательно, но в этом случае это не наносит вреда, поскольку мы удаляем только те таблицы, которые существуют. Теперь вам нужно реализовать метод, который вставляет данные в таблицу.

3.5 Использование RDS

Рядом с DynamoDB вы также можете использовать реляционную базу данных. Поэтому мы переходим в Консоль AWS к нашей среде и выбираем пункт меню «Конфигурация». В нижней части страницы конфигурации мы находим ссылку «создать новую базу данных RDS»:

Уровень данных Amazon Elastic Beanstalk

После нажатия на эту кнопку появляется следующая страница

Amazon Elastic Beanstalk создать RDS

На этой странице мы должны указать основные детали конфигурации для экземпляра RDS. Поскольку у нас нет существующего снимка, мы выбрали «Нет» в соответствующем поле. В качестве движка БД мы выбрали «MySQL» в версии 5.6.37. Предполагается, что эта БД будет работать на «микро» экземпляре с 5 ГБ памяти. «Политика удаления» определяет, что должно произойти, если мы удалим экземпляр. Поскольку это учебное пособие, мы не заботимся о данных и разрешаем Amazon удалить экземпляр и все его данные.

Кроме того, вы также можете указать, чтобы создать снимок базы данных. Для нашего примера приложения также достаточно «единой зоны доступности». После предоставления основного имени пользователя и пароля, мы можем нажать «Применить». Теперь может пройти до 10 минут, пока база данных RDS не станет доступной. А пока мы можем настроить список maven dependenciesв pom.xmlфайле и добавить соединитель mysql, как показано ниже:

1
2
3
4
5
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.44</version>
</dependency>

Обратите внимание, что мы должны установить область действия этой зависимости compile, поскольку указанный файл jar должен быть помещен в папку lib нашей войны. В противном случае классы драйверов MySQL были бы недоступны во время выполнения. Далее мы создаем новый класс с именем RdsResource:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Path("/rds")
public class RdsResource {
 
    private static final Log LOGGER = LogFactory.getLog(RdsResource.class);
 
    @GET
    @Produces("text/json")
    @Path("/list-all-tables")
    public Response listAllTables() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Listing all tables.");
        }
        List tableNames = null;
        try {
            tableNames = listAllTablesIntern();
        } catch (SQLException e) {
            return Response.status(500).entity("Listing all tables failed: " + e.getLocalizedMessage()).build();
        }
        return Response.status(200).entity(tableNames).build();
    }
 
    private List listAllTablesIntern() throws SQLException {
        List tableNames = new ArrayList<>();
        try (Connection connection = createConnection()) {
            if (connection != null) {
                try (Statement stmt = connection.createStatement()) {
                    ResultSet resultSet = stmt.executeQuery("show tables");
                    while (resultSet.next()) {
                        String tableName = resultSet.getString(1);
                        tableNames.add(tableName);
                    }
                }
            }
        }
        return tableNames;
    }
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static Connection createConnection() throws SQLException {
        if (System.getProperty("RDS_HOSTNAME") != null) {
            try {
                Class.forName("com.mysql.jdbc.Driver");
                String dbName = System.getProperty("RDS_DB_NAME");
                String userName = System.getProperty("RDS_USERNAME");
                String password = System.getProperty("RDS_PASSWORD");
                String hostname = System.getProperty("RDS_HOSTNAME");
                String port = System.getProperty("RDS_PORT");
                String jdbcUrl = "jdbc:mysql://" + hostname + ":" + port + "/" + dbName;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Connecting to JDBC-URL: " + jdbcUrl);
                }
                Connection con = DriverManager.getConnection(jdbcUrl, userName, password);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Connection to JDBC-URL: " + jdbcUrl + " successful.");
                }
                return con;
            } catch (ClassNotFoundException e) {
                LOGGER.error("Could not load driver: " + e.getLocalizedMessage(), e);
            }
        }
        return null;
    }
}

Он предоставит URL, который заканчивается /rds/list-all-tablesи возвращает список всех доступных таблиц MySQL. Поэтому метод listAllTables()вызывает закрытый метод listAllTablesInternи возвращает HTTP-ответ с кодом состояния 200, если исключение не произошло, или кодом состояния 500, если перечисление таблиц не удалось.

Частный метод listAllTablesIntern()использует другой частный метод для получения соединения с базой данных: createConnection(). В среде Tomcat вся необходимая информация передается приложению через системные свойства. Запрос этих предопределенных системных свойств позволяет нам создать URL JDBC в следующей форме:

1
jdbc:mysql://" + hostname + ":" + port + "/" + dbName

В реальном приложении это может стать URL-адресом следующей формы:

1
jdbc:mysql://aa1im36v00yvfox.cwazv5kmikco.eu-central-1.rds.amazonaws.com:3306/ebdb

Имея соединение JDBC в наших руках, мы можем выполнить инструкцию SQL, которая перечисляет все существующие таблицы базы данных:

1
2
3
4
5
6
7
try (Statement stmt = connection.createStatement()) {
    ResultSet resultSet = stmt.executeQuery("show tables");
    while (resultSet.next()) {
        String tableName = resultSet.getString(1);
        tableNames.add(tableName);
    }
}

Мы возвращаем список имен таблиц в списке, который отображается платформой JAX-RS в массив JSON. После обновления версии нашего примера приложения в облаке мы можем открыть следующий URL в нашем браузере:

1
http://.eu-central-1.elasticbeanstalk.com/tomcat-web-service/rds/list-all-tables

Еще раз: замените в вышеуказанных URL-адресах строку <your-app>и регион на CNAME и регион вашего приложения. Это должно показать пустой массив JSON. Настроив все как описано выше, мы теперь можем реализовать методы для создания и удаления таблиц нашего приложения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@GET
@Produces("text/json")
@Path("/remove-all-tables")
public Response removeAllTables() {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Removing all tables.");
    }
    List tableNames = new ArrayList<>();
    List tables;
    try {
        tables = listAllTablesIntern();
    } catch (SQLException e) {
        return Response.status(500).entity("Listing all tables failed: " + e.getLocalizedMessage()).build();
    }
    for (String table : tables) {
        try (Connection connection = createConnection()) {
            if (connection != null) {
                try (Statement stmt = connection.createStatement()) {
                    stmt.executeUpdate("drop table " + table);
                    tableNames.add(table);
                }
            }
        } catch (SQLException e) {
            LOGGER.error("Removing all tables failed: " + e.getLocalizedMessage(), e);
            return Response.status(500).entity("Removing all tables failed: " + e.getLocalizedMessage()).build();
        }
    }
    return Response.status(200).entity(tableNames).build();
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@GET
@Produces("text/json")
@Path("/create-all-tables")
public Response createAllTables() {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Creating all tables.");
    }
    List tableNames = new ArrayList<>();
    try (Connection connection = createConnection()) {
        if (connection != null) {
            try (Statement stmt = connection.createStatement()) {
                stmt.executeUpdate("create table tutorials (" +
                        "id int not null auto_increment, " +
                        "author varchar(30) not null," +
                        "title varchar(30) not null," +
                        "primary key (id)" +
                        ")");
                tableNames.add("tutorials");
            }
        }
    } catch (SQLException e) {
        LOGGER.error("Creating tables failed: " + e.getLocalizedMessage(), e);
        return Response.status(500).entity("Creating tables failed: " + e.getLocalizedMessage()).build();
    }
    return Response.status(200).entity(tableNames).build();
}

Приведенный выше код использует уже объясненный метод createConnection()и поэтому может быть легко понят. Два оператора SQL для создания и удаления таблиц выполняются через API JDBC с использованием его executeUpdate()метода. Мы закрываем каждое утверждение и соединение после его использования, чтобы освободить ресурсы. Обратите внимание, что в реальном приложении вы можете объединить соединения, чтобы повторно использовать существующее для следующего запроса. Изменение кода, который вставляет и запрашивает учебники, остается за читателем.

4. Веб-приложение Java с Spring Boot

В предыдущей главе объяснялось, как использовать среду Apache Tomcat для запуска простого сервера REST API. То же самое можно сделать с помощью популярного фреймворка Spring Boot . Поэтому в этой главе мы собираемся создать приложение Spring Boot, которое работает в среде «Java» в AWS, а не в среде «Tomcat».

Обратите внимание, что мы также можем создать приложение Spring Boot для запуска в среде Tomcat, но в демонстрационных целях мы запускаем наше приложение в автономном режиме. Сначала мы создаем новый проект maven с помощью следующей команды:

1
mvn archetype:generate -DgroupId=com.javacodegeeks.ultimate.aws.eb -DartifactId=spring-boot-web -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

Далее мы позволяем нашему проекту наследоваться от родительского проекта Spring Boot:

1
2
3
4
5
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
</parent>

Это предоставляет нам все необходимые версии, но не добавляет никаких зависимостей. Следовательно, мы должны добавить их к нашему pom.xml:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<properties>
    <jersey.version>2.26</jersey.version>
</properties>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.core</groupId>
        <artifactId>jersey-client</artifactId>
        <version>${jersey.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.inject</groupId>
        <artifactId>jersey-hk2</artifactId>
        <version>${jersey.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>
 
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Основная зависимость есть spring-boot-starter-web. Другие используются для наших интеграционных тестов. spring-boot-maven-pluginэто плагин maven, который упаковывает приложение так, что мы можем легко запустить его из командной строки. Теперь мы можем реализовать простейшее из возможных приложений:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@RestController
@EnableAutoConfiguration
public class Example {
 
    @RequestMapping("/")
    String home() {
        return "Hello World!";
    }
 
    public static void main(String[] args) throws Exception {
        SpringApplication.run(Example.class, args);
    }
 
}

main()Метод запускает Exampleкласс как контроллер и передает необязательные аргументы из командной строки к run()методу из SpringApplication. Аннотирование класса с помощью @RestControllerпревращает его в конечную точку REST. Метод home()просто возвращает статическую строку. Его аннотация @RequestMapping("/")обозначает, что он должен вызываться в случае, если пользователь запрашивает корневой URL нашего приложения.

В облаке Amazon AWS прокси-сервер перед нашим приложением будет направлять весь входящий трафик на порт 5000. Это заставляет нас запускать наше приложение на этом конкретном порту. Следовательно, мы создаем файл application.propertiesсо следующим содержимым и помещаем его в src/main/resources:

1
server.port=5000

Теперь мы можем запустить приложение из командной строки:

1
mvn spring-boot:run

Это приведет к выводу, аналогичному следующему (сокращенному):

01
02
03
04
05
06
07
08
09
10
11
12
[...]
[INFO] --- spring-boot-maven-plugin:1.5.9.RELEASE:run (default-cli) @ spring-boot-web ---
 
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.9.RELEASE)
  
[...]

Нажатие Ctrl+Cзавершит приложение. Поскольку мы не хотим тестировать весь код вручную после его загрузки в облако, мы пишем небольшой интеграционный тест, подобный следующему:

01
02
03
04
05
06
07
08
09
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest(classes = { Example.class },
    webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class ExampleTest {
 
    @Test
    public void testHelloWorld() {
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target("http://localhost:5000").path("/");
        Response response = target.request().get();
        assertThat(response.getStatus(), is(200));
    }
}

Это простой тест junit, который запускается с SpringRunner. Аннотация SpringBootTestсообщает платформе, какой ресурс мы хотим протестировать и какой порт использовать. Фактический код нашего теста прост и использует клиент Джерси для вызова URL http://localhost:5000/.

После получения ответа мы проверяем его код состояния. Зная, что локально все работает как положено, мы создаем новую среду на Amazon Beanstalk. Процедура та же, что и раньше, но теперь мы выбрали «Java» вместо «Tomcat»:

Amazon Elastic Beanstalk Тип среды

После создания новой среды мы можем ввести следующий URL в наш веб-браузер и убедиться, что он работает:

1
http://spring-boot-web-env.eu-central-1.elasticbeanstalk.com/

Пожалуйста, замените spring-boot-web-envи eu-central-1на CNAME и регион вашей среды. Теперь вы должны увидеть строку «Hello World» в вашем браузере. Обратите внимание, что мы используем стандартный порт 80 для вызова API REST, а не порт 5000, поскольку прокси-сервер перед нашим приложением прослушивает стандартный порт и отправляет его только на порт 5000 нашего приложения.

5. Загрузите исходный код

Это было учебное пособие по Elastic Beanstalk от Amazon.