EC2→가상의 컴퓨터를 빌려줌 ex) 피시방 컴퓨터
VPC→공간을 빌려줌 정확히 말하면 네트워크 ex) 피시방
IAM → 루트 계정을 이용하면 탈취 위험이 크므로 정책(권한)을 위임한 계정, 공유 작업 시 사용
AWS GUI (콘솔) : 일일이 작업을 클릭으로 수행
AWS CLI : 커맨드 작업어로 작업을 수행, 작업 효율 좋음, 설치 필요,권한 인증 필요
테라폼 : AWS 쉽게 사용할 수 있게 해줌 (AWS CLI 명령어를 학습하는 것에 대한 어려움)
A(클라이언트) → B(테라폼) → C(AWS CLI) → D(AWS)
IAM 액세스 키: AWS CLI 에서 내가 누구인지 증명하는 키(권한 인증)
IAM 액세스 키를 이용해서 AWS CLI 에서 권한 인증함
먼저 테라폼을 이용해서 AWS 자원들을 쉽게 생성한다!
1. 테라폼 프로젝트 생성
main.tf
# Terraform이 사용할 Provider 정보 설정
terraform {
required_providers {
aws = {
source = "hashicorp/aws" # AWS Provider는 HashiCorp 공식 소스에서 가져옴
}
}
}
# AWS Provider 설정 (사용할 리전은 변수로 전달받음)
provider "aws" {
region = var.region
}
# ─────────────────────────────
# VPC 및 네트워크 구성
# ─────────────────────────────
# VPC 생성 (10.0.0.0/16 범위 사용)
resource "aws_vpc" "vpc_1" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true # DNS 해석 활성화
enable_dns_hostnames = true # 퍼블릭 IP 사용시 호스트네임 활성화
tags = {
Name = "${var.prefix}-vpc-1"
}
}
# 퍼블릭 서브넷 4개 생성 (가용 영역 A~D에 각각 하나씩)
resource "aws_subnet" "subnet_1" {
vpc_id = aws_vpc.vpc_1.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.region}a"
map_public_ip_on_launch = true # 인스턴스에 퍼블릭 IP 자동 할당
tags = {
Name = "${var.prefix}-subnet-1"
}
}
resource "aws_subnet" "subnet_2" {
vpc_id = aws_vpc.vpc_1.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.region}b"
map_public_ip_on_launch = true
tags = {
Name = "${var.prefix}-subnet-2"
}
}
resource "aws_subnet" "subnet_3" {
vpc_id = aws_vpc.vpc_1.id
cidr_block = "10.0.3.0/24"
availability_zone = "${var.region}c"
map_public_ip_on_launch = true
tags = {
Name = "${var.prefix}-subnet-3"
}
}
resource "aws_subnet" "subnet_4" {
vpc_id = aws_vpc.vpc_1.id
cidr_block = "10.0.4.0/24"
availability_zone = "${var.region}d"
map_public_ip_on_launch = true
tags = {
Name = "${var.prefix}-subnet-4"
}
}
# 인터넷 게이트웨이 생성 (외부 인터넷 통신을 위해 필요)
resource "aws_internet_gateway" "igw_1" {
vpc_id = aws_vpc.vpc_1.id
tags = {
Name = "${var.prefix}-igw-1"
}
}
# 라우팅 테이블 생성 (외부로 나가는 기본 경로 설정)
resource "aws_route_table" "rt_1" {
vpc_id = aws_vpc.vpc_1.id
route {
cidr_block = "0.0.0.0/0" # 모든 외부 트래픽
gateway_id = aws_internet_gateway.igw_1.id # IGW를 통해 나감
}
tags = {
Name = "${var.prefix}-rt-1"
}
}
# 서브넷을 라우팅 테이블에 연결 (퍼블릭 서브넷화)
resource "aws_route_table_association" "association_1" {
subnet_id = aws_subnet.subnet_1.id
route_table_id = aws_route_table.rt_1.id
}
resource "aws_route_table_association" "association_2" {
subnet_id = aws_subnet.subnet_2.id
route_table_id = aws_route_table.rt_1.id
}
resource "aws_route_table_association" "association_3" {
subnet_id = aws_subnet.subnet_3.id
route_table_id = aws_route_table.rt_1.id
}
resource "aws_route_table_association" "association_4" {
subnet_id = aws_subnet.subnet_4.id
route_table_id = aws_route_table.rt_1.id
}
# ─────────────────────────────
# 보안 그룹 (모든 포트 허용 – 개발용)
# ─────────────────────────────
resource "aws_security_group" "sg_1" {
name = "${var.prefix}-sg-1"
vpc_id = aws_vpc.vpc_1.id
ingress {
from_port = 0
to_port = 0
protocol = "all"
cidr_blocks = ["0.0.0.0/0"] # 모든 외부에서 접근 허용 (주의)
}
egress {
from_port = 0
to_port = 0
protocol = "all"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.prefix}-sg-1"
}
}
# ─────────────────────────────
# IAM Role 및 EC2 인스턴스 프로파일
# ─────────────────────────────
# EC2용 IAM 역할 생성 (EC2가 S3 및 SSM에 접근할 수 있게 함)
resource "aws_iam_role" "ec2_role_1" {
name = "${var.prefix}-ec2-role-2"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow"
}
]
}
EOF
}
# EC2 역할에 S3 전체 접근 권한 부여
resource "aws_iam_role_policy_attachment" "s3_full_access" {
role = aws_iam_role.ec2_role_1.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
# EC2 역할에 SSM 관리 권한 부여 (Session Manager 사용 가능)
resource "aws_iam_role_policy_attachment" "ec2_ssm" {
role = aws_iam_role.ec2_role_1.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
}
# EC2 인스턴스에 연결할 Instance Profile 생성
resource "aws_iam_instance_profile" "instance_profile_1" {
name = "${var.prefix}-instance-profile-2"
role = aws_iam_role.ec2_role_1.name
}
# ─────────────────────────────
# EC2에 전달할 User Data 스크립트 (Docker 기반 앱 자동 설치)
# ─────────────────────────────
locals {
ec2_user_data_base = <<-END_OF_FILE
#!/bin/bash
# 스왑 파일 4GB 생성
...
# Docker 설치 및 서비스 시작
...
# nginx-proxy-manager, redis, mysql Docker 컨테이너 실행
...
# MySQL 기동 대기 및 초기화
...
# GitHub Container Registry 로그인
...
END_OF_FILE
}
# ─────────────────────────────
# Amazon Linux 2023 최신 AMI 조회
# ─────────────────────────────
data "aws_ami" "latest_amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
}
# ─────────────────────────────
# EC2 인스턴스 생성 (도커 기반 서비스 실행)
# ─────────────────────────────
resource "aws_instance" "ec2_1" {
ami = data.aws_ami.latest_amazon_linux.id
instance_type = "t3.micro" # 프리티어 호환
subnet_id = aws_subnet.subnet_4.id
vpc_security_group_ids = [aws_security_group.sg_1.id]
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.instance_profile_1.name
tags = {
Name = "${var.prefix}-ec2-1"
}
root_block_device {
volume_type = "gp3"
volume_size = 12 # 루트 디스크 용량 (12GB)
}
# EC2 부팅 시 실행할 사용자 스크립트
user_data = <<-EOF
${local.ec2_user_data_base}
EOF
}
variables.tf
환경 변수 데이터 작업하는 파일
variable "prefix" {
description = "Prefix for all resources"
default = "dev"
}
variable "region" {
description = "region"
default = "ap-northeast-2"
}
variable "nickname" {
description = "nickname"
default = "jiyun"
}
secrets.tf
variable "password_1" {
description = "password_1"
default = "NEED_TO_INPUT" # 데이터베이스 비번
}
variable "github_access_token_1" {
description = "github_access_token_1, read:packages only"
default = "NEED_TO_INPUT" # github token
}
variable "github_access_token_1_owner" {
description = "github_access_token_1_owner"
default = "NEED_TO_INPUT" # github username
}
- VPC와 4개의 퍼블릭 서브넷
- 인터넷 게이트웨이 및 라우팅
- 모든 포트를 허용하는 보안 그룹 (테스트용)
- S3, SSM 권한이 부여된 IAM 역할과 인스턴스 프로파일
- 최신 Amazon Linux 기반의 EC2 인스턴스 1대
- 인스턴스 부팅 시 Docker 설치 + nginx-proxy-manager, Redis, MySQL 자동 실행 및 초기화
테라폼 명령어
- terraform init
- 라이브러리 다운로드
- 라이브러리 관련 소스코드가 바뀔 때 마다 실행해야 한다.
- terraform plan
- 실제 리소스 생성을 하는것은 아니고
- 현재 소스코드가 실행가능한지 검사
- terraform apply
- 리소스 생성
- yes 입력
- terraform destroy
- 리소스 삭제
- yes 입력
2. AWS 자원 생성 후
GITHUB ACTION 에서 해당 리포지터리에 대해서 쓰기 권한 가지도록
리포지터리 세팅에서 APPLICATION_SECRET_YML 시크릿 변수 생성
리포지터리 세팅에서 AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 시크릿 변수 생성
3. Dockerfile, .github/workflows/deploy.yml 작업
/backend/Dockerfile
# 첫 번째 스테이지: 빌드 스테이지
FROM gradle:jdk-21-and-23-graal-jammy AS builder
# 작업 디렉토리 설정
WORKDIR /app
# 소스 코드와 Gradle 래퍼 복사
COPY build.gradle.kts .
COPY settings.gradle.kts .
# 종속성 설치
RUN gradle dependencies --no-daemon
# 소스 코드 복사
COPY src src
# 애플리케이션 빌드
RUN gradle build --no-daemon
# 두 번째 스테이지: 실행 스테이지
FROM container-registry.oracle.com/graalvm/jdk:23
# 작업 디렉토리 설정
WORKDIR /app
# 첫 번째 스테이지에서 빌드된 JAR 파일 복사
COPY --from=builder /app/build/libs/*.jar app.jar
# 실행할 JAR 파일 지정
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
.github/workflows/deploy.yml
name: deploy
on:
push:
paths:
- ".github/workflows/**"
- "backend/src/**"
- "backend/build.gradle.kts"
- "backend/settings.gradle.kts"
- "backend/Dockerfile"
branches:
- main
- dev
jobs:
makeTagAndRelease:
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.create_tag.outputs.new_tag }}
steps:
- uses: actions/checkout@v4
- name: Create Tag
id: create_tag
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.create_tag.outputs.new_tag }}
release_name: Release ${{ steps.create_tag.outputs.new_tag }}
body: ${{ steps.create_tag.outputs.changelog }}
draft: false
prerelease: false
buildImageAndPush:
name: 도커 이미지 빌드와 푸시
needs: makeTagAndRelease
runs-on: ubuntu-latest
env:
DOCKER_IMAGE_NAME: juseyo ###!!!팀 프로젝트 이미지 네임!!!###
outputs:
DOCKER_IMAGE_NAME: ${{ env.DOCKER_IMAGE_NAME }}
OWNER_LC: ${{ env.OWNER_LC }}
steps:
- uses: actions/checkout@v4
- name: application-secret.yml 생성
env:
APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET_YML }}
run: echo "$APPLICATION_SECRET" > backend/src/main/resources/application-secret.yml
- name: Docker Buildx 설치
uses: docker/setup-buildx-action@v2
- name: 레지스트리 로그인
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: set lower case owner name
run: |
echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: 빌드 앤 푸시
uses: docker/build-push-action@v3
with:
context: backend
push: true
cache-from: type=registry,ref=ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:cache
cache-to: type=registry,ref=ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:cache,mode=max
tags: |
ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:${{ needs.makeTagAndRelease.outputs.tag_name }},
ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:latest
deploy:
runs-on: ubuntu-latest
needs: [buildImageAndPush]
env:
DOCKER_IMAGE_NAME: ${{ needs.buildImageAndPush.outputs.DOCKER_IMAGE_NAME }}
OWNER_LC: ${{ needs.buildImageAndPush.outputs.OWNER_LC }}
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: 인스턴스 ID 가져오기
id: get_instance_id
run: |
INSTANCE_ID=$(aws ec2 describe-instances \\
--filters "Name=tag:Name,Values=dev-ec2-1" "Name=instance-state-name,Values=running" \\
--query "Reservations[].Instances[].InstanceId" \\
--output text | tr '\\t' ',')
echo "INSTANCE_ID=$INSTANCE_ID" >> $GITHUB_ENV
echo "👉 선택된 인스턴스 ID: $INSTANCE_ID"
- name: AWS SSM Send-Command
uses: peterkimzz/aws-ssm-send-command@master
id: ssm
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
instance-ids: ${{ env.INSTANCE_ID }}
working-directory: /
comment: Deploy
command: |
docker pull ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:latest
docker stop app1 2>/dev/null
docker rm app1 2>/dev/null
docker run -d --network common --name app1 -p 8080:8080 \\
-v /etc/localtime:/etc/localtime:ro \\
-e TZ=Asia/Seoul \\
ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:latest
docker rmi $(docker images -f "dangling=true" -q)
main 이랑 dev에 push 할 때마다 이미지를 만들어서 배포 되게 함
트러블슈팅
1. DB 연결 문제
#17 102.6 BackendApplicationTests > contextLoads() FAILED
#17 102.6 java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
#17 102.6 Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1818
#17 102.6 Caused by: jakarta.persistence.PersistenceException at AbstractEntityManagerFactoryBean.java:431
#17 102.6 Caused by: org.hibernate.exception.JDBCConnectionException at SQLExceptionTypeDelegate.java:51
#17 102.6 Caused by: java.sql.SQLTransientConnectionException at HikariPool.java:686
#17 102.6 Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException at SQLError.java:165
#17 102.6 Caused by: com.mysql.cj.exceptions.CJCommunicationsException at Constructor.java:502
#17 102.6 Caused by: java.net.ConnectException at Net.java:-2
#17 102.7
#17 102.7 1 test completed, 1 failed
#17 102.8
#17 102.8 > Task :test FAILED
#17 102.8
#17 102.8 [Incubating] Problems report is available at: file:///app/build/reports/problems/problems-report.html
#17 102.8
#17 102.8 FAILURE: Build failed with an exception.
#17 102.8
#17 102.8 * What went wrong:
#17 102.8 Execution failed for task ':test'.
#17 102.8 > There were failing tests. See the report at: file:///app/build/reports/tests/test/index.html
#17 102.8
#17 102.8 * Try:
#17 102.8 > Run with --scan6 actionable tasks: 6 executed
#17 102.8 to get full insights.
#17 102.8
#17 102.8 BUILD FAILED in 1m 42s
#17 ERROR: process "/bin/sh -c gradle build --no-daemon" did not complete successfully: exit code: 1
------
> importing cache manifest from ghcr.io/treejh/juseyo:cache:
------
------
> [builder 7/7] RUN gradle build --no-daemon:
102.8
102.8 * What went wrong:
102.8 Execution failed for task ':test'.
102.8 > There were failing tests. See the report at: file:///app/build/reports/tests/test/index.html
102.8
102.8 * Try:
102.8 > Run with --scan6 actionable tasks: 6 executed
102.8 to get full insights.
102.8
102.8 BUILD FAILED in 1m 42s
------
Dockerfile:18
--------------------
16 |
17 | # 애플리케이션 빌드
18 | >>> RUN gradle build --no-daemon
19 |
20 | # 두 번째 스테이지: 실행 스테이지
--------------------
ERROR: failed to solve: process "/bin/sh -c gradle build --no-daemon" did not complete successfully: exit code: 1
Error: buildx failed with: ERROR: failed to solve: process "/bin/sh -c gradle build --no-daemon" did not complete successfully: exit code: 1
오류 해결 방법
: application-secret.yml에 데이터 베이스 url 을 ec2 인스턴스 IP 주소로 변경 하니 연결 됨
같은 컨테이너 안에서 연결 하는 줄 알고 컨테이너 이름으로 연결 했던 것이 원인이였음
2. 같은 인스턴트 ID 중복
Run peterkimzz/aws-ssm-send-command@master
/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/request.js:31
throw err;
^
ValidationException: 1 validation error detected: Value '[i-025a8c6afd2919edf i-086c55df89e244833]' at 'instanceIds' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 20, Member must have length greater than or equal to 10, Member must satisfy regular expression pattern: (^i-(\\w{8}|\\w{17})$)|(^mi-\\w{17}$), Member must not be null]
at Request.extractError (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/protocol/json.js:52:27)
at Request.callListeners (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
at Request.emit (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
at Request.emit (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/request.js:688:14)
at Request.transition (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/request.js:22:10)
at AcceptorStateMachine.runTo (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/state_machine.js:14:12)
at /home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/state_machine.js:26:10
at Request.<anonymous> (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/request.js:38:9)
at Request.<anonymous> (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/request.js:690:12)
at Request.callListeners (/home/runner/work/_actions/peterkimzz/aws-ssm-send-command/master/node_modules/aws-sdk/lib/sequential_executor.js:116:18) {
code: 'ValidationException',
time: 2025-05-27T00:20:17.008Z,
requestId: '2363a0ee-2345-42ae-98a9-351d8fa84d62',
statusCode: 400,
retryable: false,
retryDelay: 87.38455489952408
}
Node.js v20.19.1
배포할 인스턴트를 ID 로 찾아 가져오는데 인스턴트 ID가 같은 인스턴트가 두개라서 인스턴트를 못 찾아옴 → ID 변경
"Name=tag:Name,Values=dev-ec2-1” Values뒤에 바꾼 인스턴스 ID를 넣어주면 됨
3. 배포 후 빌드에서 DB 연결 문제
유저 권한도 있고 비밀번호도 확실히 입력 했는데 계속 DB 연결이 안됨
application-prod.yml이랑 application-secret.yml의 db 정보가 꼬여서 그런걸로 추정 application-prod.yml를 지우니 연결이 됨
또는
# application-prod.yml
spring:
profiles:
include: secret
위처럼 prod 프로파일이 secret을 포함하도록 설정되어 있어야 함.
4. 도커 이미지가 생성되면 해당 패키지에 접속해서 공개상태를 private 로 수정
다음은 도메인 설정!
'Project > JUSEYO' 카테고리의 다른 글
Vercel 배포 next.config.js 설정 (0) | 2025.05.28 |
---|---|
NPM 설정 (0) | 2025.05.28 |
도메인 등록 (0) | 2025.05.28 |