반응형
Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

Devsecops로 발전하는 엔지니어

Gitlab CI/CD를 이용한 ECS배포 -1 (1) 본문

Devops

Gitlab CI/CD를 이용한 ECS배포 -1 (1)

cloud/devops/opensource 관심 많은 곰 2025. 9. 3. 15:17
반응형

📁 GitLab 프로젝트 구조

1. Terraform 인프라 구성

주요 리소스

  • VPC 및 네트워킹 (Public/Private Subnets)
  • ECS Cluster (EC2 Launch Type)
  • Application Load Balancer
  • Auto Scaling Group (t3.medium × 2)
  • IAM Roles 및 Security Groups
# ─────────────────────────────────────────────────────────────
# DevOps 이름지정 Environment - AWS Seoul Region (ap-northeast-2)
# ─────────────────────────────────────────────────────────────

terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = ">= 5.0" }
  }
}

provider "aws" {
  region = "ap-northeast-2"
}

# ─────────────────────────────────────────────────────────────
# ECR 태그 → 다이제스트 자동 조회 (TF apply 시 새 리비전/롤링 배포 트리거)
# ─────────────────────────────────────────────────────────────
data "aws_caller_identity" "current" {}

variable "ecr_repo" {
  type    = string
  default = "docker-이름지정"
}

variable "image_tag" {
  type    = string
  default = "la이름지정"
}

data "aws_ecr_image" "selected" {
  repository_name = var.ecr_repo
  image_tag       = var.image_tag
}

locals {
  ecr_image = "${data.aws_caller_identity.current.account_id}.dkr.ecr.ap-northeast-2.amazonaws.com/${var.ecr_repo}:la이름지정"
}

# ─────────────────────────────────────────────────────────────
# VPC / Subnets / Routing
# ─────────────────────────────────────────────────────────────
resource "aws_vpc" "devops_이름지정" {
  cidr_block           = "10.100.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = { Name = "devops-이름지정-vpc" }
}

resource "aws_subnet" "devops_이름지정_public_subnet1" {
  vpc_id                  = aws_vpc.devops_이름지정.id
  cidr_block              = "10.100.100.0/24"
  availability_zone       = "ap-northeast-2a"
  map_public_ip_on_launch = true
  tags = { Name = "devops-이름지정-public-subnet1" }
}

resource "aws_subnet" "devops_이름지정_public_subnet2" {
  vpc_id                  = aws_vpc.devops_이름지정.id
  cidr_block              = "10.100.101.0/24"
  availability_zone       = "ap-northeast-2b"
  map_public_ip_on_launch = true
  tags = { Name = "devops-이름지정-public-subnet2" }
}

resource "aws_subnet" "devops_이름지정_private_subnet1" {
  vpc_id            = aws_vpc.devops_이름지정.id
  cidr_block        = "10.100.1.0/24"
  availability_zone = "ap-northeast-2a"
  tags = { Name = "devops-이름지정-private-subnet1" }
}

resource "aws_subnet" "devops_이름지정_private_subnet2" {
  vpc_id            = aws_vpc.devops_이름지정.id
  cidr_block        = "10.100.2.0/24"
  availability_zone = "ap-northeast-2b"
  tags = { Name = "devops-이름지정-private-subnet2" }
}

resource "aws_internet_gateway" "devops_이름지정_igw" {
  vpc_id = aws_vpc.devops_이름지정.id
  tags = { Name = "devops-이름지정-igw" }
}

resource "aws_route_table" "devops_이름지정_public_rt" {
  vpc_id = aws_vpc.devops_이름지정.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.devops_이름지정_igw.id
  }

  tags = { Name = "devops-이름지정-public-rt" }
}

resource "aws_route_table_association" "devops_이름지정_public_rt_assoc1" {
  subnet_id      = aws_subnet.devops_이름지정_public_subnet1.id
  route_table_id = aws_route_table.devops_이름지정_public_rt.id
}

resource "aws_route_table_association" "devops_이름지정_public_rt_assoc2" {
  subnet_id      = aws_subnet.devops_이름지정_public_subnet2.id
  route_table_id = aws_route_table.devops_이름지정_public_rt.id
}

