일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- kubernetes
- terraform
- cloud armor
- 보안 규칙
- vm
- VAGRANT
- interconnect
- Java
- Terraform Cloud
- 후기
- Google Cloud Platform
- 자격증
- CentOS
- vpc peering
- devops
- direnv
- github
- Python
- pub/sub
- cloud
- Uptime Check
- MIG
- 우테캠
- gcp
- cloud function
- docker
- IAM
- cicd
- Clean Code
- AWS
- Today
- Total
EMD Blog
Spring Boot와 Jenkins를 이용한 ECR Push(1) 본문
회사 프로젝트 중 크롤링 Batch 서버를 구축해야할 일이 생겼다. 새롭게 들어온 팀원이 맡아서 진행하기로 했는데 기왕 하는 거 Docker Image push 까지 자동화 구축 후 진행을 하면 좀 더 편하게 개발을 하지 않을까 싶어 구축 과정을 간략하게 기록하려한다. 사내 서비스 운영은 인력 부족 및 금액 적인 부분 때문에 AWS에 많이 의존하고 있다. 현재 구축하려는 서비스도 MongoDB를 활용하려다가 데이터가 쌓이면 관리하기 힘들어질 것 같아 DynamoDB를 활용하기로 했다. 구축하고자 하는 개발 프로세스는 다음과 같다.
먼저 로컬에서 github repository를 remote하여 개발하다가 이슈 단위의 작업이 종료되면 Github의 Task branch에 Push 후 develop branch에 pull request한다. pull request시 jenkins에 webhook을 보내고 Jenkins에서는 pull request하고자 하는 애플리케이션의 통합테스트를 진행하고 진행결과를 Github에 반환해 merge 가능 여부를 체크한다. 통합테스트가 통과해 squash commit이 가능하면 내부적으로 코드 리뷰 후 squash merge를 진행한다. 그러면 다시 github으로 webhook를 보내 jenkins에서는 코드를 불러와 통합테스트를 진행하고 빌드 후 이미지화해서 ECR에 Push한다.
개발환경은 다음과 같다
- JDK 1.8
- Springboot 2.5.0
- Gradle
위 처럼 세팅 했다면 이제 DynamoDB와 연동해보도록 하자
DynamoDB에 대한 설명과 사용법은 AWS개발자 안내서에 상세하게 설명되어있으니 참고하도록 하자
DynamoDB는 기본 적으로 사용량에 따른 요금을 부과하게 되어있다. 하지만 테스트용으로 dynamodb-local을 지원하니 개발환경에서는 DynamoDB-local을 사용하고 테스트 환경에서는 실제 dynamodb를 사용하기로 한다. 그럼 docker-compose.yml을 먼저 작성하자
version: "3.7"
services:
dynamodb:
container_name: dynamodb
image: amazon/dynamodb-local:latest
ports:
- 8000:8000
restart: always
command: ["-jar", "DynamoDBLocal.jar", "-sharedDb"]
dynamodb-local을 실행시켜주는 간단한 compose 파일이다. -sharedDb 옵션을 줘서 dynamodb가 종료되더라도 데이터가 사라지지 않도록 했으며 -sharedDb 대신 -inMemory옵션을 주게 되면 dynamodb 종료시 데이터가 삭제된다. 그리고 터미널에서 쉽게 조작할 수 있도록 aws cli를 설치하도록 하자
여기까지 진행했으면 dynamoDB를 실행해보자
$ docker-compose up -d
Creating dynamodb ... done
dynamodb-local이 실행되었으면 aws cli 명령어를 통해 테이블을 생성해보도록 하자.
근데 테이블을 생성하기 전에 간단하게 스키마를 구성해보도록 하자. 임시로 강아지라는 테이블을 만들기로 하고 id, 이름, 나이, 종류, 생성일 정도로 구성되어있다고 하자. 그리고 그 중 PK는 id, 검색용 보조 index로 종류, 생성일을 인덱싱하자.
$ aws dynamodb create-table \
--table-name Dog \
--attribute-definitions \
AttributeName=id,AttributeType=S \
AttributeName=dogType,AttributeType=S \
AttributeName=createdAt,AttributeType=S \
--key-schema \
AttributeName=id,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
--global-secondary-indexes \
"[ { \"IndexName\": \"byDogType\", \"KeySchema\": [ {\"AttributeName\": \"dogType\",\"KeyType\":\"HASH\"}, {\"AttributeName\": \"createdAt\",\"KeyType\":\"RANGE\"} ], \"Projection\": { \"ProjectionType\": \"ALL\" }, \"ProvisionedThroughput\": { \"ReadCapacityUnits\":1, \"WriteCapacityUnits\":1 } } ]" \
--endpoint-url http://localhost:8000
{
"TableDescription": {
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
},
{
"AttributeName": "dogType",
"AttributeType": "S"
},
{
"AttributeName": "createdAt",
"AttributeType": "S"
}
],
"TableName": "Dog",
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"TableStatus": "ACTIVE",
"CreationDateTime": "2021-05-29T23:37:41.479000+09:00",
"ProvisionedThroughput": {
...
위 명령어를 통해 table을 생성할 수 있다. 보면은 이름과 나이에 대한 스키마를 명시하지 않았다. Nosql이기 때문에 동적 스키마를 사용하므로 컬럼 종류가 늘어나면 자동으로 수평확장 된다. 그래서 id와 dogType(종류), createdAt만 명시해 놓은 것이다. AttributeType의 경우 컬럼 타입으로 S면 문자열, N이면 숫자형이다. 그리고 --key-schema option을 통해 id를 키본키로 설정했으며 기본키는 꼭 한개가 아니여도 된다. 마지막으로 --global-secondary-indexes 옵션을 통해 보조 인덱스를 설정했다. dogType과 createdAt을 묶어 byDogType이라는 이름으로 설정 했으며 dogType은 검색(HASH), createdAt은 정렬(RANGE)에 사용된다.
위 명령어 옵션 중에 --provisioned-throughput이 있는데 dynamodb는 프로비저닝 모드와 온디멘드 모드로 나뉘며 프로비저닝 모드의 경우 처리량에 대한 용량을 선택해야한다. 처리량에 대한 Auto Scaling은 제공하지만 초기설정 값은 지정해줘야한다. 하지만 local의 경우 이 값에 상관없이 작동해 그냥 최소값인 1로 설정하면 된다. 이 값에 대한 자세한 설명은 홈페이지 설명을 참고해보자.
이렇게 테이블을 생성했다면 잘 생성되었는지 조회해보자
$ aws dynamodb list-tables --endpoint-url http://localhost:8000
{
"TableNames": [
"Dog"
]
}
테이블이 잘 조회되는 것을 확인할 수 있다. 참고로 테이블 생성이나 조회시 endpoint를 설정해주지 않으면 local이 아닌 AWS에 생성되니 localhost로 명시해주자.
이렇게 dynamodb의 테이블 생성과 조회를 해보았다. aws에 대한 다른 dynamodb 사용 방법은 아래 경로를 참고하자.
이제 dynamodb의 작동을 확인 했으니 spring boot와 연동해보도록 하자. 먼저 build gradle에 의존성을 몇개 추가하자.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '1.11.1000'
implementation group: 'com.github.derjust', name: 'spring-data-dynamodb', version: '5.1.0'
implementation group: 'org.assertj', name: 'assertj-core', version: '3.8.0'
implementation 'junit:junit:4.12'
}
추가된 것은 aws-java-sdk-dynamodb와 spring-data-dynamodb, assertj-core이다. aws-java-sdk-dynamodb는 dynamodb를 사용하기 위한 것이고 spring-data-dynamodb는 쿼리메서드를 사용하기 위해, assertj-core는 Test코드를 위해 추가했다.
그럼 /src/main/resource/application.properties를 application.yml로 변경 후 어플리케이션에 대한 설정 정보를 입력해주자
server:
port: 8080
amazon:
dynamodb:
endpoint: "http://localhost:8000"
region: "ap-northeast-2"
aws:
accesskey: "key"
secretkey: "key2"
서버 Port를 8080로 설정하고 amazon dynamodb의 endpoint를 local, region을 서울로 설정했다. accesskey와 secretkey는 local이기 때문에 따로 값을 설정하지 않아도 된다.
쿼리메서드 사용을 위해 com.emadam.study에 config package를 생성하고 DynamoDBConfig.java를 생성하자.
DynamoDBConfig.java
package com.emadam.study.config;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter;
import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.TimeZone;
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.emadam.study.domain")
public class DynamoDBConfig {
@Value("${amazon.dynamodb.endpoint}")
private String amazonDynamoDBEndpoint;
@Value("${amazon.dynamodb.region}")
private String amazonDynamoDbRegion;
@Value("${amazon.aws.accesskey}")
private String amazonAwsAccessKey;
@Value("${amazon.aws.secretkey}")
private String amazonAwsSecretKey;
@Bean(name = "amazonDynamoDB")
public AmazonDynamoDB amazonDynamoDB() {
AWSStaticCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(new BasicAWSCredentials(amazonAwsAccessKey, amazonAwsSecretKey));
AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(amazonDynamoDBEndpoint, amazonDynamoDbRegion);
return AmazonDynamoDBClientBuilder.standard()
.withCredentials(credentialsProvider)
.withEndpointConfiguration(endpointConfiguration).build();
}
public static class LocalDateTimeConverter implements DynamoDBTypeConverter<Date, LocalDateTime> {
@Override
public Date convert(LocalDateTime localDateTime) {
return Date.from(localDateTime.toInstant(ZoneOffset.UTC));
}
@Override
public LocalDateTime unconvert(Date date) {
return date.toInstant().atZone(TimeZone.getDefault().toZoneId()).toLocalDateTime();
}
}
}
위에서 부터 amazonDynamoDBEndpoint, amazonDynamoDBRegion, amazonAwsAccessKey, amazonAwsSecretKey에 application.yml의 값을 매핑하고 이 설정값을 들을 통해 amazonDynamoDB 메서드에서 dynamodb client객체를 생성 후 amazonDynamoDB라는 이름의 Bean으로 만든다. 마지막으로 LocalDateTimeConverter는 dynamodb sdk가 기본 Date 객체만 지원하기 때문에 저장할때는 Date, 가져올때는 LocalDateTime으로 변환되도록 하는 Converter이다.
그럼 이제 앞서 구상한 테이블 스키마에 대한 Entity 객체를 생성하자.
Dog.java
package com.emadam.study.domain;
import com.amazonaws.services.dynamodbv2.datamodeling.*;
import com.emadam.study.config.DynamoDBConfig;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@DynamoDBTable(tableName = "Dog")
public class Dog {
@DynamoDBHashKey(attributeName = "id")
@DynamoDBAutoGeneratedKey
private String id;
@DynamoDBAttribute
private String name;
@DynamoDBAttribute
private Integer age;
@DynamoDBAttribute
@DynamoDBIndexHashKey(globalSecondaryIndexName = "byDogType")
private String dogType;
@DynamoDBAttribute
@DynamoDBIndexRangeKey(globalSecondaryIndexName = "byDogType")
@DynamoDBTypeConverted(converter = DynamoDBConfig.LocalDateTimeConverter.class)
private LocalDateTime createdAt;
@Builder
public Dog(
String id,
String name,
Integer age,
String dogType
) {
this.id = id;
this.name = name;
this.age = age;
this.dogType = dogType;
this.createdAt = LocalDateTime.now();
}
}
lombok을 통해 getter와 setter를 생성해주고 기본 생성자를 생성해준다. 그다음 @DynamoDBTable 어노테이션을 통해 table을 지정해주고 각 항목 별로 어노테이션을 추가해준다.
@DynamoDBHashKey : keyType이 Hash인 기본키
@DynamoDBAutoGeneratedKey : 값을 자동으로 생성
@DynamoDBAttribute: DynamoDB table의 항목임을 명시
@DynamoDBIndexHashKey: keyType이 Hash인 second index 키
@DynamoDBIndexRangeKey: keyType이 Range인 second index 키
@DynamoDBTypeConverted: 사용할 컨버터 class를 명시
그 다음 Repository 객체를 생성해 실제 쿼리 메서드를 생성해보자.
DogRepository.java
package com.emadam.study.domain;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface DogRepository extends CrudRepository<Dog, String> {
List<Dog> findAllByDogTypeOOrderByCreatedAtDesc(String dogType);
}
CrudRepository를 상속받았으며 CrudRepository를 상속받는 다른 Repository객체들이 많이 존재하므로 자신한테 맞는 객체를 찾아 상속받아서 사용해도 된다. 그 다음 item중 dogType에 해당하는 item을 찾아 생성일 기준으로 정렬하는 쿼리 메서드를 생성한다. 쿼리 메서드의 경우 자바 ORM 표준 JPA 프로그래밍 책을 참고하거나 이쪽을 참고하여 상황에 맞게 작성하자.
그 다음 작성한 쿼리 메서드를 테스트할 Test case를 작성한다.
DogRepositoryTests.java
package com.emadam.study.domain;
import com.emadam.study.config.DynamoDBConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.Assert;
@SpringBootTest(classes = {DynamoDBConfig.class})
public class DogRepositoryTests {
private @Autowired DogRepository dogRepository;
private final String[] DOG_TYPE = {"말티즈", "푸들", "리트리버", "치와와", "불독"};
@Test
void create() {
Dog dog = dogRepository.save(Dog.builder()
.name("우디")
.age(3)
.dogType(DOG_TYPE[0])
.build()
);
Assert.assertEquals(dog.getName(), "우디");
Assert.assertEquals(dog.getAge(), Integer.valueOf(3));
Assert.assertEquals(dog.getDogType(), "말티즈");
}
}
테스트에 성공했다면 나머지 수정, 삭제, 조회에 관한 테스트도 작성하자
조회
@Test
void findCreated(){
String id = dogRepository.save(Dog.builder()
.name("모냐")
.age(10)
.dogType(DOG_TYPE[1])
.build()
).getId();
Dog dog = dogRepository.findById(id)
.orElseThrow(() -> new DogNotFoundException(id));
Assert.assertEquals(dog.getName(), "모냐");
Assert.assertEquals(dog.getAge(), Integer.valueOf(10));
Assert.assertEquals(dog.getDogType(), "푸들");
}
수정
@Test
void update(){
String id = dogRepository.save(Dog.builder()
.name("폴리")
.age(7)
.dogType(DOG_TYPE[2])
.build()
).getId();
Dog dog = dogRepository.findById(id)
.orElseThrow(() -> new DogNotFoundException(id));
dog.setName("마루");
dog.setAge(17);
Dog modifiedDog = dogRepository.save(dog);
Assert.assertEquals(modifiedDog.getName(), "마루");
Assert.assertEquals(modifiedDog.getAge(), Integer.valueOf(17));
Assert.assertEquals(modifiedDog.getDogType(), "리트리버");
}
삭제
@Test
void deleteCreated() {
Dog dog = dogRepository.save(Dog.builder()
.name("앵그리")
.age(5)
.dogType(DOG_TYPE[3])
.build()
);
dogRepository.delete(dog);
BDDAssertions.thenThrownBy(() -> dogRepository.findById(dog.getId())
.orElseThrow(() -> new DogNotFoundException(dog.getId())))
.isExactlyInstanceOf(DogNotFoundException.class);
}
여러 Item 조회
@Test
void findDogs() {
int size = 8;
IntStream.range(0, size).forEach(i -> dogRepository.save(Dog.builder()
.name("강쥐" + i)
.age(3 + 2 * i)
.dogType(DOG_TYPE[4])
.build()
));
List<Dog> dogs = dogRepository
.findAllByDogTypeOrderByCreatedAtDesc(DOG_TYPE[4]);
BDDAssertions.then(dogs.size()).isEqualTo(size);
IntStream.range(1, size).forEach(i -> {
Dog prev = dogs.get(i - 1);
Dog next = dogs.get(i);
BDDAssertions.then(prev.getCreatedAt().isAfter(next.getCreatedAt())).isTrue();
});
}
이렇게 CRUD에 대한 테스트 코드가 작성되었다. 하지만 이대로만 진행하면 테스트를 여러번 했을 때 findDogs 테스트에서 실패하게 되는데 이유는 DOG_TYPE[4]에 대한 item수가 미리 설정한 size를 넘어가게 되기 때문이다. 그렇기 때문에 테스트가 종료되면 DynamoDB의 모든 item을 삭제하는 로직을 추가한다.
DogRepository.java
package com.emadam.study.domain;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface DogRepository extends CrudRepository<Dog, String> {
List<Dog> findAllByDogTypeOrderByCreatedAtDesc(String dogType);
void deleteAllByDogType(String dogType);
}
DogRepositoryTests.java
@SpringBootTest(classes = {DynamoDBConfig.class})
public class DogRepositoryTests {
private @Autowired DogRepository dogRepository;
private final String[] DOG_TYPE = {"말티즈", "푸들", "리트리버", "치와와", "불독"};
@AfterEach
void deleteAll() {
for(String dogType : DOG_TYPE) {
dogRepository.deleteAllByDogType(dogType);
}
}
@Test
void create() {
...
}
...
}
dogType을 기준으로 item을 삭제하는 쿼리 메서드를 생성했다. 그리고 AfterEach를 통해 테스트가 종료될때마다 Dog table의 모든 item을 삭제하도록 했다. 만약에 테스트를 시작하기 전에 작업을 하고 싶다면 BeforeEach 어노테이션을 사용하면 된다.
이제 테스트르 작성했으니 Docker image생성을 위한 Dockerfile과 gradle.build를 작성해보자
DogRepositoryTests.java
FROM openjdk:8-jre-alpine
WORKDIR /root
ARG buildDir=build/unpack
COPY ${buildDir}/lib BOOT-INF/lib
COPY ${buildDir}/app .
CMD java org.springframework.boot.loader.JarLauncher
DogRepositoryTests.java
...
task moveLib {
doLast {
def unpackDir = "$buildDir/unpack"
ant.move(file: "${unpackDir}/app/BOOT-INF/lib", toFile: "${unpackDir}/lib")
}
}
task unpackJar(type: Copy) {
def unpackDir = "$buildDir/unpack"
delete unpackDir
from zipTree(jar.getArchiveFile())
into "$unpackDir/app"
finalizedBy moveLib
}
build {
finalizedBy unpackJar
}
Dockerfile과 build.gradle에 Dockerfile 최적화 과정은 양권성님의 블로그를 참고하였으니 궁금하면 확인해보자.
그럼 docker image가 잘 생성되는지 확인해보자
$ ./gradlew check
$ ./gradlew build
$ docker build -t dog:1.0 .
$ docker images
docker image가 잘 생성된 것을 볼 수 있다.
그럼 이제 github, jenkins를 사용해서 빌드, 테스트, Push해보도록 하자.
먼저 서버에 Jenkins를 설치해주자. 서버에는 docker와 docker compose가 설치되어 있어야 하며, jenkins는 docker-compose를 통해 설치하자.
Dockerfile
FROM jenkins/jenkins:latest
USER $USER
RUN curl -s https://get.docker.com/ | sh
RUN curl -L "https://github.com/docker/compose/releases/download/1.28.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && \
chmod +x /usr/local/bin/docker-compose && \
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
RUN curl -sL bit.ly/ralf_dcs -o ./dcs && \
chmod 755 dcs && \
mv dcs /usr/local/bin/dcs
RUN usermod -aG docker jenkins
docker-compose.yml
version: "3.9"
services:
jenkins:
container_name: jenkins
privileged: true
build:
context: ./
ports:
- "8080:8080"
- "50000:50000"
volumes:
- /var/jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=Asia/Seoul
Dockerfile과 docker-compose 파일을 home에 생성했다. 우리가 할 것은 Jenkins를 통해 docker를 실행해야하기 때문에 별도의 Dockerfile을 만들어 jenkins와 docker가 한 container내에 같이 설치되도록 했다. 그리고 host에 설치된 docker.sock과 mount하여 container내에서 실행하는 docker 명령어가 container외부에서도 실행되도록 설정했다. 그럼 docker-compose를 실행하자
$ docker-compose up -d
조금 기다린 후 jenkins가 시작되면 관리자 계정정보를 설정하고 기본 플러그인만 설치하자.
다음 포스팅에는 본격적으로 Github과 연동하도록하겠다.
'CICD' 카테고리의 다른 글
Github Actions Terraform Output 이슈 (0) | 2022.09.04 |
---|---|
Spring Boot와 Jenkins를 이용한 ECR Push(2) (0) | 2021.07.14 |
Container를 이용한 PHP 개발환경 가상화와 배포 자동화를 해보자 (2) (0) | 2021.04.29 |
Container를 이용한 PHP 개발환경 가상화와 배포 자동화를 해보자 (1) (0) | 2021.04.08 |