• Home
  • About
    • lahuman photo

      lahuman

      열심히 사는 아저씨

    • Learn More
    • Facebook
    • LinkedIn
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

#8-1 Working with Multiple Providers

12 Dec 2022

Reading time ~11 minutes

Working with Multiple Providers

  • 참고 링크
    • Code Github - 원서

0. 소개

single provider 블록

provider "aws" {
  region = "us-east-2"
}
  • What if you need to deploy to multiple AWS regions?
  • What if you need to deploy to multiple AWS accounts?
  • What if you need to deploy to other clouds, such as Azure or GCP?

1. Working with One Provider

1.1 What is a Provider?

테라폼 구성

  1. Core : 테라폼 바이너리이며, 모든 플랫폼에서 테라폼의 기본 기능을 제공(예. CMD)

     This is the terraform **binary**, and it provides all the basic functionality in Terraform that is used by all platforms, 
     such as a command-line interface (i.e., plan, apply, etc.), a parser and interpreter for Terraform code (HCL), 
     the ability to build a dependency graph from resources and data sources, logic to read and write state files, and so on.
     Under the hood, the code is written in Go and lives in an open source GitHub repo owned and maintained by HashiCorp.
    
  2. Provider : 테라폼 코어를 위한 플러그인, 각 플러그인은 ‘AWS, Azure, GCP’ 등 외부 플랫폼과 통신

graph LR; subgraph terraform code; A[Terraform] ; end; subgraph terraform providers; B(AWS provider); C(Azure provider); D(Google Cloud provider); end; A -->|RPC| B & C & D; E((AWS)); F((Azure)); G((Google\nCloud)); B --> |HTTP| E; C --> |HTTP| F; D --> |HTTP| G;
- 초기에는 HashiCorp 가 프로바이더를 만들었지만, 지금은 주요 공급자의 경우 해당 공급자 회사에서 직접 프로바이더를 관리
- 각 공급자는 특정 접두사를 할당하고, 해당 접두사를 포함하는 하나 이상의 리소스 및 데이터 소스를 표현합니다.
- 예시 **aws_ prefix** : aws_instance, aws_autoscaling_group, aws_ami
- 예시 **azurerm_ prefix** : azurerm_virtual_machine, azurerm_virtual_machine_scale_set, azurerm_image

```bash
The Terraform core communicates with plugins via remote procedure calls (RPCs), and those plugins, 
in turn, communicate with their corresponding platforms via the network (e.g., via HTTP calls), as shown in Figure 7-1.
```

1.2 How Do You Install Providers?

  • Provider 설정
    • provider 선언 후 terraform init 시 자동으로 provider 의 코드를 다운로드
      provider "aws" {
        region = "us-east-2"
      }
        
      $ terraform init
        
      Initializing provider plugins...
      - Finding hashicorp/aws versions matching "4.19.0"...
      - Installing hashicorp/aws v4.19.0...
      - Installed hashicorp/aws v4.19.0 (signed by HashiCorp)
      ...
    
    • provider 상세 설정
      terraform {
        required_providers {
          <LOCAL_NAME> = {
            source  = "<URL>"
            version = "<VERSION>"
          }
        }
      }
    
    • LOCAL_NAME : provider 가 사용할 local name 이며, 각 provider 에서 유일한 이름이여야 함. 드문 경우 지만 만약 두 공급자가 http 중복 사용 시 로컬 이름으로 구별 할 수 있음
    • URL : 해당 provider 다운로드 URL 이며, 형식은 [<HOSTNAME>/]<NAMESPACE>/<TYPE 이다
      • HOSTNAME 은 공급자를 배포하는 테라폼 레지스트리의 호스트 이름, 생략 시 테라폼 공개 레지스트리를 가리킴
      • NAMESPACE 는 조직 네임스페이스(일반적으로 회사 이름)
      • TYPE은 공급자가 관리하는 플랫폼의 이름(일반적으로 TYPE는 기본 로컬 이름)
      • 예 aws 공급자 전체 URL) registry.terraform.io/hashicorp/aws → 생략 표현 가능 hashicorp/aws
    • VESION : 버전 조건, 예를 들어 4.19.0 처럼 특정 버전을 지정하거나, > 4.0 , < 4.3 처럼 버전 범위를 지정
    • 아래는 aws 4.x 공급자 버전 설치를 지정하는 코드 예시
      terraform {
        required_providers {
          aws = {
            source  = "hashicorp/aws"
            version = "~> 4.0"
          }
        }
      }
    

