- README 추가

This commit is contained in:
hyunjujeong 2023-09-13 15:05:13 +09:00
parent 2aa1f0c225
commit 9e722d02c4
22 changed files with 97 additions and 584 deletions

97
README.md Normal file
View File

@ -0,0 +1,97 @@
# [BlokWorks] Device Deployer Service #
사용자에게 Command 를 입력받아 MQTT 를 발행하고 배포된 앱의 정보를 저장하고, 조회하는 Spring boot 기반 Java 애플리케이션입니다.
## 기반 기술 ##
---
* Java 17
* Spring Boot 3.1.3
* Gradle 8.2.1
## 주요 의존성 ##
---
* MQTT
* PostgreSQL
## 빌드 ##
---
### 프로젝트 패키징 ###
Gradle을 사용하여 패키징합니다.
```shell
$ ./gradlew clean build -x test
```
### 도커 빌드 ###
```dockerfile
FROM amazoncorretto:17.0.6-al2023
EXPOSE 8081
ADD ./build/libs/*.jar device-deployer.jar
ENTRYPOINT ["java", "-jar", "/device-deployer.jar"]
```
Image Registry로 사용할 AWS ECR에 로그인을 합니다.
```shell
$ aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 003960268191.dkr.ecr.ap-northeast-2.amazonaws.com
```
패키징된 jar 파일을 Docker image로 빌드합니다.
```shell
$ docker build --platform linux/amd64 -t device-deployer:0.0.7-SNAPSHOT .
$ docker tag sdt-cloud/device-deployer:0.0.7-SNAPSHOT 003960268191.dkr.ecr.ap-northeast-2.amazonaws.com/sdt-cloud/device-deployer:{{version}}
```
### 이미지 업로드 ###
빌드 및 태깅된 Docker image를 Image Registry에 업로드 합니다.
```shell
$ docker push 003960268191.dkr.ecr.ap-northeast-2.amazonaws.com/sdt-cloud/device-deployer:{{version}}
```
## 구동 ##
---
### 사전 구동 애플리케이션 및 모듈 ###
1. MQTT 컨테이너 모듈
- 사용자의 Command 를 Edge(Device)에게 전달하기 위한 Message Queue 입니다.
2. PostgreSQL 컨테이너 모듈
- 실행된 Command 의 정보를 저장하고 조회할 수 있는 RDB 입니다.
### 설정 정보 ###
설정 정보 등록은 설정 관리 애플리케이션의 POST /configuration API 를 이용합니다.
| 옵션 | 설명 | 필수 여부 | 기본값 |
|:--------------------------:|:-----------------------------------:|:-----:|:---:|
| server.port | 서버 포트 | O | - |
| inbound.mqtt.url | MQTT 모듈의 url | O | - |
| inbound.mqtt.username | MQTT 모듈의 username | O | - |
| inbound.mqtt.password | MQTT 모듈의 password | O | - |
| inbound.mqtt.topics | MQTT 모듈이 구독할 Inference 결과 데이터 topic | O | - |
| spring.datasource.uri | PostgreSQL 모듈의 연결을 위한 URI | O | - |
| spring.datasource.username | PostgreSQL 모듈의 username | O | - |
| spring.datasource.pssword | PostgreSQL 모듈의 password | O | - |
### 배포 ###
배포는 k8s manifest로 작성한 deploy.yaml을 이용합니다.
Deployment 오브젝트에 반드시 명시해야 하는 설정 정보(`env`)는 아래와 같습니다.
| 옵션 | 설명 | 필수 여부 | 예시 |
|:-----------------------------:|:-------------------------------------------------:|:-----:|:-----------------------------------------------------------------------------------------------------------------------------------------:|
| SPRING_PROFILES_ACTIVE | 구동 profile | O | k8s |
| SERVER_PORT | 애플리케이션이 등록될 포트 | O | 8085 |
| POSTGRES_CREDENTIALS_URL | PostgreSQL의 URL | O | jdbc:postgresql://localhost:5432/blokworks |
| POSTGRES_CREDENTIALS_USERNAME | PostgreSQL의 계정 | O | sdt |
| POSTGRES_CREDENTIALS_PASSWORD | PostgreSQL의 비밀번호 | O | 2xxxxx |
| IAM_AMQP_HOST | RabbitMQ의 URL | O | rabbitmq.sdt-cloud.svc.cluster.local |
| IAM_AMQP_PORT | RabbitMQ의 포트 | O | 5672 |
| IAM_AMQP_USERNAME | RabbitMQ에 접속할 계정 | O | guest |
| IAM_AMQP_PASSWORD | RabbitMQ에 접속할 비밀번호 | O | guest |

