# terraform

## terraform 서비스 구축

> ## Intro

* 본 문서는 terraform 으로 실습한 소스코드를 설명하는 데 중점을 두었습니다.
* 본 문서는 markdown 언어로 제작 되었습니다.
* 김태형(<kimutae@naver.com>) 의 저작물이며 무단 배포 / 재배포를 금합니다.

> ## IAC(infra as code) 이란

* 인프라를 코드로 프로비저닝 / 관리한다는 의미입니다.
* 장점
* 비용 절감
* 배포 속도 향상
* 오류 감소
* 인프라 일관성 향상
* 구성 변동 제거

> ## terraform 이란

* 테라폼(Terraform)은 하시코프(Hashicorp)에서 오픈소스로 개발중인 클라우드 인프라스트럭처 입니다.
* 대표적인 public cloud 인 aws / gcp / azure 등을 모두 지원합니다.
* 하시코프 설정 언어HCL(Hashicorp Configuration Language)을 사용해 클라우드 리소스를 선언합니다
* 파일 확장자는 .tf입니다.

> ## terraform 명령어

* init : . terrform 초기화 이후 각 vendor사에 맞는 provier 파일을 설치 합니다.
* plan :
  1. 테라폼 프로젝트 디렉터리 아래의 모든 .tf 파일의 내용을 실제로 적용 가능한지 확인하는 작업을 계획
  2. 테라폼은 이를 terraform plan 명령어로 제공하며, 이 명령어를 실행하면 어떤 리소스가 생성되고, 수정되고, 삭제
* apply :
  1. 테라폼 프로젝트 디렉터리 아래의 모든 .tf 파일의 내용대로 리소스를 생성, 수정, 삭제
  2. 테라폼은 이를 terraform apply 명령어로 제공합니다. 이 명령어를 실행하기 전에 변경 예정 사항은 plan 명령어

> ## terraform 으로 구성하기 앞서 생각해야 할 점

* 상시 변경 인프라 / 기반인프라 구분
* 테라폼을 사용하게 되면 한 디렉토리 내에 있는 모든 tf 파일들을 동시에 실행을 합니다.
* 그렇기에 한번 구축하면 거의 변경 할 일이 없는 인프라와 상시로 변경이나 구축이 자주 일어나는 인프라를 구분해야 합니다.
* 초기 구성 이후 거의 변하지 않는 항목은 vpc등 네트워크 인프라이며 - resource(신규생성)를 지정 해서 인프라를 생성 합니다.
* 서비스 구성 시 항상 자주 구성해야 하는 항목 - data(불러오기) / resource를 혼용 하여 사용 합니다.
* EX

  | resource    | data            |
  | ----------- | --------------- |
  | vpc         | security gruop  |
  | subnet      | Load Balancer   |
  | igw         | target group    |
  | NAT         | EC2             |
  | ECS-Cluster | ecs             |
  |             | task difinition |
  |             | ecr             |
  |             | RDS             |
* code 재 사용이 가능하도록 설계
* IAC 를 사용 한다는 것은 결국 인프라를 프로그래밍한다는 의미이며 프로그래밍 언어의 특징도 그대로 가져옵니다.
* 한번 작성한 코드는 재 사용이 가능 하도록 설계 되어야 합니다.
* 공통으로 사용하는 부분은 변수로 처리 해야 합니다.
* 자주 사용 하는 구문은 모듈화 해야 합니다.<br>

<br>

> ## variables를 활용한 code 사용 예시

* 변수지정

```hci
variable "cidr_numeral" {
description = "The VPC CIDR numeral (10.x.0.0/16)"
type  = number
default = "30"
}

variable "cidr_numeral_public" {
default = {
    "0" = "0"
    "1" = "16"
    "2" = "32"
}
}

############# 가용 영역 지정 - 가용영역의 갯수만큼 서브넷이 만들어진다
variable "availability_zones" {
    type    = list(string)
    default = [
    "ap-northeast-2a" ,
    "ap-northeast-2b"
    ]
}
```

* 사용

```
    resource "aws_subnet" "public" {
    count  = length(var.availability_zones)  # 변수로 지정된 가용 영역의 갯수 현재 2 즉 변수는 0,1 
    vpc_id = aws_vpc.vpc.id #위에서 생성한 vpc 별칭 입력
    #################
    # count.index = for 문 역할 0 = 0 / 1=16
    cidr_block  = "10.${var.cidr_numeral}.${var.cidr_numeral_public[count.index]}.0/20"    
    # resource참조는 element 구문을 활용 하여 for 문을 작성합니다
    availability_zone = element(var.availability_zones, count.index)
    
    #################
    map_public_ip_on_launch = true
    tags = {
        Name = "public${count.index}-${var.vpc_name}"
    }
    }
    
```

