diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..63800188 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,219 @@ +name: Deploy to EC2 + +env: + IMAGE_REPOSITORY: fivelogic + CONTAINER_NAME: spring-boot + EC2_INSTANCE_TAG_NAME: devcos-team10-main + DOCKER_NETWORK: app-network + BACKEND_DIR: back + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + packages: write + +defaults: + run: + shell: bash + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + - name: Java 21 설정 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: 테스트 실행 + run: | + cd back + chmod +x gradlew + ./gradlew test + + - name: 테스트 결과 업로드 + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: back/build/reports/tests/test/ + + makeTagAndRelease: + needs: test + if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/chore/107') && github.event_name == 'push' + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.create_tag.outputs.new_tag }} + + steps: + - name: 코드 체크아웃 + 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 + 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 + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + - name: .env 파일 생성 + env: + DOT_ENV: ${{ secrets.DOT_ENV }} + run: | + mkdir -p "${{ env.BACKEND_DIR }}" + printf "%s" "${DOT_ENV}" > "${{ env.BACKEND_DIR }}/.env" + + - name: Docker Buildx 설치 + uses: docker/setup-buildx-action@v3 + + - name: 레지스트리 로그인 + uses: docker/login-action@v3 + 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@v6 + with: + context: ${{ env.BACKEND_DIR }} + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ghcr.io/${{ env.OWNER_LC }}/${{ env.IMAGE_REPOSITORY }}:${{ needs.makeTagAndRelease.outputs.tag_name }} + ghcr.io/${{ env.OWNER_LC }}/${{ env.IMAGE_REPOSITORY }}:latest + + deploy: + name: 배포 + needs: [makeTagAndRelease, buildImageAndPush] + runs-on: ubuntu-latest + + steps: + - name: AWS 자격 구성 + 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 가져오기 + run: | + INSTANCE_ID=$(aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=${{ env.EC2_INSTANCE_TAG_NAME }}" "Name=instance-state-name,Values=running" \ + --query "Reservations[].Instances[].InstanceId" --output text) + + if [[ -z "${INSTANCE_ID}" || "${INSTANCE_ID}" == "None" ]]; then + echo "❌ 실행 중인 EC2 인스턴스를 찾을 수 없습니다." + exit 1 + fi + + echo "✅ EC2 인스턴스 발견: ${INSTANCE_ID}" + echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}" + + - name: AWS SSM Send-Command로 배포 + uses: peterkimzz/aws-ssm-send-command@master + 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: /home/ssm-user + comment: Deploy Spring Boot Application + command: | + set -Eeuo pipefail + + LOG="/tmp/deploy-$(date +%Y%m%d_%H%M%S).log" + exec > >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG") + exec 2> >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG" >&2) + + echo "🚀 배포 시작..." + + source /etc/environment || true + + OWNER_LC="${{ github.repository_owner }}" + OWNER_LC="${OWNER_LC,,}" + IMAGE_TAG='${{ needs.makeTagAndRelease.outputs.tag_name }}' + IMAGE_REPOSITORY='${{ env.IMAGE_REPOSITORY }}' + IMAGE="ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:${IMAGE_TAG}" + CONTAINER_NAME="${{ env.CONTAINER_NAME }}" + + echo "📦 이미지: ${IMAGE}" + echo "📦 컨테이너: ${CONTAINER_NAME}" + + cd /home/ssm-user/WEB6_8_FiveLogic_BE || exit 1 + + echo "📥 Docker 이미지 다운로드 중..." + docker pull $IMAGE + + echo "🛑 기존 컨테이너 중지 중..." + docker-compose stop $CONTAINER_NAME || true + docker-compose rm -f $CONTAINER_NAME || true + + sed -i "s|image:.*${IMAGE_REPOSITORY}.*|image: ${IMAGE}|g" docker-compose.yml + + echo "🚀 새 컨테이너 시작 중..." + docker-compose up -d $CONTAINER_NAME + + echo "🏥 헬스체크 중..." + for i in {1..30}; do + if docker exec $CONTAINER_NAME curl -f http://localhost:8080/health > /dev/null 2>&1; then + echo "✅ 서버 정상 구동!" + break + fi + echo "대기 중... ($i/30)" + sleep 2 + done + + echo "📊 컨테이너 상태:" + docker-compose ps $CONTAINER_NAME + + echo "📋 최근 로그:" + docker-compose logs --tail=50 $CONTAINER_NAME + + echo "🧹 오래된 이미지 정리 중..." + { + docker images --format '{{.Repository}}:{{.Tag}}' \ + | grep -F "ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:" \ + | grep -v -F ":${IMAGE_TAG}" \ + | grep -v -F ":latest" \ + | xargs -r docker rmi + } || true + + echo "✅ 배포 완료!" \ No newline at end of file diff --git a/.gitignore b/.gitignore index d2f225f8..0c10730b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,11 @@ cookies.txt .DS_Store ### QueryDSL Q클래스 ### -**/generated/ \ No newline at end of file +**/generated/ + +.terraform +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup +secrets.tf +gradle.properties diff --git a/back/.env.default b/back/.env.default index a3242c8b..56fb874d 100644 --- a/back/.env.default +++ b/back/.env.default @@ -1 +1,3 @@ CUSTOM__JWT__SECRET_KEY=NEED_TO_SET +DB_USERNAME=NEED_TO_SET +DB_PASSWORD=NEED_TO_SET diff --git a/back/Dockerfile b/back/Dockerfile new file mode 100644 index 00000000..b86cb32d --- /dev/null +++ b/back/Dockerfile @@ -0,0 +1,51 @@ +# ========================================== +# 첫 번째 스테이지: 빌드 스테이지 +# ========================================== +FROM gradle:8.5-jdk21 AS builder + +WORKDIR /app + +# Gradle 설정 파일 먼저 복사 (캐시 최적화) +COPY build.gradle.kts . +COPY settings.gradle.kts . + +# Gradle wrapper 복사 +COPY gradle gradle +COPY gradlew . + +RUN chmod +x gradlew + +# 의존성 다운로드 (캐시 활용) +RUN ./gradlew dependencies --no-daemon + +# 소스 코드 및 .env 복사 +COPY .env . +COPY src src + +# 애플리케이션 빌드 +RUN ./gradlew build --no-daemon -x test + +# ========================================== +# 두 번째 스테이지: 실행 스테이지 +# ========================================== +FROM eclipse-temurin:21-jre-alpine + +WORKDIR /app + +# 첫 번째 스테이지에서 빌드된 JAR 파일 복사 +COPY --from=builder /app/build/libs/*.jar app.jar + +# 첫 번째 스테이지에서 .env 파일 복사 +COPY --from=builder /app/.env .env + +# 환경변수 기본값 +ENV SPRING_PROFILES_ACTIVE=prod + +# 헬스체크용 curl 설치 +RUN apk add --no-cache curl + +# 포트 노출 +EXPOSE 8080 + +# 실행 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/back/build.gradle.kts b/back/build.gradle.kts index 506d7da5..8f5cd7bd 100644 --- a/back/build.gradle.kts +++ b/back/build.gradle.kts @@ -1,5 +1,3 @@ -import org.gradle.kotlin.dsl.implementation - plugins { java id("org.springframework.boot") version "3.5.5" @@ -11,9 +9,8 @@ version = "0.0.1-SNAPSHOT" description = "back" java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } configurations { @@ -68,6 +65,8 @@ dependencies { implementation ("software.amazon.awssdk:s3:2.25.0") implementation ("org.springframework.kafka:spring-kafka") + + runtimeOnly("com.mysql:mysql-connector-j") } tasks.withType { diff --git a/back/src/main/java/com/back/domain/member/member/verification/InMemoryVerificationCodeStore.java b/back/src/main/java/com/back/domain/member/member/verification/InMemoryVerificationCodeStore.java index d9b45b3b..2374e6dd 100644 --- a/back/src/main/java/com/back/domain/member/member/verification/InMemoryVerificationCodeStore.java +++ b/back/src/main/java/com/back/domain/member/member/verification/InMemoryVerificationCodeStore.java @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap; @Component -@Profile({"test", "dev"}) +@Profile({"test", "dev", "prod"}) @Slf4j public class InMemoryVerificationCodeStore implements VerificationCodeStore { private final Map store = new ConcurrentHashMap<>(); diff --git a/back/src/main/java/com/back/global/kafka/KafkaConfig.java b/back/src/main/java/com/back/global/kafka/KafkaConfig.java index 6a24b0e9..68f006a7 100644 --- a/back/src/main/java/com/back/global/kafka/KafkaConfig.java +++ b/back/src/main/java/com/back/global/kafka/KafkaConfig.java @@ -2,6 +2,7 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.annotation.EnableKafka; @@ -16,11 +17,14 @@ @Configuration public class KafkaConfig { + @Value("${spring.kafka.bootstrap-servers:localhost:9092}") + private String bootstrapServers; + @Bean public ConsumerFactory consumerFactory() { Map props = new HashMap<>(); - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); props.put(ConsumerConfig.GROUP_ID_CONFIG, "spring-boot-group"); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); diff --git a/back/src/main/resources/application-prod.yml b/back/src/main/resources/application-prod.yml index 403ac94c..6830ed1f 100644 --- a/back/src/main/resources/application-prod.yml +++ b/back/src/main/resources/application-prod.yml @@ -1,11 +1,30 @@ spring: autoconfigure: exclude: - datasource: #임시 - url: jdbc:h2:mem:proddb - username: sa - password: - driver-class-name: org.h2.Driver + - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration + datasource: + url: jdbc:mysql://devcos-team10-mysql.czvbgr7hie3i.ap-northeast-2.rds.amazonaws.com:3306/fivelogic?useSSL=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + auto-commit: false + maximum-pool-size: 10 + minimum-idle: 5 + connection-timeout: 30000 + jpa: + database: mysql + database-platform: org.hibernate.dialect.MySQLDialect + hibernate: + ddl-auto: update + show-sql: false + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 100 + kafka: + bootstrap-servers: kafka:29092 + logging: level: org.hibernate.orm.jdbc.bind: INFO @@ -13,8 +32,12 @@ logging: org.springframework.transaction.interceptor: INFO com.back: INFO custom: - site: # 임시입니다 - cookieDomain: fivelogic.com - frontUrl: "https://fivelogic.vercel.app" - backUrl: "https://api.fivelogic.com" - name: fivelogic \ No newline at end of file + site: + cookieDomain: ".jobmate.info" # 도메인 설정 + frontUrl: "https://jobmate.info" + backUrl: "https://api.jobmate.info" # HTTPS로 변경 + cookie: + httpOnly: true + secure: true # HTTPS에서는 true로 변경 + sameSite: "Lax" + maxAge: "#{60*60*24}" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e139969c..53301ffd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: environment: - ZOOKEEPER_CLIENT_PORT=2181 - ZOOKEEPER_TICK_TIME=2000 + networks: + - app-network kafka: image: confluentinc/cp-kafka:7.0.1 @@ -23,6 +25,8 @@ services: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + networks: + - app-network minio: image: minio/minio @@ -42,21 +46,45 @@ services: - minio-data:/data depends_on: - kafka + networks: + - app-network + minio-consumer: - build: . + build: + context: . + dockerfile: Dockerfile container_name: minio-consumer depends_on: - kafka - minio environment: - KAFKA_TOPIC=s3-events - - KAFKA_BOOTSTRAP=kafka:29092 # 컨테이너 네트워크 내부 주소 + - KAFKA_BOOTSTRAP=kafka:29092 - MINIO_ENDPOINT=http://minio:9000 - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin - DOWNLOAD_DIR=/downloads volumes: - ./downloads:/downloads + networks: + - app-network + + spring-boot: + image: ghcr.io/prgms-web-devcourse-final-project/fivelogic:latest + container_name: spring-boot + depends_on: + - kafka + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=prod + networks: + - app-network + restart: unless-stopped volumes: minio-data: + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf new file mode 100644 index 00000000..a052e53c --- /dev/null +++ b/infra/terraform/main.tf @@ -0,0 +1,367 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} + +provider "aws" { + region = var.region +} + +# VPC 설정 +resource "aws_vpc" "vpc_1" { + cidr_block = "10.0.0.0/16" + + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = "${var.prefix}-vpc-1" + } +} + +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 + + tags = { + Name = "${var.prefix}-subnet-1" + } +} + +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 + } + + 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_security_group" "sg_1" { + name = "${var.prefix}-sg-1" + vpc_id = aws_vpc.vpc_1.id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 8080 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 9000 + to_port = 9000 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "MinIO API" + } + + ingress { + from_port = 9001 + to_port = 9001 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "MinIO Console" + } + + ingress { + from_port = 9092 + to_port = 9092 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/16"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.prefix}-sg-1" + } +} + +# S3 버킷은 기존에 생성된 fivelogic-files-bucket 사용 + +# EC2 역할 생성 +resource "aws_iam_role" "ec2_role_1" { + name = "${var.prefix}-ec2-role-1" + + assume_role_policy = <> /etc/fstab + +# 작업 디렉토리 생성 +mkdir -p /home/ec2-user/kafka +mkdir -p /home/ec2-user/app +cd /home/ec2-user/kafka + +# Kafka docker-compose.yml 생성 +cat > docker-compose.yml <<'COMPOSE' +services: + zookeeper: + image: confluentinc/cp-zookeeper:7.0.1 + container_name: zookeeper + ports: + - "2181:2181" + environment: + - ZOOKEEPER_CLIENT_PORT=2181 + - ZOOKEEPER_TICK_TIME=2000 + restart: always + + kafka: + image: confluentinc/cp-kafka:7.0.1 + container_name: kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + restart: always +COMPOSE + +# Kafka 실행 +docker-compose up -d + +# 스프링부트 실행 스크립트 생성 (나중에 jar 배포 후 사용) +cat > /home/ec2-user/app/start-spring.sh <<'SPRING' +#!/bin/bash +cd /home/ec2-user/app +nohup java -jar app.jar > app.log 2>&1 & +echo $! > app.pid +SPRING + +chmod +x /home/ec2-user/app/start-spring.sh + +# 권한 설정 +chown -R ec2-user:ec2-user /home/ec2-user + +echo "Setup completed!" > /home/ec2-user/setup-complete.txt +EOF +} + +# Outputs +output "ec2_public_ip" { + value = aws_instance.main.public_ip + description = "EC2 인스턴스 퍼블릭 IP" +} + +output "existing_s3_bucket" { + value = "fivelogic-files-bucket" + description = "기존 S3 버킷 이름" +} + +# ========================= +# RDS MySQL 설정 시작 +# ========================= + +# Subnet 추가 (RDS는 최소 2개 AZ 필요) +resource "aws_subnet" "subnet_2" { + vpc_id = aws_vpc.vpc_1.id + cidr_block = "10.0.2.0/24" + availability_zone = "${var.region}c" + map_public_ip_on_launch = true + + tags = { + Name = "${var.prefix}-subnet-2" + } +} + +resource "aws_route_table_association" "association_2" { + subnet_id = aws_subnet.subnet_2.id + route_table_id = aws_route_table.rt_1.id +} + +# RDS Subnet Group +resource "aws_db_subnet_group" "rds_subnet_group" { + name = "${var.prefix}-rds-subnet-group" + subnet_ids = [ + aws_subnet.subnet_1.id, + aws_subnet.subnet_2.id + ] + + tags = { + Name = "${var.prefix}-rds-subnet-group" + } +} + +# RDS Security Group (별도로 생성) +resource "aws_security_group" "rds_sg" { + name = "${var.prefix}-rds-sg" + vpc_id = aws_vpc.vpc_1.id + + # EC2에서 RDS로의 접근만 허용 + ingress { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + security_groups = [aws_security_group.sg_1.id] + description = "MySQL from EC2" + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.prefix}-rds-sg" + } +} + +# RDS MySQL Instance +resource "aws_db_instance" "mysql" { + identifier = "${var.prefix}-mysql" + engine = "mysql" + engine_version = "8.0" + instance_class = "db.t3.micro" + allocated_storage = 20 + storage_type = "gp3" + + db_name = "fivelogic" + username = "admin" + password = var.password_1 + + db_subnet_group_name = aws_db_subnet_group.rds_subnet_group.name + vpc_security_group_ids = [aws_security_group.rds_sg.id] + + publicly_accessible = false + skip_final_snapshot = true + backup_retention_period = 7 + backup_window = "03:00-04:00" + maintenance_window = "mon:04:00-mon:05:00" + + tags = { + Name = "${var.prefix}-mysql" + } +} + +# Outputs +output "rds_endpoint" { + value = aws_db_instance.mysql.endpoint + description = "RDS MySQL endpoint" + sensitive = false +} + +output "rds_database_name" { + value = aws_db_instance.mysql.db_name + description = "RDS database name" +} + +# ========================= +# RDS MySQL 설정 끝 +# ========================= \ No newline at end of file diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf new file mode 100644 index 00000000..d11979cd --- /dev/null +++ b/infra/terraform/variables.tf @@ -0,0 +1,11 @@ +variable "region" { + description = "AWS 리전" + type = string + default = "ap-northeast-2" +} + +variable "prefix" { + description = "리소스 이름 prefix" + type = string + default = "devcos-team10" +} \ No newline at end of file