View File

@ -1,7 +1,6 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.presentation.GiteaApiRequestHandler;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;

View File

@ -1,37 +0,0 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class DefaultProcessService implements ProcessService{
private final Logger log;
private final ProcessRepositoryDelegate processRepositoryDelegate;
public DefaultProcessService(ProcessRepositoryDelegate processRepositoryDelegate) {
this.log = LoggerFactory.getLogger(this.getClass());
this.processRepositoryDelegate = processRepositoryDelegate;
}
@Override
public Mono<Void> apply(InboundProcessMessagePayload inboundProcessMessagePayload) {
log.info("[apply] inboundProcessMessagePayload = {}", inboundProcessMessagePayload);
// 프로세스 저장
return Mono.just(inboundProcessMessagePayload)
.doOnNext(processRepositoryDelegate)
.then();
}
@Override
public Page<Process> getAll(String assetCode, int page, int size) {
log.info("[getAll] assetCode = {}, page = {}, size = {}", assetCode, page, size);
return processRepositoryDelegate.findAllByAssetCode(assetCode, page, size);
}
}

View File

@ -1,7 +1,6 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;

View File

@ -1,15 +1,9 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import inc.sdt.blokworks.devicedeployer.presentation.GiteaApiRequestHandler;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
@Component
public class DeployCommandInvoker {

View File

@ -7,8 +7,6 @@ import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessage
import org.springframework.data.domain.Page;
import reactor.core.publisher.Mono;
import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.function.Function;
public interface DeployerService extends Function<InboundDeployMessagePayload, Mono<Void>> {

View File

@ -1,12 +0,0 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.springframework.data.domain.Page;
import java.util.function.Consumer;
public interface ProcessRepositoryDelegate extends Consumer<InboundProcessMessagePayload> {
Process save(Process process);
Page<Process> findAllByAssetCode(String assetCode, int page, int size);
}

View File

@ -1,12 +0,0 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.springframework.data.domain.Page;
import reactor.core.publisher.Mono;
import java.util.function.Function;
public interface ProcessService extends Function<InboundProcessMessagePayload, Mono<Void>> {
Page<Process> getAll(String assetCode, int page, int size);
}

View File

@ -1,63 +0,0 @@
package inc.sdt.blokworks.devicedeployer.domain;
public class Port {
private String protocol;
private Integer hostPort;
private Integer containerPort;
protected Port() {}
public String getProtocol() {
return protocol;
}
public Integer getHostPort() {
return hostPort;
}
public Integer getContainerPort() {
return containerPort;
}
@Override
public String toString() {
return "Port{" +
"protocol='" + protocol + '\'' +
", hostPort=" + hostPort +
", containerPort=" + containerPort +
'}';
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String protocol;
private Integer hostPort;
private Integer containerPort;
public Builder protocol(String protocol) {
this.protocol = protocol;
return this;
}
public Builder hostPort(Integer hostPort) {
this.hostPort = hostPort;
return this;
}
public Builder containerPort(Integer containerPort) {
this.containerPort = containerPort;
return this;
}
public Port build() {
Port port = new Port();
port.protocol = this.protocol;
port.hostPort = this.hostPort;
port.containerPort = this.containerPort;
return port;
}
}
}

View File

@ -1,139 +0,0 @@
package inc.sdt.blokworks.devicedeployer.domain;
public class Process {
private String assetCode;
private Integer pid;
private String name;
private Integer cpu;
private Integer memory;
private Integer network;
private Long processedAt;
private OperationType operationType;
protected Process() {}
public Process(String assetCode, Integer pid, String name, Integer cpu, Integer memory, Integer network, Long processedAt, OperationType operationType) {
this.assetCode = assetCode;
this.pid = pid;
this.name = name;
this.cpu = cpu;
this.memory = memory;
this.network = network;
this.processedAt = processedAt;
this.operationType = operationType;
}
public String getAssetCode() {
return assetCode;
}
public Integer getPid() {
return pid;
}
public String getName() {
return name;
}
public Integer getCpu() {
return cpu;
}
public Integer getMemory() {
return memory;
}
public Integer getNetwork() {
return network;
}
public Long getProcessedAt() {
return processedAt;
}
public OperationType getOperationType() {
return operationType;
}
@Override
public String toString() {
return "Process{" +
"assetCode='" + assetCode + '\'' +
", pid=" + pid +
", name='" + name + '\'' +
", cpu=" + cpu +
", memory=" + memory +
", network=" + network +
", processedAt=" + processedAt +
", operationType=" + operationType +
'}';
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String assetCode;
private Integer pid;
private String name;
private Integer cpu;
private Integer memory;
private Integer network;
private Long processedAt;
private OperationType operationType;
public Builder assetCode(String assetCode) {
this.assetCode = assetCode;
return this;
}
public Builder pid(Integer pid) {
this.pid = pid;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder cpu(Integer cpu) {
this.cpu = cpu;
return this;
}
public Builder memory(Integer memory) {
this.memory = memory;
return this;
}
public Builder network(Integer network) {
this.network = network;
return this;
}
public Builder processedAt(Long processedAt) {
this.processedAt = processedAt;
return this;
}
public Builder operationType(OperationType operationType) {
this.operationType = operationType;
return this;
}
public Process build() {
Process process = new Process();
process.assetCode = this.assetCode;
process.pid = this.pid;
process.name = this.name;
process.cpu = this.cpu;
process.memory = this.memory;
process.network = this.network;
process.processedAt = this.processedAt;
process.operationType = this.operationType;
return process;
}
}
}

View File

@ -1,15 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.mqtt;
import inc.sdt.blokworks.devicedeployer.domain.Status;
public record InboundProcessMessagePayload(
Status status,
String assetCode,
Integer pid,
String name,
Integer cpu,
Integer memory,
Integer network,
Long processedAt
) {
}

View File

@ -1,7 +1,6 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.mqtt;
import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.DeviceType;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import java.util.HashMap;

View File

@ -1,78 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
import jakarta.persistence.*;
@Entity(name = "app_process")
class ProcessEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "process_id", length = 36)
private String id;
@Column(name = "asset_code", length = 255)
private String assetCode;
@Column(name = "pid")
private Integer pid;
@Column(name = "app_name", length = 255)
private String appName;
@Column(name = "cpu")
private Integer cpu;
@Column(name = "memory")
private Integer memory;
@Column(name = "network")
private Integer network;
@Column(name = "processed_at")
private Long processedAt;
@Column(name = "operation_type")
@Enumerated(EnumType.STRING)
private OperationType operationType;
protected ProcessEntity() {}
public ProcessEntity(String assetCode, Integer pid, String appName, Integer cpu, Integer memory, Integer network, Long processedAt, OperationType operationType) {
this.assetCode = assetCode;
this.pid = pid;
this.appName = appName;
this.cpu = cpu;
this.memory = memory;
this.network = network;
this.processedAt = processedAt;
this.operationType = operationType;
}
public String getId() {
return id;
}
public String getAssetCode() {
return assetCode;
}
public Integer getPid() {
return pid;
}
public String getAppName() {
return appName;
}
public Integer getCpu() {
return cpu;
}
public Integer getMemory() {
return memory;
}
public Integer getNetwork() {
return network;
}
public Long getProcessedAt() {
return processedAt;
}
public OperationType getOperationType() {
return operationType;
}
}

View File

@ -1,9 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProcessJpaRepository extends JpaRepository<ProcessEntity, String> {
Page<ProcessEntity> findAllByAssetCode(String assetCode, Pageable pageable);
}

View File

@ -1,84 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import inc.sdt.blokworks.devicedeployer.application.ProcessRepositoryDelegate;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
@Component
public class ProcessRelationalRepository implements ProcessRepositoryDelegate {
private final Logger log;
private final ProcessJpaRepository processJpaRepository;
public ProcessRelationalRepository(ProcessJpaRepository processJpaRepository) {
this.log = LoggerFactory.getLogger(this.getClass());
this.processJpaRepository = processJpaRepository;
}
@Override
public void accept(InboundProcessMessagePayload payload) {
this.save(this.fromMessage(payload));
}
@Override
public Process save(Process process) {
log.info("[save] process = {}", process);
ProcessEntity entity = this.toEntity(process);
processJpaRepository.save(entity);
return process;
}
@Override
public Page<Process> findAllByAssetCode(String assetCode, int page, int size) {
log.info("[findAllByAssetCode] assetCode = {}", assetCode);
Pageable pageable = page < 0 ? Pageable.unpaged() : PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "processedAt"));
return processJpaRepository.findAllByAssetCode(assetCode, pageable)
.map(this::fromEntity);
}
private ProcessEntity toEntity(Process process) {
return new ProcessEntity(
process.getAssetCode(),
process.getPid(),
process.getName(),
process.getCpu(),
process.getMemory(),
process.getNetwork(),
process.getProcessedAt(),
OperationType.START
);
}
private Process fromEntity(ProcessEntity entity) {
return Process.builder()
.assetCode(entity.getAssetCode())
.pid(entity.getPid())
.name(entity.getAppName())
.cpu(entity.getCpu())
.memory(entity.getMemory())
.network(entity.getNetwork())
.processedAt(entity.getProcessedAt())
.operationType(entity.getOperationType())
.build();
}
private Process fromMessage(InboundProcessMessagePayload payload) {
return Process.builder()
.assetCode(payload.assetCode())
.pid(payload.pid())
.name(payload.name())
.cpu(payload.cpu())
.memory(payload.memory())
.network(payload.network())
.processedAt(payload.processedAt())
.operationType(OperationType.START)
.build();
}
}