resource "aws_eip" "devops_이름지정_nat" {
  domain = "vpc"
  tags   = { Name = "devops-이름지정-nat-eip" }
}

resource "aws_nat_gateway" "devops_이름지정_nat" {
  allocation_id = aws_eip.devops_이름지정_nat.id
  subnet_id     = aws_subnet.devops_이름지정_public_subnet1.id
  depends_on    = [aws_internet_gateway.devops_이름지정_igw]
  tags          = { Name = "devops-이름지정-nat-gw" }
}

resource "aws_route_table" "devops_이름지정_private_rt" {
  vpc_id = aws_vpc.devops_이름지정.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.devops_이름지정_nat.id
  }

  tags = { Name = "devops-이름지정-private-rt" }
}

resource "aws_route_table_association" "devops_이름지정_private_rt_assoc1" {
  subnet_id      = aws_subnet.devops_이름지정_private_subnet1.id
  route_table_id = aws_route_table.devops_이름지정_private_rt.id
}

resource "aws_route_table_association" "devops_이름지정_private_rt_assoc2" {
  subnet_id      = aws_subnet.devops_이름지정_private_subnet2.id
  route_table_id = aws_route_table.devops_이름지정_private_rt.id
}

# ─────────────────────────────────────────────────────────────
# Security Groups
# ─────────────────────────────────────────────────────────────
resource "aws_security_group" "devops_이름지정_bastion_sg" {
  name        = "devops-이름지정-bastion-sg"
  description = "Allow SSH from specific IP"
  vpc_id      = aws_vpc.devops_이름지정.id

  ingress {
    description = "SSH from my IP"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["ip"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "devops-이름지정-bastion-sg" }
}

resource "aws_security_group" "devops_이름지정_alb_sg" {
  name        = "devops-이름지정-alb-sg"
  description = "Allow HTTP from anywhere"
  vpc_id      = aws_vpc.devops_이름지정.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "devops-이름지정-alb-sg" }
}

resource "aws_security_group" "devops_이름지정_ecs_sg" {
  name        = "devops-이름지정-ecs-sg"
  description = "Allow HTTP from ALB, SSH from bastion"
  vpc_id      = aws_vpc.devops_이름지정.id

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [aws_security_group.devops_이름지정_bastion_sg.id]
  }

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.devops_이름지정_alb_sg.id]
  }

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.devops_이름지정_alb_sg.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "devops-이름지정-ecs-sg" }
}

# ─────────────────────────────────────────────────────────────
# Bastion (Amazon Linux 2023)
# ─────────────────────────────────────────────────────────────
data "aws_ami" "devops_이름지정_amazonlinux2023" {
  most_recent = true
  owners      = ["137112412989"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "devops_이름지정_bastion" {
  ami                         = data.aws_ami.devops_이름지정_amazonlinux2023.id
  instance_type               = "t3.nano"
  subnet_id                   = aws_subnet.devops_이름지정_public_subnet1.id
  key_name                    = "docker-bastion"
  vpc_security_group_ids      = [aws_security_group.devops_이름지정_bastion_sg.id]
  associate_public_ip_address = true
  tags = { Name = "devops-이름지정-bastion" }
}

# ─────────────────────────────────────────────────────────────
# ECS Cluster & IAM (EC2 Launch)
# ─────────────────────────────────────────────────────────────
resource "aws_ecs_cluster" "devops_이름지정_cluster" {
  name = "devops-이름지정-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  tags = { Name = "devops-이름지정-cluster" }
}

resource "aws_iam_role" "devops_이름지정_ecs_instance_role" {
  name = "devops-이름지정-ecs-instance-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Action    = "sts:AssumeRole",
      Effect    = "Allow",
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })

  tags = { Name = "devops-이름지정-ecs-instance-role" }
}

resource "aws_iam_role_policy_attachment" "devops_이름지정_ecs_instance_role_policy" {
  role       = aws_iam_role.devops_이름지정_ecs_instance_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}

resource "aws_iam_role_policy_attachment" "devops_이름지정_ssm_policy" {
  role       = aws_iam_role.devops_이름지정_ecs_instance_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role_policy_attachment" "devops_이름지정_ecr_ro" {
  role       = aws_iam_role.devops_이름지정_ecs_instance_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}