1.3 How Do You Use Providers?

사용

  1. required_providers 블록을 설정

     terraform {
       required_providers {
         aws = {
           source  = "hashicorp/aws"
           version = "~> 4.0"
         }
       }
     }
    
  2. provider 블록을 설정

     provider "aws" {
       region = "us-east-2"
     }
    

2. Working with Multiple copies of the same provider

2.1 Working with Multiple AWS Regions

  • [실습] 2개의 리전에 배포 시

      provider "aws" {
        region = "us-east-2"
      }
        
      provider "aws" {
        region = "us-west-1"
      }
    
    • 리전간 ‘리소스, 데이터 소스, 모듈’에 대한 구별? ⇒ alias 활용
      provider "aws" {
        region = "us-east-2"
        alias  = "region_1"
      }
        
      provider "aws" {
        region = "us-west-1"
        alias  = "region_2"
      }
        
      data "aws_region" "region_1" {
        provider = aws.region_1
      }
        
      data "aws_region" "region_2" {
        provider = aws.region_2
      }
        
      output "region_1" {
        value       = data.aws_region.region_1.name
        description = "The name of the first region"
      }
        
      output "region_2" {
        value       = data.aws_region.region_2.name
        description = "The name of the second region"
      }
    
    • 실습 따라하기
      # 이동
      cd ~/terraform-up-and-running-code/code/terraform/07-working-with-multiple-providers
        
      # 테라폼 코드 파일 생성
      cat <<EOT > multple-region.tf
      terraform {
        required_version = ">= 1.0.0, < 2.0.0"
        
        required_providers {
          aws = {
            source  = "hashicorp/aws"
            version = "~> 4.0"
          }
        }
      }
        
      provider "aws" {
        region = "us-east-2"
        alias  = "region_1"
      }
        
      provider "aws" {
        region = "us-west-1"
        alias  = "region_2"
      }
        
      data "aws_region" "region_1" {
        provider = aws.region_1
      }
        
      data "aws_region" "region_2" {
        provider = aws.region_2
      }
        
      output "region_1" {
        value       = data.aws_region.region_1.name
        description = "The name of the first region"
      }
        
      output "region_2" {
        value       = data.aws_region.region_2.name
        description = "The name of the second region"
      }
      EOT
        
      # init & plan & apply
      tfenv use 1.2.3
      **terraform init
      terraform plan & terraform apply -auto-approve**
      Outputs:
      region_1 = "us-east-2"
      region_2 = "us-west-1"
    
  • [실습] 2개의 리전에 Ubuntu EC2 배포 시
    • 리전 별 AMI ID값이 다르므로, 필터를 활용하자
      data "aws_ami" "ubuntu_region_1" {
        provider = aws.region_1
        
        most_recent = true
        owners      = ["099720109477"] # Canonical
        
        filter {
          name   = "name"
          values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
        }
      }
        
      data "aws_ami" "ubuntu_region_2" {
        provider = aws.region_2
        
        most_recent = true
        owners      = ["099720109477"] # Canonical
        
        filter {
          name   = "name"
          values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
        }
    
    • ami 참조
      resource "aws_instance" "region_1" {
        provider = aws.region_1
        
        ami           = data.aws_ami.ubuntu_region_1.id
        instance_type = "t2.micro"
      }
        
      resource "aws_instance" "region_2" {
        provider = aws.region_2
        
        ami           = data.aws_ami.ubuntu_region_2.id
        instance_type = "t2.micro"
      }
    
    • (옵션) output 으로 배포된 가용 영역(az) 확인
      output "instance_region_1_az" {
        value       = aws_instance.region_1.availability_zone
        description = "The AZ where the instance in the first region deployed"
      }
        
      output "instance_region_2_az" {
        value       = aws_instance.region_2.availability_zone
        description = "The AZ where the instance in the second region deployed"
      }
    
    • 실습 따라하기
      # [터미널1] us-east-2
      while true; do aws ec2 describe-instances --region us-east-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done
        
      # [터미널2] us-west-1 
      while true; do aws ec2 describe-instances --region us-west-1  --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done
        
      # 이동
      cd ~/terraform-up-and-running-code/code/terraform/07-working-with-multiple-providers/examples/multi-region
      cat main.tf outputs.tf
        
      # init & plan & apply
      terraform init 
      terraform plan && terraform apply -auto-approve
      Outputs:
        
      instance_region_1_az = "us-east-2c"
      instance_region_2_az = "us-west-1c"
      region_1 = "us-east-2"
      region_2 = "us-west-1"
        
      # 삭제 : 아래 RDS 배포 후 데이터 INSERT 등 실습 후 삭제해도 됨
      terraform destroy -auto-approve
    
  • [실습] Primary RDS 의 replica RDS를 다른 리전에 배포 시
    • Primary RDS 는 backup_retention_period 설정이 0보다 커야 되며, replicate_source_db ARN 설정 시 replica RDS 이다 - 링크

        variable "backup_retention_period" {
          description = "Days to retain backups. Must be > 0 to enable replication."
          type        = number
          default     = null
        }
              
        variable "replicate_source_db" {
          description = "If specified, replicate the RDS database at the given ARN."
          type        = string
          default     = null
        }
      
    • 모듈의 main.tf 에서 사용 확인 : Primary 와 replica RDS가 다르므로 분기 사용 - 링크

        resource "aws_db_instance" "example" {
          identifier_prefix   = "terraform-up-and-running"
          allocated_storage   = 10
          instance_class      = "db.t2.micro"
          skip_final_snapshot = true
              
          # Enable backups
          backup_retention_period = var.backup_retention_period
              
          # If specified, this DB will be a replica
          replicate_source_db = var.replicate_source_db
              
          # Only set these params if replicate_source_db is not set
          engine   = var.replicate_source_db == null ? "mysql" : null
          db_name  = var.replicate_source_db == null ? var.db_name : null
          username = var.replicate_source_db == null ? var.db_username : null
          password = var.replicate_source_db == null ? var.db_password : null
        }
      
    • 참고로 아래 값 미지정 시 null 지정됨 - 링크

        variable "db_name" {
          description = "Name for the DB."
          type        = string
          default     = null
        }
              
        variable "db_username" {
          description = "Username for the DB."
          type        = string
          sensitive   = true
          default     = null
        }
              
        variable "db_password" {
          description = "Password for the DB."
          type        = string
          sensitive   = true
          default     = null
        }
      
    • replica RDS 는 primary RDS의 ARM 정보 참조 필요 : output 출력 - 링크

        output "arn" {
          value       = aws_db_instance.example.arn
          description = "The ARN of the database"
        }
      
    • 실습 따라하기 : 백엔드 리소스 배포
    • S3/DynamoDB 생성
      • S3/DynamoDB 생성을 위한 환경변수 지정
        # 환경변수에 지정
        export TF_VAR_bucket_name=gasida-t101-tfstate
        export TF_VAR_table_name=gasida-t101-locks
              
        # 환경변수 확인
        export | grep TF_VAR_
      
      • 배포
        # 이동
        cd ~/terraform-up-and-running-code/code/terraform/03-terraform-state/file-layout-example/global/s3
        cat main.tf variables.tf
              
        # 초기화 및 검증 및 배포 : 환경변수 적용 확인
        terraform init
        terraform plan && terraform apply -auto-approve
              
        # 확인
        aws s3 ls
        aws dynamodb list-tables --output text
      
    • Primary RDS 리소스 생성 코드 내용 확인 - 링크

        module "mysql_primary" {
          source = "../../../../modules/data-stores/mysql"
              
          db_name     = var.db_name
          db_username = var.db_username
          db_password = var.db_password
              
          # Must be enabled to support replication
          backup_retention_period = 1
        }
      
    • Replica RDS 리소스 생성 코드 내용 확인

        module "mysql_replica" {
          source = "../../../../modules/data-stores/mysql"
              
          # Make this a replica of the primary
          replicate_source_db = module.mysql_primary.arn
        }
      
    • 리전 간 리소스 구분을 위해서 alias 사용 - 링크

        provider "aws" {
          region = "us-east-2"
          alias  = "primary"
        }
              
        provider "aws" {
          region = "us-west-1"
          alias  = "replica"
        }
              
        ...
              
        module "mysql_primary" {
          source = "../../../../modules/data-stores/mysql"
              
          providers = {
            aws = aws.primary
          }
              
          db_name     = var.db_name
              
          db_username = var.db_username
          db_password = var.db_password
              
          # Must be enabled to support replication
          backup_retention_period = 1
        }
              
        module "mysql_replica" {
          source = "../../../../modules/data-stores/mysql"
              
          providers = {
            aws = aws.replica
          }
              
          # Make this a replica of the primary
          replicate_source_db = module.mysql_primary.arn
        }
      
    • 마지막으로 output 출력 - 링크

        output "primary_address" {
          value       = module.mysql_primary.address
          description = "Connect to the primary database at this endpoint"
        }
              
        output "primary_port" {
          value       = module.mysql_primary.port
          description = "The port the primary database is listening on"
        }
              
        output "primary_arn" {
          value       = module.mysql_primary.arn
          description = "The ARN of the primary database"
        }
              
        output "replica_address" {
          value       = module.mysql_replica.address
          description = "Connect to the replica database at this endpoint"
        }
              
        output "replica_port" {
          value       = module.mysql_replica.port
          description = "The port the replica database is listening on"
        }
              
        output "replica_arn" {
          value       = module.mysql_replica.arn
          description = "The ARN of the replica database"
        }
      
    • 배포

        # [터미널1] us-east-2
        while true; do aws rds describe-db-instances --region us-east-2 --query "*[].[Endpoint.Address,Endpoint.Port,MasterUsername]" --output text  ; echo "------------------------------" ; sleep 1; done
              
        # [터미널2] us-west-1 
        while true; do aws rds describe-db-instances --region us-west-1 --query "*[].[Endpoint.Address,Endpoint.Port,MasterUsername]" --output text  ; echo "------------------------------" ; sleep 1; done
              
        # 환경변수에 지정
        export TF_VAR_db_username='cloudneta'
        export TF_VAR_db_password='cloudnetaQ!'
              
        # 이동
        cd ~/terraform-up-and-running-code/code/terraform/07-working-with-multiple-providers/live/prod/data-stores/mysql
              
        # main.tf 에 백엔드 부분 수정
        vi main.tf
          backend "s3" {
            # This backend configuration is filled in automatically at test time by Terratest. If you wish to run this example
            # manually, uncomment and fill in the config below.
            bucket         = "gasida-t101-tfstate"
            key            = "prod/data-stores/mysql/terraform.tfstate"
            region         = "us-east-2"
            dynamodb_table = "gasida-t101-locks"
            # encrypt        = true
          }
              
        # 초기화 및 검증 : 환경변수 적용 확인
        terraform init
        terraform plan
              
        # 배포 : 총 25분 정도 소요 (8분 후 primary 완료, 이후 16분 후 replica 생성 및 동기화 완료)
        terraform apply -auto-approve
        terraform output
        aws s3 ls s3://$TF_VAR_bucket_name --recursive --human-readable --summarize
              
        # (옵션) Primary RDS에 데이터 INSERT 후 Replica RDS에 복제되는지 확인 해볼것
              
        # 삭제 6분 정도 소요
        terraform destroy -auto-approve
              
        # S3/DynamoDB 삭제
        cd ~/terraform-up-and-running-code/code/terraform/03-terraform-state/file-layout-example/global/s3
        terraform destroy -auto-approve
      

2가지 주의 사항

  1. Warning 1 : Multiregion is hard 프로덕션 수준의 멀티 리전은 어렵다
    • Active-Active 멀티 리전 서비스를 위해서 ‘지역간 지연 시간, 고유 ID, 최종 일관성’ 등 여러가지 고려사항이 많아서 쉽지 않다.
  2. Warning 2 : Use aliases sparingly Alias 를 빈번하게 사용하지 말자
    • 별칭을 사용하여 두 리전에 배포하는 단일 테라폼 모듈은 한 리전이 다운 시, plan 과 apply 시도가 실패합니다
    • 프로덕션 환경은 멀티 리전의 별칭을 사용하는 것보다는, 3장에서 설명한 것 처럼 환경을 완전히 격리해야 합니다. 이를 통해 영향도를 최소화 할 수 있습니다.

2.2 Working with Multiple AWS Accounts

멀티 계정 사용 이유

  1. Isolation (aka compartmentalization)
    • Staging 과 Prod 간 다른 계정을 사용함으로써 영향도 최소화
  2. Authentication and authorization
    • 다중 계정에 따른 인증과 권한 분리로 영향도 최소화(예를 들면 권한을 잘못 부여 시) 및 계정 별 다른 권한으로 통제 가능
  3. Auditing and reporting
    • 모든 환경과 계정의 변경 사항 및 감사 추적을 위지하고, 규정 준수와 이상 징후 감시를 할 수 있고 비용 추적등에 용이
  • 사전 준비

    [중급 지식] IAM & Organizations

    1. AWS Account 최소 2개 이상 생성 해두기 (아래는 예시)

      가입 이메일명 AWS Account ID 별칭 IAM User(관리자 수준)
      gasida.seo@gmail.com 911283464785 awsgasida admin
      gasida.seo+aws1@gmail.com 565813864476 awsgasida2 admin
      gasida.seo+aws2@gmail.com 272704923229 awsgasida3 admin
       aws sts get-caller-identity --query Account --output text
       911283464785
      
    2. AWS Organizations 설정 및 계정 초대(추가) 해두기

    3. 계정 간 역할 전환을 위한 IAM Role 설정 해두기 : 세번째 계정에 IAM Role 설정하는 것 보여주기

    4. 첫 번째 계정에서 S3 버킷 정보 확인 → 두 번째 계정으로 역할 전환 후 S3 버킷 정보 확인 해보기 → 다시 전환 후 S3 버킷 정보 확인

  • 실습 따라하기

    • main.tf 파일 확인 : alias child 는 IAM Role 역할 전환 - 링크

        provider "aws" {
          region = "us-east-2"
          alias  = "parent"
        }
              
        provider "aws" {
          region = "us-east-2"
          alias  = "child"
              
          assume_role {
            role_arn = var.child_iam_role_arn
          }
        }
              
        data "aws_caller_identity" "parent" {
          provider = aws.parent
        }
              
        data "aws_caller_identity" "child" {
          provider = aws.child
        }
      
    • variables.tf 파일 확인 - 링크

        variable "**child_iam_role_arn**" {
          description = "The ARN of an IAM role to assume in the child AWS account"
          type        = string
        }
      
    • outputs.tf 파일 확인 : 각각의 caller 의 AWS Account 출력 - 링크

        output "parent_account_id" {
          value       = data.aws_caller_identity.parent.account_id
          description = "The ID of the parent AWS account"
        }
              
        output "child_account_id" {
          value       = data.aws_caller_identity.child.account_id
          description = "The ID of the child AWS account"
        }
      
    • 배포 및 확인

        # 이동
        cd ~/terraform-up-and-running-code/code/terraform/07-working-with-multiple-providers/examples/multi-account-root
        cat main.tf variables.tf
              
        # IAM Role ARN 지정
        #export TF_VAR_child_iam_role_arn='arn:aws:iam::<자신의 두번쨰 혹은 세번째 계정 Account ID>:role/<해당 계정의 IAM Role 이름>'
        export TF_VAR_child_iam_role_arn='arn:aws:iam::565813864476:role/ma-in-a2'
              
        # 배포 확인
        terraform init
        terraform plan && terraform apply -auto-approve
        Outputs:
        child_account_id = "565813864476"
        parent_account_id = "911283464785"
      

2가지 주의 사항

  1. Warning 1 : Cross-account IAM roles are double opt-in
    • 교차 계정 역할 동작을 위해서 2개의 계정에서 모두 권한 등 설정이 필요함
  2. Warning 2 : Use aliases sparingly Alias 를 빈번하게 사용하지 말자
    • 영향도 최소를 위해서 계정 간 분리를 하는 것을 권장

2.3 Creating modules that can work with multiple providers

2.2와 거의 유사하여 Skip

3. Working with multiple different providers

도커와 쿠버네티스 실습 환경에서 진행되어 Skip



terraformstudy Share Tweet +1