메뉴

문서정보

목차

Terraform

테라폼(Terraform)은 HashiCorp에서 개발한 오픈소스 IaC(Infrastructure as code) 소프트웨어다. 개발자는 테라폼에서 제공하는 HCL(Hashicorp Configuration Language)라는 고수준 설정 언어(high-level configuration language) 혹은 json 를 이용해서 데이터센터 인프라스트럭처를 정의할 수 있다. 테라폼은 Amazon Web Service, IBM Cloud, Google Cloud Platform, Linode, Microsoft Azure, Oracle Cloud Infrastructure, VMware vSphere, OpenStack 등의 인프라스트럭처 제공자를 지원한다.

IaC

Infrastructure as Code(IaC)는 물리적 하드웨어 구성이나 대화형 소프트웨어나 대화형 스크립트 대신, 기계가 읽을 수 있는 설정 파일(코드)를 통해서 데이터 센터를 관리 및 프로비저닝하는 프로세스를 의미한다. 사용자는 베어메탈 서버와 같은 물리적 장비와 가상 시스템의 리소스를 코드형태로 관리하게 된다.

IaC의 장점은 아래와 같다.
  1. 비용의 감소
  2. 빠른 실행. 속도
  3. 위험 관리(혹은 감소)
비용 절감은 재정적인 것 뿐만 아니라 인력과 노력의 측면에서 살펴볼 수 있다. 사람이 직접 (반복적으로)개입하는 요소를 제거함으로써, 다른 생산적인 작업에 노력을 집중 할 수 있게 한다. 인프라 스트럭처의 자동화는 인프라의 신속한 구성이 가능하게 하고, 다른 조직에서 빠르고 효율적으로 개발을 할 수 있도록 한다. 또한 인프라스트럭처의 전개가 자동화 되면서, 사람의 실수를 줄일 수 있다. 사람의 실수가 줄어들면서 가동 주기가 빨라짐으로서 인프라스트럭처의 안정성을 높일 수 있다. 이러한 IaC의 속성은 DevOps 문화를 구현하는데 도움을 준다.

테라폼은 가장 널리 알려진 IaC 솔류션 중 하나다.

학습 방법

먼저 간단?하면서도 가장 기본이 된다고 생각하는 VPC 네트워크를 테라폼으로 전개해볼 생각이다. 처음에는 연습삼아서 아주 간단한 네트워크를 만들고, 이후 현실적인 네트워크, EC2, NAT GATEWAY 등의 자원을 배치해서 완전히 작동하는 인프라를 만들것이다.

이렇게 작지만 작동하는 인프라를 만든 다음, 테라폼의 기능들을 살펴볼 것이다.

설치

Installing Terraform을 방문해서 운영체제에 맞는 테라폼을 다운로드해서 설치하자. 나는 리눅스 0.11.13 버전을 설치했다.
# unzip terraform_0.11.13_linux_amd64.zip
압축을 풀면 terraform 실행파일 하나만 나온다. 단일파일로 배포된다는 것도 장점 이다. /usr/local/bin 에 복사했다.
$ terraform version
Terraform v0.11.13

테스트 네트워크

 테스트 네트워크

퍼블릭/프라이빗 서브넷으로 구성된 네트워크를 만들려고 한다. 가용성 존은 고려하지 않았다. 이 네트워크는 아래의 요소들로 구성된다.
  1. VPC : 10.100.0.0/16
  2. 인터넷 게이트웨이(igw)
  3. 퍼블릭 서브넷 : 10.100.1.0/24
  4. 프라이빗 서브넷: 10.100.2.0/24
  5. Router

프로젝트 디렉토리

$HOME/workspace 밑에 terraform 디렉토리를 만들었다.
# mkdir $HOME/workspace/terraform -p
# cd $HOME/workspace/terraform
이 디렉토리 밑에 아래에서 다룰 전역변수 파일(variables.tf), 프로바이더 파일(provider.tf), vpc 파일(vpc.tf), 리소스 파일(resource.tf)등을 배치했다.

전역 변수 - Global variables

테라폼은 프로그래밍 언어다. 프로그래밍 언어와 마찬가지로 변수를 이용해서, 일정한 구조를 유지하면서도 유연함을 확보 할 수 있다. 테라폼의 변수 정보는 별도의 파일에 저장할 수 있으므로, 단일 파일에서 쉽게 값을 읽고 수정할 수 있다.