View File

@ -1,10 +1,7 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.DeviceType;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import org.wildfly.common.annotation.NotNull;
import org.wildfly.common.annotation.Nullable;
import java.util.LinkedHashMap;

View File

@ -2,7 +2,6 @@ package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import org.springframework.stereotype.Component;
import java.util.*;

View File

@ -1,8 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
public record PortResource(
String protocol,
Integer hostPort,
Integer containerPort
) {
}

View File

@ -1,23 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.Port;
import org.springframework.stereotype.Component;
@Component
public class PortResourceConverter {
public PortResource toResource(Port port) {
return new PortResource(
port.getProtocol(),
port.getHostPort(),
port.getContainerPort()
);
}
public Port fromResource(PortResource resource) {
return Port.builder()
.protocol(resource.protocol())
.hostPort(resource.hostPort())
.containerPort(resource.containerPort())
.build();
}
}

View File

@ -1,41 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.application.ProcessService;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.amqp.ResourceMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@RestController
public class ProcessController {
private final Logger log;
private final ProcessService processService;
private final ProcessResourceConverter processResourceConverter;
public ProcessController(ProcessService processService,
ProcessResourceConverter processResourceConverter) {
this.log = LoggerFactory.getLogger(this.getClass());
this.processService = processService;
this.processResourceConverter = processResourceConverter;
}
/**
* ( )
* @param assetCode
*/
@ResourceMapping(name = "Get_App_Process", method = "GET", uri = "/assets/{code}/apps/process", description = "배포된 앱의 프로세스 정보 조회")
@ResponseStatus(HttpStatus.OK)
@GetMapping("/assets/{assetCode}/apps/process")
public PageableResponse<ProcessResource> get(@PathVariable String assetCode,
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "20") int size) {
log.info("[get] assetCode = {}, page = {}, size = {}", assetCode, page, size);
Page<Process> process = processService.getAll(assetCode, page, size);
return PageableResponse.from(process, processResourceConverter::toResource);
}
}

View File

@ -1,15 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
record ProcessResource(
String assetCode,
Integer pid,
String name,
Integer cpu,
Integer memory,
Integer network,
Long processedAt,
OperationType operationType
) {
}

View File

@ -1,33 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import org.springframework.stereotype.Component;
@Component
public class ProcessResourceConverter {
public Process fromResource(ProcessResource resource) {
return Process.builder()
.assetCode(resource.assetCode())
.name(resource.name())
.pid(resource.pid())
.cpu(resource.cpu())
.memory(resource.memory())
.network(resource.network())
.processedAt(resource.processedAt())
.operationType(resource.operationType())
.build();
}
public ProcessResource toResource(Process process) {
return new ProcessResource(
process.getAssetCode(),
process.getPid(),
process.getName(),
process.getCpu(),
process.getMemory(),
process.getNetwork(),
process.getProcessedAt(),
process.getOperationType()
);
}
}