> ## 디렉토리 구성

```dos
main
├── global           >> global 설정 - vpc를 추가 할때 사용
│   └── mk-network-var   >>  vpc / subnet /igw 등 네트워크 설정
│       ├── main.tf
│       ├── network.tf
│       ├── terraform.tfstate
│       ├── terraform.tfstate.backup
│       └── variables.tf
└── service
└── ecs-web
├── falgate   >> ecs-cluster 내 다중 서비스를 포함하고 있어 별도로 생성 할수 있도록 구성
│   ├── ecr.tf
│   ├── ecs-cluster.tf
│   ├── td-service
│   │   ├── ecs.tf
│   │   ├── serevice.conf.json
│   │   ├── serevice.conf.json.tpl
│   │   ├── td.tf
│   │   ├── terraform.tfstate
│   │   ├── terraform.tfstate.backup
│   │   └── variables.tf -> ../../variables.tf   >> 공용 변수를 사용 하기 위해 심볼릭 링크로 구성
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
│   └── variables.tf -> ../variables.tf
├── sg-lb     >> security group 과 로드벨런서를 구성
│   ├── lb.tf
│   ├── output.tf
│   ├── sg.tf -> ../sg.tf
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
│   ├── variables.tf -> ../variables.tf
├── sg.tf
└── variables.tf

```

> ## VPC 구성

<details>

<summary>코드 펼치기</summary>

* 공용 변수 : variables.tf

```terraform

variable "access_key" {
type = string
}

variable "secret_key" {
type = string
}

variable "region" {
type    = string
default = "ap-northeast-2"
}

variable "vpc_name" {
description = "The name of the VPC"
default = "terra"
}

########### CIDR 변수 지정 
variable "cidr_numeral" {
description = "The VPC CIDR numeral (10.x.0.0/16)"
type  = number
default = "30"
}

############# 가용 영역 지정 - 가용영역의 갯수만큼 서브넷이 만들어진다
variable "availability_zones" {
type    = list(string)
default = [
"ap-northeast-2a" ,
"ap-northeast-2b"
]
}

variable "cidr_numeral_public" {
default = {
    "0" = "0"
    "1" = "16"
    "2" = "32"
}
}

variable "cidr_numeral_private" {
default = {
    "0" = "80"
    "1" = "96"
    "2" = "112"
}
}

variable "cidr_numeral_private_db" {
default = {
    "0" = "160"
    "1" = "176"
    "2" = "192"
}
}

variable "site_name" {
type = string
default = "terra"
}
```

* 프로바이더 지정 - main.tf

```terraform
provider "aws" {
access_key = var.access_key
secret_key = var.secret_key
region     = var.region
}
```

* subnet / igw / nat 구성