변수는 아래와 같이 정의 할 수 있다.
variable "variable_name" {}
변수는 list, map, string, boolean 타입이 있으며 기본값(default)를 정의 할 수도 있다.타입을 정의하지 않을 경우 자동으로 타입을 추론한다.
# 타입을 string으로 설정할 경우
variable "admin" {
  type = "string"
  default = "admin@joinc.co.kr"
}

# 타입추론에 의해서 string 타입으로 설정된다.
variable "public_cidr" {
  default = "10.100.0.0/16"
}

자원을 전개할 리전(region), VPC와 subnet의 CIDR, AMI등을 변수로 설정했다.
# variables.tf
variable "aws_region" {
  description = "Region for VPC"
  default = "ap-northeast-1"
}

variable "vpc_cidr" {
  description = "CIDR for JOINC"
  default = "10.100.0.0/16"
} 

variable "public_subnet_cidr" {
  description = "CIDR for Joinc public subnet"
  default = "10.100.1.0/24"
}

variable "private_subnet_cidr" {
  description = "CIDR for Joinc private subnet"
  default = "10.100.2.0/24"
}

variable "ami" {
  description = "Amazon Linux AMI"
  default = "ami-047f7b46bd6dd5d84"
}

AWS provider 설정

테라폼은 베이메탈 시스템, VM, 네트워크 스위치, 컨테이너, 애저, GCP와 같은 다양한 인파라자원을 관리 할 수 있다. provider 를 이용해서 어떤 인프라자원을 사용 할 것인지를 선택 할 수 있다. 테라폼은 AWS를 포함해서 100개가 넘는 provider를 준비한다.

# provider.tf
provider "aws" {
  region = "${var.aws_region}"
}

VPC 정의

리소스(resource)는 테라폼에서 가장 중요한 요소다. 리소스는 네트워크, 인스턴스, 블럭 스토리지, DNS 레코드와 같은 인프라를 구성하는 객체들이다. AWS에서 vpc(가상네트워크)자원의 이름은 "aws_vpc"다. 리소스 문법의 구조는 아래와 같다.
# resource.tf
resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
}
위 resource 블럭은 로컬이름 "web"인 "aws_instance" 자원을 선언한다. resource 블럭안에는 자원의 속성을 설정하기 위한 아규먼트들이 있다. 위 예제에서 amiinstance_type는 aws_instance 리소스를 위해서 특별히 정의된 아규먼트들이다. 로컬이름은 같은 종류의 자원들을 서로 식별하기 위한 목적으로 사용한다.

이제 VPC를 만들어보자.
# resource.tf
resource "aws_vpc" "default" {
  cidr_block = "${var.vpc_cidr}"
  enable_dns_hostnames = true

  tags = {
    Name = "test-vpc"
  }
}
vpc 리소스의 이름은 "aws_vpc"다 aws_vpc 자원은 cidr_blockenable_dns_hostname 아규먼트를 가진다.

서브넷 생성

퍼블릭 서브넷과 프라이빗 서브넷을 만들자.

# resource.tf
resource "aws_subnet" "public-subnet" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.public_subnet_cidr}"
  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "Web Public Subnet"
  }
}

resource "aws_subnet" "private-subnet" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.private_subnet_cidr}"
  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "Database Private Subnet"
  }
}
퍼블릭 서브넷과 프라이빗 서브넷, 두 개의 서브넷을 만들었다. 인터넷 게이트웨이(Internet Gateway)의 유무로 퍼블릭 서브넷과 프라이빗 서브넷이 결정된다. 퍼블릭 서브넷의 경우 0.0.0.0\0 패킷이 인터넷 게이트웨이로 향하도록 라우팅 테이블을 만들면 된다.

인터넷 게이트웨이

# resource.tf
resource "aws_internet_gateway" "gw" {
  vpc_id = "${aws_vpc.default.id}"

  tags = {
    Name = "VPC IGW"
  }
}

라우트 테이블(Route Table)

# resource.tf
resource "aws_route_table" "web-public-rt" {
  vpc_id = "${aws_vpc.default.id}"

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = "${aws_internet_gateway.gw.id}"
  }

  tags = {
    Name = "Public Subnet Route Table"
  }
}

