Amazon RDS에서 민감한 데이터 보호

60059 단어 securityawsrdspostgres
에서 EKS 클러스터를 만들었습니다. 이 부분에서는 Amazon RDS 인스턴스를 구성합니다.

다음 리소스가 생성됩니다.
  • 프라이빗 다중 AZ RDS PostgreSQL 인스턴스.
  • VPC 서브넷 보안 그룹.
  • (선택 사항) 개인 RDS를 특정 범위의 IP 주소에 노출하는 네트워크 로드 밸런서.
  • (선택 사항) NLB 대상 그룹을 RDS 개인 IP로 채우는 Lambda입니다.



  • 아마존 RDS


  • 사용된 Amazon RDS 인스턴스는 PostgreSQL 데이터베이스 서버입니다.
  • 다중 Az 옵션이 활성화되어 고가용성을 보장합니다.
  • 인스턴스에 공개적으로 액세스할 수 없으며 전용 서브넷에서 호스팅됩니다.
  • 모든 인증은 IAM 데이터베이스 인증을 통해 수행됩니다.
  • 자동 백업이 활성화되었습니다.

  • terraform 파일 생성infra/plan/rds.tf
    resource "random_string" "db_suffix" {
      length = 4
      special = false
      upper = false
    }
    
    resource "random_string" "root_username" {
      length = 12
      special = false
      upper = true
    }
    
    resource "random_password" "root_password" {
      length = 12
      special = true
      upper = true
    }
    
    resource "aws_db_instance" "postgresql" {
    
      # Engine options
      engine                              = "postgres"
      engine_version                      = "12.5"
    
      # Settings
      name                                = "postgresql${var.env}"
      identifier                          = "postgresql-${var.env}"
    
      # Credentials Settings
      username                            = "u${random_string.root_username.result}"
      password                            = "p${random_password.root_password.result}"
    
      # DB instance size
      instance_class                      = "db.m5.large"
    
      # Storage
      storage_type                        = "gp2"
      allocated_storage                   = 100
      max_allocated_storage               = 200
    
      # Availability & durability
      multi_az                            = true
    
      # Connectivity
      db_subnet_group_name                = aws_db_subnet_group.sg.id
    
      publicly_accessible                 = false
      vpc_security_group_ids              = [aws_security_group.sg.id]
      port                                = var.rds_port
    
      # Database authentication
      iam_database_authentication_enabled = true 
    
      # Additional configuration
      parameter_group_name                = "default.postgres12"
    
      # Backup
      backup_retention_period             = 14
      backup_window                       = "03:00-04:00"
      final_snapshot_identifier           = "postgresql-final-snapshot-${random_string.db_suffix.result}" 
      delete_automated_backups            = true
      skip_final_snapshot                 = false
    
      # Encryption
      storage_encrypted                   = true
    
      # Maintenance
      auto_minor_version_upgrade          = true
      maintenance_window                  = "Sat:00:00-Sat:02:00"
    
      # Deletion protection
      deletion_protection                 = false
    
      tags = {
        Environment = var.env
      }
    }
    


    다음 출력 추가

    output "rds-username" {
        value = "u${random_string.root_username.result}"
    }
    
    output "rds-password" {
        value = "p${random_password.root_password.result}"
    }
    
    output "private-rds-endpoint" {
        value = aws_db_instance.postgresql.address
    }
    


    DB 서브넷 그룹



    프라이빗 서브넷에 Amazon RDS 인스턴스를 배포합니다.

    resource "aws_db_subnet_group" "sg" {
      name       = "postgresql-${var.env}"
      subnet_ids = [aws_subnet.private["private-rds-1"].id, aws_subnet.private["private-rds-2"].id]
    
      tags = {
        Environment = var.env
        Name        = "postgresql-${var.env}"
      }
    }
    


    VPC 보안 그룹



    VPC 보안 그룹에서 다음을 허용합니다.
  • RDS 퍼블릭 서브넷이 있는 포트 5432의 인바운드/아웃바운드 트래픽.
  • RDS 프라이빗 서브넷 간의 인바운드/아웃바운드 TCP 네트워크 트래픽.

  • resource "aws_security_group" "sg" {
      name        = "postgresql-${var.env}"
      description = "Allow inbound/outbound traffic"
      vpc_id      = aws_vpc.main.id
    
      ingress {
        from_port       = var.rds_port
        to_port         = var.rds_port
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.private["private-rds-1"].cidr_block]  
      }
    
      ingress {
        from_port       = var.rds_port
        to_port         = var.rds_port
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.private["private-rds-2"].cidr_block]  
      }
    
      ingress {
        from_port       = var.rds_port
        to_port         = var.rds_port
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.public["public-rds-1"].cidr_block]  
      }
    
      ingress {
        from_port       = var.rds_port
        to_port         = var.rds_port
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.public["public-rds-2"].cidr_block]  
      }
    
      egress {
        from_port       = 0
        to_port         = 65535
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.private["private-rds-1"].cidr_block]  
      }
    
      egress {
        from_port       = 0
        to_port         = 65535
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.private["private-rds-2"].cidr_block]  
      }
    
      egress {
        from_port       = var.rds_port
        to_port         = var.rds_port
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.public["public-rds-1"].cidr_block]  
      }
    
      egress {
        from_port       = var.rds_port
        to_port         = var.rds_port
        protocol        = "tcp"
        cidr_blocks = [aws_subnet.public["public-rds-2"].cidr_block]  
      }
    
      tags = {
        Name        = "postgresql-${var.env}"
        Environment = var.env
      }
    }
    


    (선택 사항) RDS 인스턴스 노출



    로컬 시스템에서 또는 외부 CI/CD 도구를 통해 RDS 인스턴스 데이터베이스에 액세스하려는 경우 외부 네트워크 로드 밸런서를 생성하고 RDS 인스턴스의 사설 IP 주소를 대상으로 지정할 수 있습니다. 인스턴스가 실패하면 네트워크 인터페이스의 프라이빗 IP 주소가 변경될 수 있으므로 Lambda 함수를 배포하여 현재 프라이빗 IP 주소를 지속적으로 확인하고 이전 IP 주소를 등록 해제하고 새 프라이빗 IP 주소로 새 대상을 등록할 수 있습니다.

    네트워크 로드 밸런서



    RDS 프라이빗 IP 주소에 도달하려면 RDS 인스턴스와 외부 네트워크 로드 밸런서가 동일한 가용 영역에 있어야 합니다. 따라서 NLB는 기본 RDS 인스턴스와 동일한 서브넷에 배포됩니다.

    대상 유형이 IP 주소인 대상 그룹을 생성합니다. NLB와 RDS 간의 연결을 모니터링하기 위해 Cloud Watch 경보가 추가되었습니다.

    파일 생성infra/plan/nlb.tf
    locals {
        subnet_id = aws_subnet.public["public-rds-1"].availability_zone == aws_db_instance.postgresql.availability_zone ? aws_subnet.public["public-rds-1"].id : aws_subnet.public["public-rds-2"].id
    }
    
    resource "aws_lb" "rds" {
      name               = "nlb-expose-rds-${var.env}"
      internal           = false
      load_balancer_type = "network"
      subnets            = [local.subnet_id]
    
      enable_deletion_protection = false
    
      tags = {
        Environment = var.env
      }
    }
    
    resource "aws_lb_listener" "rds" {
      load_balancer_arn = aws_lb.rds.id
      port              = var.rds_port
      protocol          = "TCP"
    
      default_action {
        target_group_arn = aws_lb_target_group.rds.id
        type             = "forward"
      }
    }
    
    resource "aws_lb_target_group" "rds" {
      name                 = "expose-rds-${var.env}"
      port                 = var.rds_port
      protocol             = "TCP"
      target_type          = "ip"
      vpc_id               = aws_vpc.main.id
      health_check {
        enabled             = true
        protocol            = "TCP"
      }
      tags = {
        Environment = var.env
      }
    }
    
    resource "aws_cloudwatch_metric_alarm" "rds-access" {
      alarm_name                = "rds-external-access-status"
      comparison_operator       = "GreaterThanOrEqualToThreshold"
      evaluation_periods        = "1"
      metric_name               = "UnHealthyHostCount"
      namespace                 = "AWS/NetworkELB"
      period                    = "60"
      statistic                 = "Maximum"
      threshold                 = 1
      alarm_description         = "Monitoring RDS External Access"
      treat_missing_data        = "breaching"
    
      dimensions = {
        TargetGroup  = aws_lb_target_group.rds.arn_suffix
        LoadBalancer = aws_lb.rds.arn_suffix
      }
    }
    


    파일 완성infra/plan/output
    output "public-rds-endpoint" {
        value = "${element(split("/", aws_lb.rds.arn), 2)}-${element(split("/", aws_lb.rds.arn), 3)}.elb.${var.region}.amazonaws.com"
    }
    


    이제 대상을 등록해야 합니다. Lambda 함수를 사용하여 작업을 수행할 수 있습니다. Amazon CloudWatch 이벤트 규칙이 추가되어 15분마다 Lambda 함수를 호출합니다.

    람다 함수



    파일 생성infra/plan/lambda.tf
    data "archive_file" "lambda_zip" {
        type          = "zip"
        source_file   = "${path.module}/lambda/populate-nlb-tg-with-rds-private-ip.py"
        output_path   = "lambda_function_payload.zip"
    }
    
    resource "aws_lambda_function" "rds" {
      filename      = "lambda_function_payload.zip"
      function_name = "populate-nlb-tg-with-rds-private-ip"
      role          = aws_iam_role.iam_for_lambda.arn
      handler       = "populate-nlb-tg-with-rds-private-ip.handler"
    
      source_code_hash = data.archive_file.lambda_zip.output_base64sha256
    
      runtime = "python3.8"
    
      timeout = 300
    
      environment {
        variables = {
          RDS_PORT = var.rds_port
          NLB_TG_ARN = aws_lb_target_group.rds.arn
          RDS_SG_ID = aws_security_group.sg.id
          RDS_ID = aws_db_instance.postgresql.id
        }
      }
    
      tags = {
        Environment = var.env
      }
    }
    
    resource "aws_iam_role" "iam_for_lambda" {
      name = "iam_for_lambda"
    
      assume_role_policy = <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Action": "sts:AssumeRole",
          "Principal": {
            "Service": "lambda.amazonaws.com"
          },
          "Effect": "Allow",
          "Sid": ""
        }
      ]
    }
    EOF
    }
    
    resource "aws_iam_role_policy" "lambda_nlb" {
      name = "nlb-tg-access"
      role = aws_iam_role.iam_for_lambda.id
    
      policy = jsonencode({
        Version = "2012-10-17"
        Statement = [
          {
            Action = [
                "ec2:DescribeNetworkInterfaces",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:RegisterTargets",
                "rds:DescribeDBInstances"
            ]
            Effect   = "Allow"
            Resource = "*"
          },
        ]
      })
    }
    
    resource "aws_iam_role_policy" "lambda_logging" {
      name        = "lambda_logging"
      role = aws_iam_role.iam_for_lambda.id
    
      policy = jsonencode({
        Version = "2012-10-17"
        Statement = [
          {
            Action = [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ]
            Effect   = "Allow"
            Resource = "arn:aws:logs:*:*:*"
          },
        ]
      })
    }
    
    resource "aws_cloudwatch_log_group" "lambda" {
      name              = "/aws/lambda/${aws_lambda_function.rds.function_name}"
      retention_in_days = 1
    }
    
    resource "aws_cloudwatch_event_rule" "lambda" {
      name        = "populate-nlb-tg-with-rds-private-ip"
      description = "Populate NLB tg with RDS private IP"
      schedule_expression = "rate(15 minutes)"
    }
    
    resource "aws_cloudwatch_event_target" "lambda" {
      rule      = aws_cloudwatch_event_rule.lambda.name
      target_id = "Lambda"
      arn       = aws_lambda_function.rds.arn
    }
    
    resource "aws_lambda_permission" "cloudwatch" {
      statement_id  = "AllowExecutionFromCloudWatch"
      action        = "lambda:InvokeFunction"
      function_name = aws_lambda_function.rds.function_name
      principal     = "events.amazonaws.com"
      source_arn    = aws_cloudwatch_event_rule.lambda.arn
    }
    


    람다 함수는 파이썬으로 작성되었습니다. 프로세스는 다음과 같습니다.
  • describe_target_health 함수를 사용하여 현재 등록된 IP를 가져옵니다.
  • describe_db_instances 함수를 사용하여 현재 RDS 인스턴스 가용성 영역을 가져옵니다.
  • describe_network_interfaces 기능을 사용하여 현재 RDS 사설 IP를 검색합니다.
  • 이미 할당된 항목Registry Target이 없으면 새 항목을 만듭니다. 현재 등록 대상이 오래된 경우 등록을 취소하고 새 RDS 개인 IP 주소로 새 등록 대상을 만듭니다.

  • 파일 생성infra/plan/lambda/populate-nlb-tg-with-rds-private-ip.py
    import json
    import os
    import random
    import sys
    import boto3
    import logging
    
    from datetime import datetime
    from botocore.exceptions import ClientError
    
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    
    '''
    This function populates a Network Load Balancer's target group with RDS IP addresses
    
    Configure these environment variables in your Lambda environment
    
    1. NLB_TG_ARN - The ARN of the Network Load Balancer's target group
    2. RDS_PORT
    3. RDS_SG_ID - RDS VPC Security Group Id
    4. RDS_ID - RDS Identifier
    '''
    
    NLB_TG_ARN = os.environ['NLB_TG_ARN']
    RDS_PORT = int(os.environ['RDS_PORT'])
    RDS_SG_ID = os.environ['RDS_SG_ID']
    RDS_ID = os.environ['RDS_ID']
    
    try:
        elbv2client = boto3.client('elbv2')
    except ClientError as e:
        logger.error(e.response['Error']['Message'])
        sys.exit(1)
    
    try:
        rdsclient = boto3.client('rds')
    except ClientError as e:
        logger.error(e.response['Error']['Message'])
        sys.exit(1)
    
    try:
        ec2client = boto3.client('ec2')
    except ClientError as e:
        logger.error(e.response['Error']['Message'])
        sys.exit(1)
    
    
    def register_target(tg_arn, new_target_list):
    
        logger.info(f"INFO: Register new_target_list:{new_target_list}")
    
        try:
            elbv2client.register_targets(
                TargetGroupArn=tg_arn,
                Targets=new_target_list
            )
        except ClientError as e:
            logger.error(e.response['Error']['Message'])
    
    
    def deregister_target(tg_arn, new_target_list):
    
        try:
            logger.info(f"INFO: Deregistering targets: {new_target_list}")
            elbv2client.deregister_targets(
                TargetGroupArn=tg_arn,
                Targets=new_target_list
            )
        except ClientError as e:
            logger.error(e.response['Error']['Message'])
    
    
    def target_group_list(ip_list):
    
        target_list = []
        for ip in ip_list:
            target = {
                'Id': ip,
                'Port': RDS_PORT,
            }
            target_list.append(target)
        return target_list
    
    
    def get_registered_ips(tg_arn):
    
        registered_ip_list = []
        try:
            response = elbv2client.describe_target_health(
                TargetGroupArn=tg_arn)
            registered_ip_count = len(response['TargetHealthDescriptions'])
            logger.info(f"INFO: Number of currently registered IP: {registered_ip_count}")
            for target in response['TargetHealthDescriptions']:
                registered_ip = target['Target']['Id']
                registered_ip_list.append(registered_ip)
        except ClientError as e:
            logger.error(e.response['Error']['Message'])
        return registered_ip_list
    
    
    def get_rds_private_ips(rds_az):
    
        resp = ec2client.describe_network_interfaces(Filters=[{
            'Name': 'group-id',
            'Values': [RDS_SG_ID]
        }, {
            'Name': 'availability-zone',
            'Values': [rds_az]
        }])
        private_ip_address = []
        for interface in resp['NetworkInterfaces']:
            private_ip_address.append(interface['PrivateIpAddress'])
        return private_ip_address
    
    
    def get_rds_az():
    
        logger.info(f"INFO: Get RDS current AZ: {RDS_ID}")
        az = None
        try:
            response = rdsclient.describe_db_instances(
                DBInstanceIdentifier=RDS_ID
            )
            if len(response['DBInstances']) > 0:
                az = response['DBInstances'][0]['AvailabilityZone']
                logger.info(f"INFO: RDS AZ is: {az}")
    
        except ClientError as e:
            logger.error(e.response['Error']['Message'])
    
        return az
    
    
    def handler(event, context):
    
        registered_ip_list = get_registered_ips(NLB_TG_ARN)
        current_rds_az = get_rds_az()
        new_active_ip_set = get_rds_private_ips(current_rds_az)
    
        registration_ip_list = []
        # IPs that have not been registered
        if len(registered_ip_list) == 0 or registered_ip_list != new_active_ip_set:
            registration_ip_list = new_active_ip_set
    
        if registration_ip_list:
            registerTarget_list = target_group_list(registration_ip_list)
            register_target(NLB_TG_ARN, registerTarget_list)
            logger.info(f"INFO: Registering {registration_ip_list}")
        else:
            logger.info(f"INFO: No new target registered")
    
        deregistration_ip_list = []
        if registered_ip_list != new_active_ip_set:
            for ip in registered_ip_list:
                deregistration_ip_list.append(ip)
                logger.info(f"INFO: Deregistering IP: {ip}")
                deregisterTarget_list = target_group_list(deregistration_ip_list)
                deregister_target(NLB_TG_ARN, deregisterTarget_list)
        else:
            logger.info(f"INFO: No old target deregistered")
    


    파일 완성infra/plan/variable.tf:

    variable "rds_port" {
      type    = number
      default = 5432
    }
    


    RDS 인스턴스를 배포해 보겠습니다.

    cd infra/envs/dev
    
    terraform apply ../../plan/ 
    


    다음 부분으로 이동하기 전에 Amazon RDS 인스턴스에 metabase 데이터베이스를 생성해야 합니다.

    PGPASSWORD=$(terraform output rds-password) psql --host $(terraform output public-rds-endpoint) --port 5432 --user $(terraform output rds-username) --dbname postgres
    CREATE USER metabase;
    GRANT rds_iam TO metabase;
    CREATE DATABASE metabase;
    GRANT ALL ON DATABASE metabase TO metabase;
    


    모든 리소스가 생성되고 올바르게 작동하는지 확인하겠습니다.

    RDS 인스턴스



    VPC 보안 그룹





    람다



    NLB 대상 그룹



    결론



    이제 RDS 인스턴스를 사용할 수 있습니다. 에서는 Amazon EKS에 배포된 컨테이너와 Amazon RDS 인스턴스에서 생성된 데이터베이스 간에 연결을 설정합니다.

    좋은 웹페이지 즐겨찾기