Skip to content

Commit

Permalink
Merge pull request #46 from APPS-sookmyung/dev
Browse files Browse the repository at this point in the history
[Feat] CI/CD 추가
  • Loading branch information
ajung7038 authored Aug 7, 2024
2 parents c02ec2d + 99e1384 commit e015220
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 3 deletions.
83 changes: 83 additions & 0 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# github repository actions 페이지에 나타날 이름
name: OUTFOOT CI/CD (Docker Image)

# event trigger
on:
push:
branches: [ "main" ]

permissions:
contents: read

jobs:
CI-CD:
runs-on: ubuntu-latest
steps:

# JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'

# gradle caching - 빌드 시간 향상
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# 환경별 yml 파일 생성 - application.yml
- name: make application.yml
if: |
contains(github.ref, 'main')
run: |
mkdir ./src/main/resources
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.YML }}" > ./application.yml
shell: bash

# .env 파일 생성
- name: make .env
if: |
contains(github.ref, 'main')
run: |
touch ./.env
echo "${{ secrets.YML }}" >> .env
shell: bash

# gradle build
- name: Build with Gradle
run: ./gradlew clean build -x test

# docker build & push to production
- name: Docker build & push
if: contains(github.ref, 'main')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_USERNAME }}/outfoot .
docker push ${{ secrets.DOCKER_USERNAME }}/outfoot
## deploy to production
- name: Deploy
uses: appleboy/ssh-action@master
id: deploy-main
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST }} # EC2 퍼블릭 IPv4 DNS
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }} # .pem 키
envs: GITHUB_SHA
script: |
sudo docker ps
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/outfoot
sudo docker run -d -p 8080:38080 --env-file=.env ${{ secrets.DOCKER_USERNAME }}/outfoot
sudo docker image prune -f
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM openjdk:17-jdk
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
COPY src/main/resources/application.yml application.yml
ENTRYPOINT ["java", "-Dspring.profiles.active=local", "-jar", "app.jar"]
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
19 changes: 16 additions & 3 deletions src/main/java/outfoot/outfootserver/HealthCheck.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import outfoot.outfootserver.common.response.BasicResponse;
import outfoot.outfootserver.common.response.ResponseUtil;
import outfoot.outfootserver.files.FileUploader;

@RestController
@Tag(name = "Health Check", description = "API 테스트")
@RequestMapping("/health")
@RequiredArgsConstructor
public class HealthCheck {
private final FileUploader fileUploader;
private final String TEST_KEY = "test/";

@GetMapping("/health")
@GetMapping
@Operation(summary = "서버 테스트용 API")
public BasicResponse<String> healthCheck() {
return ResponseUtil.success("health check");
}

@PostMapping("/file")
@Operation(summary = "서버 파일 업로드 테스트용 API")
public BasicResponse<String> healthCheckFile (@ModelAttribute MultipartFile file) {
String url = fileUploader.uploadFile(file, TEST_KEY);
return ResponseUtil.success(url);
}
}
27 changes: 27 additions & 0 deletions src/main/java/outfoot/outfootserver/files/FileUploader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package outfoot.outfootserver.files;

import com.amazonaws.services.s3.model.ObjectMetadata;
import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

public interface FileUploader {
String uploadFile(MultipartFile file, String path);
void deleteFile(String url, String path);

default ObjectMetadata createObjectMetaData(MultipartFile file) {
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType("image/png");
objectMetadata.setContentLength(file.getSize());
return objectMetadata;
}

default String generateKey(MultipartFile file, String path) {
if (file.getOriginalFilename() == null || file.getOriginalFilename().isBlank()) {
throw new S3IOException("파일 이름이 존재하지 않습니다.");
}
String fileName = file.getOriginalFilename();
String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
return path + UUID.randomUUID() + "." + ext;
}
}
36 changes: 36 additions & 0 deletions src/main/java/outfoot/outfootserver/files/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package outfoot.outfootserver.files;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3Client s3Client() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}

@Bean
public FileUploader fileUploader() {
return new TestFileUploader(s3Client());
}

}
54 changes: 54 additions & 0 deletions src/main/java/outfoot/outfootserver/files/S3FileUploader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package outfoot.outfootserver.files;

import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;

@Slf4j
@Component
@RequiredArgsConstructor
public class S3FileUploader implements FileUploader {

private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Override
public String uploadFile(MultipartFile file, String path) {
ObjectMetadata objectMetadata = createObjectMetaData(file);
String key = generateKey(file, path);

try (InputStream inputStream = file.getInputStream()){
amazonS3Client.putObject(new PutObjectRequest(bucket, key, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
log.error("이미지 업로드 IOException");
throw new S3IOException(e.getMessage());
}

return amazonS3Client.getUrl(bucket, key).toString();
}

@Override
public void deleteFile(String url, String path) {
try {
String fileName = url.substring(url.indexOf(path));
amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, fileName));
} catch (SdkClientException e) {
throw new S3IOException(e.getMessage());
} catch (NullPointerException e) {
throw new S3IOException("파일이 존재하지 않습니다.");
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/outfoot/outfootserver/files/S3IOException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package outfoot.outfootserver.files;

import lombok.Getter;

@Getter
public class S3IOException extends RuntimeException {

private final String errorCode = "S3FILE_IO_EXCEPTION";
private final String message;

public S3IOException() {
super();
this.message = "S3 파일 업로드 Exception";
}

public S3IOException(String message) {
super(message);
this.message = message;
}
}
55 changes: 55 additions & 0 deletions src/main/java/outfoot/outfootserver/files/TestFileUploader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package outfoot.outfootserver.files;

import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;

@Slf4j
@RequiredArgsConstructor
public class TestFileUploader implements FileUploader {

private final AmazonS3Client amazonS3Client;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

private static final String TEST_KEY = "test/";

@Override
public String uploadFile(MultipartFile file, String path) {
ObjectMetadata objectMetadata = createObjectMetaData(file);
String key = generateKey(file, TEST_KEY);

try (InputStream inputStream = file.getInputStream()) {
amazonS3Client.putObject(new PutObjectRequest(bucket, key, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
log.error("이미지 업로드 IOException");
throw new S3IOException(e.getMessage());
}

return amazonS3Client.getUrl(bucket, key).toString();
}

@Override
public void deleteFile(String url, String path) {
try {
String fileName = url.substring(url.indexOf(TEST_KEY));
amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, fileName));
} catch (SdkClientException e) {
throw new S3IOException(e.getMessage());
} catch (NullPointerException e) {
throw new S3IOException("파일이 존재하지 않습니다.");
}
}
}

0 comments on commit e015220

Please sign in to comment.