resource "aws_route_table_association" "web-public-rt" {
  subnet_id = "${aws_subnet.public-subnet.id}"
  route_table_id = "${aws_route_table.web-public-rt.id}"
}
라우트 테이블은 2개의 과정을 통해서 완성된다.

  1. 라우팅 정책을 담고 있는 라우트 테이블(route table) 작성 : 라우트 테이블은 CIDR 주소목적지 두 개의 필드로 구성된 하나 이상의 레코드로 구성된다. CIDR로 표현된 주소로 향하는 패킷은 목적지로 보낸다는 의미다. 위 예제에 있는 "aws_route_table.web-public-rt"은 아래의 내용을 담고 있다.
CIDR Block 목적지(Target) 설명
0.0.0.0/0 aws_internet_gateway.gw.id 0.0.0.0/0(인터넷) 으로 향하는 패킷은 인터넷 게이트웨이로 보내라
2. 라우트 테이블을 적용할 서브넷을 연결(등록) 한다. 인터넷 게이트웨이로 향하는 정책을 담은 라우트 테이블에 퍼블릭 서브넷을 연결한다. 이제 퍼블릭 서브넷에서 발생하는 패킷 중 0.0.0.0/0으로 향하는 패킷은 인터넷 게이트웨이를 통해서 인터넷으로 나가게 된다.

시큐리티 그룹

이제 시큐리티 그룹을 설정한다.
WebServer 시큐리티 그룹
# resource.tf
resource "aws_security_group" "sgweb" {
  vpc_id = "${aws_vpc.default.id}"

  name = "vpc_test_web"
  description = "Allow incoming HTTP connections & SSH access"
  
  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
 
  tags = {
    Name = "Web Server SG"
  }
}
웹 서버의 시큐리티 그룹이다. HTTP(80)/HTTPS(443)와 관리자를 위한 SSH(22) 접근을 허용했다.

Database 시큐리티 그룹
# resource.tf
resource "aws_security_group" "sgdb" {
  vpc_id = "${aws_vpc.default.id}"

  name = "sg_test_web"
  description = "Allow traffic from public subnet"
  
  ingress {
    from_port = 3306
    to_port = 3306 
    protocol = "tcp"
    cidr_blocks = ["${var.public_subnet_cidr}"]
  }
 
  tags = {
    Name = "DB SG"
  }
}
MySQL RDS를 데이터베이스로 사용하려 한다. 퍼블릭 서브넷에서 포트 3306(MySQL 기본 서비스 포트)으로 향하는 패킷을 허용한다.

VPC 생성