resource "aws_iam_instance_profile" "devops_이름지정_ecs_instance_profile" {
  name = "devops-이름지정-ecs-instance-profile"
  role = aws_iam_role.devops_이름지정_ecs_instance_role.name
}

data "aws_ssm_parameter" "ecs_al2_ami" {
  name = "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id"
}

resource "aws_launch_template" "devops_이름지정_ecs_lt" {
  name_prefix   = "devops-이름지정-ecs-"
  image_id      = data.aws_ssm_parameter.ecs_al2_ami.value
  instance_type = "t3.medium"
  key_name      = "docker-bastion"

  vpc_security_group_ids = [aws_security_group.devops_이름지정_ecs_sg.id]

  iam_instance_profile {
    name = aws_iam_instance_profile.devops_이름지정_ecs_instance_profile.name
  }

  user_data = base64encode(<<-EOF
    #!/bin/bash
    echo "ECS_CLUSTER=${aws_ecs_cluster.devops_이름지정_cluster.name}" >> /etc/ecs/ecs.config
  EOF
  )

  tag_specifications {
    resource_type = "instance"
    tags = { Name = "devops-이름지정-ecs-instance" }
  }
}

resource "aws_autoscaling_group" "devops_이름지정_ecs_asg" {
  name                        = "devops-이름지정-ecs-asg"
  vpc_zone_identifier         = [aws_subnet.devops_이름지정_private_subnet1.id, aws_subnet.devops_이름지정_private_subnet2.id]
  health_check_type           = "EC2"
  health_check_grace_period   = 300
  protect_from_scale_in       = true
  min_size                    = 2
  max_size                    = 2
  desired_capacity            = 2

  launch_template {
    id      = aws_launch_template.devops_이름지정_ecs_lt.id
    version = "$La이름지정"
  }

  tag {
    key                 = "Name"
    value               = "devops-이름지정-ecs-asg"
    propagate_at_launch = true
  }

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 90
      instance_warmup        = 120
    }
    triggers = ["launch_template"]
  }
}

resource "aws_ecs_capacity_provider" "devops_이름지정_capacity_provider" {
  name = "devops-이름지정-capacity-provider"

  auto_scaling_group_provider {
    auto_scaling_group_arn         = aws_autoscaling_group.devops_이름지정_ecs_asg.arn
    managed_termination_protection = "ENABLED"

    managed_scaling {
      maximum_scaling_step_size = 2
      minimum_scaling_step_size = 1
      status                    = "ENABLED"
      target_capacity           = 100
    }
  }

  tags = { Name = "devops-이름지정-capacity-provider" }
}

resource "aws_ecs_cluster_capacity_providers" "devops_이름지정_cluster_cp" {
  cluster_name       = aws_ecs_cluster.devops_이름지정_cluster.name
  capacity_providers = [aws_ecs_capacity_provider.devops_이름지정_capacity_provider.name]

  default_capacity_provider_strategy {
    base              = 1
    weight            = 100
    capacity_provider = aws_ecs_capacity_provider.devops_이름지정_capacity_provider.name
  }
}

# ─────────────────────────────────────────────────────────────
# ALB / TargetGroup / Listener
# ─────────────────────────────────────────────────────────────
resource "aws_lb" "devops_이름지정_alb" {
  name               = "devops-이름지정-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.devops_이름지정_alb_sg.id]
  subnets            = [aws_subnet.devops_이름지정_public_subnet1.id, aws_subnet.devops_이름지정_public_subnet2.id]
  tags = { Name = "devops-이름지정-alb" }
}

resource "aws_lb_target_group" "devops_이름지정_tg" {
  name        = "devops-이름지정-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.devops_이름지정.id
  target_type = "instance"

  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 30
    path                = "/"
    matcher             = "200-399"
    port                = "traffic-port"
    protocol            = "HTTP"
  }

  tags = { Name = "devops-이름지정-tg" }
}

resource "aws_lb_listener" "devops_이름지정_listener" {
  load_balancer_arn = aws_lb.devops_이름지정_alb.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.devops_이름지정_tg.arn
  }
}

