EC2 배포 과정 (테라 폼&깃 허브 액션)

2025. 5. 28. 21:48·Project/JUSEYO

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
'Project/JUSEYO' 카테고리의 다른 글
  • Vercel 배포 next.config.js 설정
  • NPM 설정
  • 도메인 등록
Jiyuuuuun
Jiyuuuuun
  • Jiyuuuuun
    Hello, World!
    Jiyuuuuun
  • 전체
    오늘
    어제
    • 분류 전체보기 (112)
      • TIL (56)
      • CS (17)
        • Network (4)
        • Algorithm (10)
      • JAVA (5)
      • Project (10)
        • HakPle (3)
        • JUSEYO (4)
      • Spring (2)
      • C (3)
      • C++ (16)
      • Snags (2)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    HTML
    juseyo
    nginx
    my_favorite_place
    front-end
    Kubernetes
    JPA
    springboot
    JDBC
    java
    부트캠프
    Docker
    javascript
    node.js
    멋쟁이사자처럼
    db
    hakple
    react
    SQL
    CSS
    back-end
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Jiyuuuuun
EC2 배포 과정 (테라 폼&깃 허브 액션)
상단으로

티스토리툴바