```terraform
resource "aws_vpc" "vpc" {
# cidr_block = "10.30.0.0/16" #IPv4 CIDR Block
cidr_block  = "10.${var.cidr_numeral}.0.0/16" # Please set this according to your company size
enable_dns_hostnames = true #DNS Hostname 사용 옵션, 기본은 false
tags =  { Name = "${var.site_name}_vpc" } #tag 입력
}


resource "aws_internet_gateway" "default" {
vpc_id = aws_vpc.vpc.id

tags = {
Name = "igw-${var.vpc_name}"
}
}


## NAT Gateway 
resource "aws_nat_gateway" "nat" {
count = length(var.availability_zones)

allocation_id = element(aws_eip.nat.*.id, count.index)

subnet_id = element(aws_subnet.public.*.id, count.index)

lifecycle {
create_before_destroy = true
}

tags = {
Name = "NAT-GW${count.index}-${var.site_name}"
}

}

# Elastic IP for NAT Gateway 
resource "aws_eip" "nat" {
count = length(var.availability_zones)
vpc   = true

lifecycle {
create_before_destroy = true
}
}


#### PUBLIC SUBNETS
resource "aws_subnet" "public" {
count  = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id #위에서 생성한 vpc 별칭 입력
cidr_block  = "10.${var.cidr_numeral}.${var.cidr_numeral_public[count.index]}.0/20"
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = true
tags = {
Name = "public${count.index}-${var.vpc_name}"
}
}

# Route Table for public subnets
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id #위에서 생성한 vpc 별칭 입력

tags = {
Name = "publicrt-${var.vpc_name}"
}
}


# Route Table Association for public subnets
resource "aws_route_table_association" "public" {
count          = length(var.availability_zones)
subnet_id      = element(aws_subnet.public.*.id, count.index)
route_table_id = aws_route_table.public.id
}


#### PRIVATE SUBNETS
# Subnet will use cidr with /20 -> The number of available IP is 4,096  (Including reserved ip from AWS)
resource "aws_subnet" "private" {
count  = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id

cidr_block        = "10.${var.cidr_numeral}.${var.cidr_numeral_private[count.index]}.0/20"
availability_zone = element(var.availability_zones, count.index)

tags = {
Name    = "private${count.index}-${var.vpc_name}"
Network = "Private"
}
}

# Route Table for private subnets
resource "aws_route_table" "private" {
count  = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id

tags = {
Name    = "private${count.index}rt-${var.vpc_name}"
Network = "Private"
}
}

# Route Table Association for private subnets
resource "aws_route_table_association" "private" {
count          = length(var.availability_zones)
subnet_id      = element(aws_subnet.private.*.id, count.index)
route_table_id = element(aws_route_table.private.*.id, count.index)
}


# DB PRIVATE SUBNETS
# This subnet is only for the database. 
# For security, it is better to assign ip range for database only. This subnet will not use NAT Gateway
# This is also going to use /20 cidr, which might be too many IPs... Please count it carefully and change the cidr.
resource "aws_subnet" "private_db" {
count  = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id

cidr_block   = "10.${var.cidr_numeral}.${var.cidr_numeral_private_db[count.index]}.0/20"
availability_zone = element(var.availability_zones, count.index)

tags = {
Name    = "db-private${count.index}-${var.vpc_name}"
Network = "Private"
}
}

# Route Table for DB subnets
resource "aws_route_table" "private_db" {
count  = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id

tags = {
Name    = "privatedb${count.index}rt-${var.vpc_name}"
Network = "Private"
}
}

# Route Table Association for DB subnets
resource "aws_route_table_association" "private_db" {
count          = length(var.availability_zones)
subnet_id      = element(aws_subnet.private_db.*.id, count.index)
route_table_id = element(aws_route_table.private_db.*.id, count.index)
}
```

</details>

<br>

