tail -n 100 /home/ec2-user/app.log
dnf를 이용해 시스템 패키지를 최신 상태로 유지 (yum보다 최신형 패
키지 관리자)
$ sudo dnf update -y
# Docker를 설치
$ sudo dnf install docker -y
# Docker 서비스를 시작 (현재 세션에서 활성화)
$ sudo systemctl start docker
# 시스템 부팅 시 Docker가 자동으로 실행되도록 설정
$ sudo systemctl enable docker
# 현재 사용자를 docker 그룹에 추가 (sudo 없이 docker 명령어 사용
가능하도록 설정)
$ sudo usermod -aG docker $USER
# Jenkins 사용자도 docker 그룹에 추가 (CI/CD 자동 배포 시 Jenkin
s가 docker 명령어 사용 가능하도록 설정)
$ sudo usermod -aG docker jenkins
# 현재 세션에 적용된 그룹 정보를 갱신 (로그아웃 없이 docker 그룹 권
한 적용)
$ newgrp docker
# Docker 버전 확인 (정상적으로 설치되었는지 확인용)
$ docker -v
pipeline {
agent any // 어떤 에이전트(실행 서버)에서든 실행 가능
tools {
maven 'maven 3.9.12' // Jenkins에 등록된 Maven 3.9.12를 사용
}
environment {
// 배포에 필요한 변수 설정
DOCKER_IMAGE = "demo-app" // 도커 이미지 이름
CONTAINER_NAME = "springboot-container" // 도커 컨테이너 이름
JAR_FILE_NAME = "app.jar" // 복사할 JAR 파일 이름
PORT = "8081" // 컨테이너에 연결한 포트
REMOTE_USER = "ec2-user" // 원격(spring) 서버 사용자
REMOTE_HOST = "15.165.103.223" // 원격(spring) 서버 IP(Public IP)
REMOTE_DIR = "/home/ec2-user/deploy" // 원격 서버에 파일 복사할 경로
SSH_CREDENTIALS_ID = "42b04877-7e3a-4e26-802e-582117df36ce" // Jenkins SSH 자격 증명 ID
}
stages {
stage('Git Checkout') {
steps { // steps : stage 안에서 실행할 실제 명령어
// Jenkins에 연결된 Git 저장소에서 최신 코드 체크 아웃
checkout scm
}
}
stage('Maven Build') {
steps {
// 테스트는 건너뛰고 Maven 빌드 수행
sh 'mvn clean package -DskipTests'
// sh '' : 리눅스 명령어 실행
}
}
stage('Prepare Jar') {
steps {
// 빌드 결과물인 JAR 파일을 지정한 이름(app.jar)으로 복사
sh 'cp target/demo-0.0.1-SNAPSHOT.jar ${JAR_FILE_NAME}'
}
}
}
}
———————————
// 원격 서버에 배포 디렉토리 생성 (없으면 새로 만듦)
sh "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${REMOTE_USER}@${REMOTE_HOST} \"mkdir -p ${REMOTE_DIR}\""
// JAR 파일과 Dockerfile을 원격 서버에 복사
sh "scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${JAR_FILE_NAME} Dockerfile ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
—————————
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${REMOTE_USER}@${REMOTE_HOST} << ENDSSH
cd ${REMOTE_DIR} || exit 1
docker rm -f ${CONTAINER_NAME} || true
docker build -t ${DOCKER_IMAGE} .
docker run -d --name ${CONTAINER_NAME} -p ${PORT}:${PORT} ${DOCKER_IMAGE}
ENDSSH
https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-supported-redistributable-version
Latest Supported Visual C++ Redistributable Downloads
This article provides download links for the latest Visual C++ Redistributable packages.
learn.microsoft.com
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.15-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description/>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- DB (MySQL, H2 for test) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>
package com.example.demo;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(unique = true, nullable = false, length = 100)
private String email;
}
—————————
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {}
—————————
package com.example.demo;
import java.util.List;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member save(Member member) { return memberRepository.save(member); }
public List<Member> findAll() { return memberRepository.findAll(); }
}
—————————
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
@GetMapping
public String list(Model model) {
model.addAttribute("members", memberService.findAll());
return "members/list";
}
@GetMapping("/new")
public String createForm(Model model) {
model.addAttribute("member", new Member());
return "members/createForm";
}
@PostMapping
public String create(@ModelAttribute Member member) {
memberService.save(member);
return "redirect:/members";
}
}
—————————
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 목록</title>
<style>
table {
width: 60%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
a {
display: inline-block;
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>회원 목록</h1>
<a href="/members/new">회원 등록</a>
<table>
<thead>
<tr>
<th>ID</th>
<th>이름</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr th:each="m : ${members}">
<td th:text="${m.id}"></td>
<td th:text="${m.name}"></td>
<td th:text="${m.email}"></td>
</tr>
</tbody>
</table>
</body>
</html>
—————————
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 등록</title>
<style>
form {
display: flex;
flex-direction: column;
width: 300px;
margin-top: 20px;
}
label {
margin-bottom: 10px;
}
input {
padding: 5px;
margin-top: 2px;
}
button {
width: 100px;
padding: 5px;
margin-top: 10px;
}
a {
display: inline-block;
margin-top: 15px;
}
</style>
</head>
<body>
<h1>회원 등록</h1>
<form th:action="@{/members}" th:object="${member}" method="post">
<label>
이름:
<input type="text" th:field="*{name}" placeholder="이름 입력"/>
</label>
<label>
Email:
<input type="email" th:field="*{email}" placeholder="Email 입력"/>
</label>
<button type="submit">등록</button>
</form>
<a href="/members">목록으로</a>
</body>
</html>
—————————
spring.datasource.url=jdbc:mysql://{Mysql Endpoint}:3306/demo?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=admin
spring.datasource.password=cicd1234!
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
—————————
stage('Inject Spring Config (Secret File)') {
steps {
withCredentials([file(credentialsId: env.SECRET_FILE_ID, variable: 'SPRING_CONFIG_FILE')]) {
sh """
echo "[INFO] Using secret file: $SPRING_CONFIG_FILE"
cp \$SPRING_CONFIG_FILE ./application-prod.properties
"""
}
}
}
—————————
—————————