# ─────────────────────────────────────────────────────────────
# Task Definition / Service
# ─────────────────────────────────────────────────────────────
resource "aws_iam_role" "devops_task_exec_role" {
  name = "devops-이름지정-ecs-task-exec-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect    = "Allow",
      Principal = { Service = "ecs-tasks.amazonaws.com" },
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "devops_task_exec_attach" {
  role       = aws_iam_role.devops_task_exec_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

resource "aws_cloudwatch_log_group" "devops_이름지정_log_group" {
  name              = "/ecs/devops-이름지정-task"
  retention_in_days = 7
  tags = { Name = "devops-이름지정-log-group" }
}

resource "aws_ecs_task_definition" "devops_이름지정_task" {
  family                   = "devops-이름지정-task"
  network_mode             = "bridge"
  requires_compatibilities = ["EC2"]
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = aws_iam_role.devops_task_exec_role.arn

  container_definitions = jsonencode([
    {
      name      = "devops-이름지정-container",
      image     = local.ecr_image,
      cpu       = 256,
      memory    = 512,
      essential = true,
      portMappings = [
        {
          containerPort = 80,
          hostPort      = 80,
          protocol      = "tcp"
        }
      ],
      logConfiguration = {
        logDriver = "awslogs",
        options = {
          awslogs-group         = "/ecs/devops-이름지정-task",
          awslogs-region        = "ap-northeast-2",
          awslogs-stream-prefix = "ecs"
        }
      }
    }
  ])

  tags = { Name = "devops-이름지정-task" }
}

resource "aws_ecs_service" "devops_이름지정_service" {
  name            = "devops-이름지정-service"
  cluster         = aws_ecs_cluster.devops_이름지정_cluster.id
  task_definition = aws_ecs_task_definition.devops_이름지정_task.arn
  desired_count   = 2

  capacity_provider_strategy {
    capacity_provider = aws_ecs_capacity_provider.devops_이름지정_capacity_provider.name
    weight            = 100
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.devops_이름지정_tg.arn
    container_name   = "devops-이름지정-container"
    container_port   = 80
  }

  depends_on = [
    aws_lb_listener.devops_이름지정_listener,
    aws_ecs_cluster_capacity_providers.devops_이름지정_cluster_cp
  ]

  # 필요 시 주석 해제하여 매 apply 때 강제 롤링
  # force_new_deployment = true

  tags = { Name = "devops-이름지정-service" }
}

# ─────────────────────────────────────────────────────────────
# Outputs
# ─────────────────────────────────────────────────────────────
output "bastion_public_ip" {
  description = "Bastion 서버의 Public IP"
  value       = aws_instance.devops_이름지정_bastion.public_ip
}

output "alb_dns_name" {
  description = "Application Load Balancer DNS 이름"
  value       = aws_lb.devops_이름지정_alb.dns_name
}

output "alb_url" {
  description = "Application Load Balancer URL"
  value       = "<http://$>{aws_lb.devops_이름지정_alb.dns_name}"
}

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.devops_이름지정.id
}

output "private_subnet_cidrs" {
  description = "Private Subnet CIDR 블록들 (ECS 인스턴스 IP 범위)"
  value = [
    aws_subnet.devops_이름지정_private_subnet1.cidr_block,
    aws_subnet.devops_이름지정_private_subnet2.cidr_block
  ]
}

output "ssh_connection_guide" {
  description = "SSH 접속 가이드"
  value = <<-EOT
    1) Bastion 접속:
       ssh -i .pem ec2-user@${aws_instance.devops_이름지정_bastion.public_ip}

    2) ECS 인스턴스 접속 (ProxyJump 예시):
       ssh -J ec2-user@${aws_instance.devops_이름지정_bastion.public_ip} -i <ecs.pem> ec2-user@<ECS_PRIVATE_IP>

    3) 웹 서비스 확인:
       <http://$>{aws_lb.devops_이름지정_alb.dns_name}
  EOT
}

cd /infra
terraform init // terraform 구성 파일이 포함된 작업 디렉토리 초기화.
terraform plan // terraform 실행 계획
terraform apply // terraform 배포

이렇게 하면 terraform으로 aws vpc/ecs/lb다 세팅이 완료됨

반응형