![subnet](https://user-images.githubusercontent.com/71241225/173634024-e65e56c7-11ca-4e05-b64e-8294f4d73b49.png)

> ## Load Balancer 구성

<details>

<summary>코드 펼치기</summary>

```terraform
data "aws_subnet" "public" {
count  = length(var.availability_zones)
tags = {
Name = "public${count.index}-${var.vpc_name}"
}
}

#resource "aws_lb" "terra-lb" {
resource "aws_lb" "terra-lb" {
count = length(var.product) 
name               = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
subnets            = data.aws_subnet.public[*].id
load_balancer_type = "application"
security_groups    = [aws_security_group.lb.id]

#  access_logs {
#    bucket  = aws_s3_bucket.log_storage.id
#    prefix  = "frontend-alb"
#    enabled = true
#  }

tags = {
Environment = "terra-lb"
Application = var.app_name
}
}

#resource "aws_lb_listener" "https_forward" {
#  load_balancer_arn = aws_lb.terra-lb.arn
#  port              = 443
#  protocol          = "HTTPS"
#  certificate_arn   = aws_acm_certificate.cert.arn
#  ssl_policy        = "ELBSecurityPolicy-2016-08"
#
#  default_action {
#    type             = "forward"
#    target_group_arn = aws_lb_target_group.terra-lb.arn
#  }
#}


resource "aws_lb_listener" "http_forward" {

count = length(var.product) 

load_balancer_arn = element(aws_lb.terra-lb.*.arn,count.index)
#load_balancer_arn = aws_lb.terra-lb.arn
port              = 80
protocol          = "HTTP"

default_action {
#    type = "redirect"
type             = "forward"
target_group_arn = element(aws_lb_target_group.terra-lb.*.arn,count.index)


#    redirect {
#      port        = "443"
#      protocol    = "HTTPS"
#      status_code = "HTTP_301"
#    }
}
}

resource "aws_lb_target_group" "terra-lb" {

count = length(var.product) 
name = "${var.site_name}-${var.service_name}-${var.product[count.index]}" 
#  name        = "service-alb-tg"
port        = 80
protocol    = "HTTP"
vpc_id      = data.aws_vpc.vpc.id
target_type = "ip"

health_check {
enabled             = true
interval            = 300
path                = "/"
timeout             = 60
matcher             = "200"
healthy_threshold   = 5
unhealthy_threshold = 5
}

lifecycle {
create_before_destroy = true
}
}

```

</details>

* 결과 ![image](https://user-images.githubusercontent.com/71241225/173634646-d38ca2a4-29d3-4f4c-bb17-22e516026612.png)

<br>

> ## task definition 구성

<details>

<summary>코드 펼치기</summary>

* templete 를 활용 하여 사전에 작성한 td.json파일을 import 합니다

  ```terraform
  data "aws_iam_role" "ecs_task_execution_role" {
  name     = "ecsTaskExecutionRole"
  }
  data "aws_ecr_repository" "repo" {

  count = length(var.product) 
  name = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
  #  name = "ecr-repository"
  }

  data "template_file" "service" {
   template = file(var.tpl_path)
  count = length(var.product) 
   vars = {
     region             = var.region
     aws_ecr_repository = element(data.aws_ecr_repository.repo.*.repository_url,count.index)
     tag                = "latest"
     container_port    = var.container_port
     host_port          = var.host_port
     app_name         = var.app_name
   }
  }



  #output "template_file"  {
  #    value = data.template_file.service.*.vars
  #}


  #output "aws_ecs_task_definition" {
  #  value    = data.template_file.service
  #}

  resource "aws_ecs_task_definition" "service" {
   count = length(var.product) 
   family                   = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
   network_mode             = "awsvpc"
   execution_role_arn       = data.aws_iam_role.ecs_task_execution_role.arn
   cpu                      =2048 
   memory                   =4096 
   requires_compatibilities = ["FARGATE"]
   container_definitions    = element(data.template_file.service.*.rendered,count.index)
   tags = {
     Environment = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
     Application = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
   }
  }


  ```

</details>

<br>

* 결과 ![image](https://user-images.githubusercontent.com/71241225/173635721-f6a34e81-b478-44c5-bd92-fbeb3f0fe713.png)

> ## ECS - service 구성

<details>

<summary>코드 펼치기</summary>

```
  data "aws_ecs_cluster" cluster {
    cluster_name = "${var.site_name}-${var.service_name}"
  }
  
  data "aws_security_group" sg  {
   name = "${var.app_name}"
  }
  
  data "aws_lb_target_group" tg  {
   count = length(var.product) 
   name = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
  }
  
  data "aws_subnet" "public_sub" {
  
   count = length(var.availability_zones) 
    tags = {
      Name = "public${count.index}-${var.vpc_name}"
    }
  }
  
   
  
  resource "aws_ecs_service" "ecs_service" {
    count = length(var.product) 
    name = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
    cluster         = data.aws_ecs_cluster.cluster.id
    
    task_definition = element(aws_ecs_task_definition.service.*.arn,count.index)
    desired_count   = 3
    launch_type     = "FARGATE"
  
    network_configuration {
      security_groups  = [data.aws_security_group.sg.id]
     # count = length(var.availability_zones) 
      subnets          = data.aws_subnet.public_sub[*].id
      assign_public_ip = true
    }
  
    load_balancer {
     # count = length(var.product) 
      target_group_arn = element(data.aws_lb_target_group.tg.*.arn,count.index)
      container_name   = var.app_name
      container_port   = var.container_port
    }
  
  #  depends_on = [aws_lb_listener.https_forward, aws_lb_listener.http_forward, aws_iam_role_policy_attachment.ecs_task_execution_role]
  
    tags = {
  
      count = length(var.product) 
      Environment = "ecs_service"
      Application = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
    }
  }
```

</details>

<br>

* 결과

![image](https://user-images.githubusercontent.com/71241225/173635470-0e3cb5f1-f792-4798-adce-a29c75462476.png)

## 두 신뢰관계 정책 파일을 병합하는 jq 명령어

#### TrustShip

```bash
aws iam update-assume-role-policy --role-name MyRole --policy-document "$(
  aws iam get-role --role-name MyRole --query "Role.AssumeRolePolicyDocument" --output json | \
  jq -c '.Statement |= map(
    if .Principal.AWS then
      .Principal.AWS |= (if type == "string" then [.] else . end + ["arn:aws:iam::xxxx:root"] | unique)
    else
      .
    end
  )'
)"
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://dorian.gitbook.io/devops/iac/terraform.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
