# 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 를 사용 한다는 것은 결국 인프라를 프로그래밍한다는 의미이며 프로그래밍 언어의 특징도 그대로 가져옵니다.
* 한번 작성한 코드는 재 사용이 가능 하도록 설계 되어야 합니다.
* 공통으로 사용하는 부분은 변수로 처리 해야 합니다.
* 자주 사용 하는 구문은 모듈화 해야 합니다.\\

\\

> ## 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>

\\

![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)

\\

> ## 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>

\\

* 결과 ![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>

\\

* 결과

![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
  )'
)"
```