현재 작업 디렉토리에는 세 개의 파일이 있다. 제일 먼저 init명령을 이용해서, 현재 디렉토리에 있는 내용을 읽어서 초기화 한다.
$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (2.10.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.
......

plain 명령으로 실행계획를 작성 할 수 있다.
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_internet_gateway.gw
      id:                                         <computed>
      owner_id:                                   <computed>
      tags.%:                                     "1"
      tags.Name:                                  "VPC IGW"
      vpc_id:                                     "${aws_vpc.default.id}"
......

apply명령으로 자원을 전개한다.
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

aws_vpc.default: Refreshing state... (ID: vpc-0f26506a082462cce)
aws_subnet.private-subnet: Refreshing state... (ID: subnet-0060964dea16492a2)
aws_subnet.public-subnet: Refreshing state... (ID: subnet-0853434d40094a98a)
aws_internet_gateway.gw: Refreshing state... (ID: igw-0323bd2060c3dcdf3)
aws_security_group.sgweb: Refreshing state... (ID: sg-04580dbf77dbf2fb0)
aws_security_group.sgdb: Refreshing state... (ID: sg-0b55bb1ce6aee68f1)
aws_route_table.web-public-rt: Refreshing state... (ID: rtb-0523a5d6eab071f96)
aws_route_table_association.web-public-rt: Refreshing state... (ID: rtbassoc-0ab1eada885d7a1b9)
......

AWS 웹 콘솔에 접근해서 VPC 생성 상태를 확인했다.

 만들어진 VPC

 만들어진 VPC

AWS CLI 명령으로 제사한 VPC 정보를 확인했다.
aws> ec2 describe-vpcs --vpc-ids vpc-0f1cd88d7b65d59da
{
    "Vpcs": [
        {
            "VpcId": "vpc-0f1cd88d7b65d59da", 
            "InstanceTenancy": "default", 
            "Tags": [
                {
                    "Value": "test-vpc", 
                    "Key": "Name"
                }
            ], 
            "CidrBlockAssociationSet": [
                {
                    "AssociationId": "vpc-cidr-assoc-0dfb90595865e2e7b", 
                    "CidrBlock": "10.100.0.0/16", 
                    "CidrBlockState": {
                        "State": "associated"
                    }
                }
            ], 
            "State": "available", 
            "DhcpOptionsId": "dopt-535a9438", 
            "OwnerId": "522373083963", 
            "CidrBlock": "10.100.0.0/16", 
            "IsDefault": false
        }
    ]
}
VPC의 서브넷 정보를 확인해보자.
aws> ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-0f1cd88d7b65d59da"
{
    "Subnets": [
        {
            "MapPublicIpOnLaunch": false, 
            "AvailabilityZoneId": "apne2-az1", 
            "Tags": [
                {
                    "Value": "Database Private Subnet", 
                    "Key": "Name"
                }
            ], 
            "AvailableIpAddressCount": 251, 
            "DefaultForAz": false, 
            "SubnetArn": "arn:aws:ec2:ap-northeast-2:522373083963:subnet/subnet-0e8ad94b2e135e1bf", 
            "Ipv6CidrBlockAssociationSet": [], 
            "VpcId": "vpc-0f1cd88d7b65d59da", 
            "State": "available", 
            "AvailabilityZone": "ap-northeast-2a", 
            "SubnetId": "subnet-0e8ad94b2e135e1bf", 
            "OwnerId": "522373083963", 
            "CidrBlock": "10.100.2.0/24", 
            "AssignIpv6AddressOnCreation": false
        }, 
        {
            "MapPublicIpOnLaunch": false, 
            "AvailabilityZoneId": "apne2-az1", 
            "Tags": [
                {
                    "Value": "Web Public Subnet", 
                    "Key": "Name"
                }
            ], 
            "AvailableIpAddressCount": 251, 
            "DefaultForAz": false, 
            "SubnetArn": "arn:aws:ec2:ap-northeast-2:522373083963:subnet/subnet-0320c7438c5101c68", 
            "Ipv6CidrBlockAssociationSet": [], 
            "VpcId": "vpc-0f1cd88d7b65d59da", 
            "State": "available", 
            "AvailabilityZone": "ap-northeast-2a", 
            "SubnetId": "subnet-0320c7438c5101c68", 
            "OwnerId": "522373083963", 
            "CidrBlock": "10.100.1.0/24", 
            "AssignIpv6AddressOnCreation": false
        }
    ]
}
제대로 만들어졌다. 라우팅테이블을 살펴본다.
aws> ec2 describe-route-tables --filters "Name=vpc-id,Values=vpc-0f1cd88d7b65d59da"
{
    "RouteTables": [
        {
            "Associations": [
                {
                    "RouteTableAssociationId": "rtbassoc-0b5ff063a8cad73f8", 
                    "Main": true, 
                    "RouteTableId": "rtb-0549640f665dbbd3d"
                }
            ], 
            "RouteTableId": "rtb-0549640f665dbbd3d", 
            "VpcId": "vpc-0f1cd88d7b65d59da", 
            "PropagatingVgws": [], 
            "Tags": [], 
            "Routes": [
                {
                    "GatewayId": "local", 
                    "DestinationCidrBlock": "10.100.0.0/16", 
                    "State": "active", 
                    "Origin": "CreateRouteTable"
                }
            ], 
            "OwnerId": "522373083963"
        }, 
        {
            "Associations": [
                {
                    "SubnetId": "subnet-0320c7438c5101c68", 
                    "RouteTableAssociationId": "rtbassoc-0e7137ff88df5024a", 
                    "Main": false, 
                    "RouteTableId": "rtb-0013b1c158113518a"
                }
            ], 
            "RouteTableId": "rtb-0013b1c158113518a", 
            "VpcId": "vpc-0f1cd88d7b65d59da", 
            "PropagatingVgws": [], 
            "Tags": [
                {
                    "Value": "Public Subnet Route Table", 
                    "Key": "Name"
                }
            ], 
            "Routes": [
                {
                    "GatewayId": "local", 
                    "DestinationCidrBlock": "10.100.0.0/16", 
                    "State": "active", 
                    "Origin": "CreateRouteTable"
                }, 
                {
                    "GatewayId": "igw-0c37fc3409f9d1991", 
                    "DestinationCidrBlock": "0.0.0.0/0", 
                    "State": "active", 
                    "Origin": "CreateRoute"
                }
            ], 
            "OwnerId": "522373083963"
        }
    ]
}

EC2 인스턴스 전개

이렇게 VPC 설정이 끝났다. 이제 Web Public subnet에 EC 인스턴스를 전개해보자.
resource "aws_instance" "wb" {
  ami = "${var.ami}"
  instance_type = "t3.micro"
  key_name = "joinc_test"
  subnet_id = "${aws_subnet.public-subnet.id}"
  vpc_security_group_ids = ["${aws_security_group.sgweb.id}"] 
  associate_public_ip_address = true
  source_dest_check = false

  tags {
    Name = "webserver"
  }
}

aws_instance 리소스를 정의한다. 테라폼 파일을 수정했으므로 plan을 다시 수행해야 한다.
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

aws_vpc.default: Refreshing state... (ID: vpc-0f1cd88d7b65d59da)
aws_security_group.sgweb: Refreshing state... (ID: sg-0d84ed2263a81fbd0)
aws_security_group.sgdb: Refreshing state... (ID: sg-029d1dc0c3229efbe)

......

Plan: 2 to add, 0 to change, 0 to destroy.
Plan에 2개의 요소가 추가됐음 알 수 있다. apply 해보자.
$ terraform apply
aws_vpc.default: Refreshing state... (ID: vpc-0f1cd88d7b65d59da)
aws_security_group.sgdb: Refreshing state... (ID: sg-029d1dc0c3229efbe)
aws_subnet.public-subnet: Refreshing state... (ID: subnet-0320c7438c5101c68)
aws_subnet.private-subnet: Refreshing state... (ID: subnet-0e8ad94b2e135e1bf
......
aws_instance.wb: Still creating... (10s elapsed)
aws_instance.wb: Creation complete after 13s (ID: i-0c552d5b9165a285e)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

인스턴스 정보를 확인해보자.
aws> ec2 describe-instances --instance-ids i-0c552d5b9165a285e
{
    "Reservations": [
        {
            "Instances": [
                {
                    "Monitoring": {
                        "State": "disabled"
                    }, 
                    "PublicDnsName": "ec2-13-209-10-70.ap-northeast-2.compute.amazonaws.com", 
                    "State": {
                        "Code": 16, 
                        "Name": "running"
                    }, 
                    "EbsOptimized": false, 
                    "LaunchTime": "2019-05-11T17:33:49.000Z", 
                    "PublicIpAddress": "13.209.10.70", 
                    "PrivateIpAddress": "10.100.1.207", 
                    "ProductCodes": [], 
                    "VpcId": "vpc-0f1cd88d7b65d59da", 
                    "CpuOptions": {
                        "CoreCount": 1, 
                        "ThreadsPerCore": 2
                    }, 
                    "StateTransitionReason": "", 
                    "InstanceId": "i-0c552d5b9165a285e", 
                    "EnaSupport": true, 
                    "ImageId": "ami-047f7b46bd6dd5d84", 
                    "PrivateDnsName": "ip-10-100-1-207.ap-northeast-2.compute.internal", 
                    "KeyName": "joinc_test", 
                    "SecurityGroups": [
                        {
                            "GroupName": "vpc_test_web", 
                            "GroupId": "sg-0d84ed2263a81fbd0"
                        }
                    ], 
                    "ClientToken": "", 
                    "SubnetId": "subnet-0320c7438c5101c68", 
                    "InstanceType": "t3.micro", 
                    "CapacityReservationSpecification": {
                        "CapacityReservationPreference": "open"
                    }, 
                    "NetworkInterfaces": [
                        {
                            "Status": "in-use", 
                            "MacAddress": "02:38:10:68:11:60", 
                            "SourceDestCheck": false, 
                            "VpcId": "vpc-0f1cd88d7b65d59da", 
                            "Description": "", 
                            "NetworkInterfaceId": "eni-0079892c314584b57", 
                            "PrivateIpAddresses": [
                                {
                                    "PrivateDnsName": "ip-10-100-1-207.ap-northeast-2.compute.internal", 
                                    "PrivateIpAddress": "10.100.1.207", 
                                    "Primary": true, 
                                    "Association": {
                                        "PublicIp": "13.209.10.70", 
                                        "PublicDnsName": "ec2-13-209-10-70.ap-northeast-2.compute.amazonaws.com", 
                                        "IpOwnerId": "amazon"
                                    }
                                }
                            ], 
                            "PrivateDnsName": "ip-10-100-1-207.ap-northeast-2.compute.internal", 
                            "Attachment": {
                                "Status": "attached", 
                                "DeviceIndex": 0, 
                                "DeleteOnTermination": true, 
                                "AttachmentId": "eni-attach-0e675fcef4ddfb486", 
                                "AttachTime": "2019-05-11T17:33:49.000Z"
                            }, 
                            "Groups": [
                                {
                                    "GroupName": "vpc_test_web", 
                                    "GroupId": "sg-0d84ed2263a81fbd0"
                                }
                            ], 
                            "Ipv6Addresses": [], 
                            "OwnerId": "522373083963", 
                            "PrivateIpAddress": "10.100.1.207", 
                            "SubnetId": "subnet-0320c7438c5101c68", 
                            "Association": {
                                "PublicIp": "13.209.10.70", 
                                "PublicDnsName": "ec2-13-209-10-70.ap-northeast-2.compute.amazonaws.com", 
                                "IpOwnerId": "amazon"
                            }
                        }
                    ], 
                    "SourceDestCheck": false, 
                    "Placement": {
                        "Tenancy": "default", 
                        "GroupName": "", 
                        "AvailabilityZone": "ap-northeast-2a"
                    }, 
                    "Hypervisor": "xen", 
                    "BlockDeviceMappings": [
                        {
                            "DeviceName": "/dev/xvda", 
                            "Ebs": {
                                "Status": "attached", 
                                "DeleteOnTermination": true, 
                                "VolumeId": "vol-0e47abd7128e55c32", 
                                "AttachTime": "2019-05-11T17:33:50.000Z"
                            }
                        }
                    ], 
                    "Architecture": "x86_64", 
                    "RootDeviceType": "ebs", 
                    "RootDeviceName": "/dev/xvda", 
                    "VirtualizationType": "hvm", 
                    "Tags": [
                        {
                            "Value": "webserver", 
                            "Key": "Name"
                        }
                    ], 
                    "HibernationOptions": {
                        "Configured": false
                    }, 
                    "AmiLaunchIndex": 0
                }
            ], 
            "ReservationId": "r-09be5ae37a858d098", 
            "Groups": [], 
            "OwnerId": "522373083963"
        }
    ]
}
잘 만들어진 것 같다. 연결 테스트를 해봤다.
$ ssh -i key/joinc_test.pem ec2-user@13.209.10.70
The authenticity of host '13.209.10.70 (13.209.10.70)' can't be established.
ECDSA key fingerprint is SHA256:8JTLKtqgEQdZXNkTjOJCQwNa5P9nwNJzpR6dHRCVVyM.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '13.209.10.70' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-10-100-1-207 ~]$ 

HCL (HashiCorp Configuration Language)

테라폼을 이용한 인프라관리의 중심에는 HCL이 있다. HCL은 HashCorp Configuration Language로 인프라를 정의하기 위한 목적으로 만들어진 특수목적의 언어다. 특수한 목적의 언어가 그렇듯이 사람이 읽기 쉽게 만들어졌다. 일종의 DSL(Domain Specific Language - 도메인 특화언어)라고 할 수 있다. 선언적 구성을 위한 DSL 이므로 YAML이나 JSON 과 유사하다. 실제 HCL 대신 JSON을 사용 할 수 있다. 실제 JSON과 매우 비슷하다.

기본 문법은 아래와 같다.
# AMI 
variable "ami" {
  description = "the AMI to use"
}

/* 멀티라인 
   주석 */ 
resource "aws_instance" "web" {
  ami               = "${var.ami}"
  count             = 2
  source_dest_check = false

  connection {
    user = "root"
  }
}
HCL의 상세 구성요소는 다른 문서에서 살펴봐야 겠다.

정리