Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

AWS Transfer 패밀리로 SFTP 구성하기 #1

AWS Transfer 패밀리는 AWS에서 관리해주는 SFTP 서버로 S3를 스토리지로 사용할 수 있고 2018년에 공개되었다. 개발 자동화에는 API나 CLI를 이용하는 것이 더 편하지만, 업무의 성격상 SFTP를 운영해야 하는 경우가 있다.

SFTP를 운영하는 게 대단한 노력이 드는 것은 아니지만 또 용량관리나 계정 관리 등 은근히 관리하기 귀찮기도 하다. 그럴 때 AWS Transfer 서버는 괜찮은 대안이 될 수 있지만, 가격이 싸진 않다. SFTP에 접속할 수 있는 엔드포인트를 기준으로 시간당 $0.30이 과금되기 때문에 한 달을 운영한다면 $216가 되고 업로드에 기가당 $0.04가 들기 때문에 이득이 되는지 가격을 잘 계산해 봐야 한다. 회사에서 인프라가 커지면 $200가 큰 비용은 아니지만 그래도 하는 일에 비해서는 좀 비싸다는 생각은 든다.

작년에 필요해서 이 SFTP 서버 구성을 좀 만져봤는데 미루다가 이제야 정리를 한다. AWS Transfer 패밀리에서는 프로토콜도 SFTP, FTPS, FTP 중에서 선택해서 사용할 수 있지만, 서비스 프로바이더도 "Service managed"와 "Custom"으로 나뉜다. "Service managed"는 사용자 관리는 AWS Transfer 패밀리 내에서 다 할 수 있고 Custom은 Amazon API Gateway와 연결해서 사용하는 방식이다. 장단점이 있는데 비교하기 위해 "Service managed"를 먼저 만들어 보자.

Service managed AWS Transfer for SFTP 구성

Terraform으로 구성하기 위해 일단 Terraform 설정을 한다. 여기선 Terraform v0.12.29를 사용했다.

provider "aws" {
  region = "ap-northeast-2"
  version = "~> 3.8"
}

SFTP의 파일을 저장할 S3 버킷을 먼저 생성한다.

resource "aws_s3_bucket" "stfp-sample" {
  bucket        = "outsider-sftp"
  acl           = "private"
}

꼭 필요한 건 아니지만 SFTP의 활동을 CloudWatch에 로그를 남길 수 있어서 로깅을 위한 IAM Role을 생성했다. transfer.amazonaws.com AssuemRole을 지정하고 CloudWatch에 로그를 남길 수 있도록 권한을 부여했다.

resource "aws_iam_role" "sftp_logging" {
  name               = "sftp-logging"
  description        = "write logs from transfer"
  assume_role_policy = data.aws_iam_policy_document.sftp_logging.json
}

data "aws_iam_policy_document" "sftp_logging" {
  version = "2012-10-17"
  statement {
    effect = "Allow"

    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["transfer.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy_attachment" "sftp_logging" {
  role       = aws_iam_role.sftp_logging.name
  policy_arn = aws_iam_policy.sftp_logging.arn
}

resource "aws_iam_policy" "sftp_logging" {
  name   = "sftp-logging"
  policy = data.aws_iam_policy_document.sftp_logging_cloudwatch.json
}

data "aws_iam_policy_document" "sftp_logging_cloudwatch" {
  version = "2012-10-17"
  statement {
    sid       = "AllowFullAccesstoCloudWatchLogs"
    effect    = "Allow"
    actions   = ["logs:*"]
    resources = ["*"]
  }
}

Terraform에서는 aws_transfer_server를 사용해서 SFTP를 생성할 수 있다.

resource "aws_transfer_server" "service_managed_sftp" {
  identity_provider_type = "SERVICE_MANAGED"
  logging_role           = aws_iam_role.sftp_logging.arn
}

AWS Transfer Family에 접속하면 SFTP 서버가 생긴 것을 볼 수 있다.

AWS Transfer에 생성된 SFTP 서버

잘 만들어졌지만, 호스트네임을 지정하지 않았으므로 호스트네임이 지정되어 있지 않다. 안타깝게도 AWS API에서 이 기능을 제공하지 않기 때문에 Terraform 리소스에서도 이를 지정할 수 없다. Terraform으로 관리하고자 한다면 꼼수가 있기는 하다.

먼저 Route53에 연결할 DNS 레코드를 생성한다. 여기서는 sftp.outsider.dev를 사용했다. 이 레코드는 CNAME으로 aws_transfer_server.service_managed_sftp.endpoint에 연결하는데 <Server ID>.server.transfer.ap-northeast-2.amazonaws.com의 형태가 되므로 여기서는 s-faabc20d45d74e9a9.server.transfer.ap-northeast-2.amazonaws.com이 된다.

resource "aws_route53_record" "sftp_outsider_dev" {
  zone_id = aws_route53_zone.outsider_dev.zone_id
  name    = "sftp.outsider.dev"
  type    = "CNAME"
  ttl     = "300"
  records = [aws_transfer_server.service_managed_sftp.endpoint]
}

이를 null 프로바이더를 이용해서 sftp 서버에 커스텀 호스트네임을 지정해 준다. null 프로바이더를 추가했으므로 terraform init으로 설치해 주어야 하고 여기서는 local-exec 프로비저너를 이용해서 로컬의 AWS CLI를 이용해서 SFTP 서버에 aws:transfer:customHostname을 지정해 주는 방법이다. 즉, 로컬에 AWS CLI가 설치되어 있어야 한다.

provider "null" {
  version = "~> 2.1"
}

resource "null_resource" "service_managed_sftp_custom_hostname" {
  depends_on = [aws_transfer_server.service_managed_sftp]

  provisioner "local-exec" {
    command = <<EOF
aws transfer tag-resource \
  --arn '${aws_transfer_server.service_managed_sftp.arn}' \
  --tags 'Key=aws:transfer:customHostname,Value=sftp.outsider.dev' \
  --region 'ap-northeast-2'
EOF
  }
}

아래처럼 커스텀 호스트네임이 지정된 것을 볼 수 있다. 태그라서 Terraform으로도 가능할 것처럼 보이지만 Terraform의 태그로 지정하면 InvalidTag: System tags cannot be added/updated by requester 오류가 발생한다.

생성된 SFTP 서버에 sftp.outsider.dev 도메인이 설정됨

서버는 설정되었으므로 이제 SFTP에 접속할 사용자를 추가해야 하는데 그전에 이 사용자한테 부여할 IAM Role을 생성한다. 이 Role이 앞에서 만든 S3 버킷에 접근할 권한을 지정한다. 즉, 사용자에게 권한을 다르게 주고 싶다면 Role을 여러 개 만들어서 사용자를 생성할 때 지정해 주면 된다. 여기서는 사용자마다 디렉터리를 부여하고 자신의 디렉터리만 접근할 수 있도록 권한을 부여했다.

resource "aws_iam_role" "sftp_user" {
  name               = "sftp-user"
  description        = "Access S3 bucket from tranfer SFTP"
  assume_role_policy = data.aws_iam_policy_document.sftp_user.json
}

data "aws_iam_policy_document" "sftp_user" {
  version = "2012-10-17"
  statement {
    effect = "Allow"

    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["transfer.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy_attachment" "sftp_user" {
  role       = aws_iam_role.sftp_user.name
  policy_arn = aws_iam_policy.sftp_user_s3_access.arn
}

resource "aws_iam_policy" "sftp_user_s3_access" {
  name   = "sftp-user-s3-access"
  policy = data.aws_iam_policy_document.sftp_user_s3_access.json
}

data "aws_iam_policy_document" "sftp_user_s3_access" {
  version = "2012-10-17"
  statement {
    sid    = "AllowListingOfUserFolder"
    effect = "Allow"
    actions = [
      "s3:ListBucket",
      "s3:GetBucketLocation"
    ]
    resources = ["arn:aws:s3:::${aws_s3_bucket.stfp_sample.id}"]
  }
  statement {
    sid    = "HomeDirObjectAccess"
    effect = "Allow"
    actions = [
      "s3:PutObject",
      "s3:GetObject",
      "s3:DeleteObjectVersion",
      "s3:DeleteObject",
      "s3:GetObjectVersion"
    ]
    resources = ["arn:aws:s3:::${aws_s3_bucket.stfp_sample.id}/*"]
  }
}

위 Role을 사용해서 testuser라는 SFTP 사용자를 만든다.

resource "aws_transfer_user" "test_user" {
  server_id      = aws_transfer_server.service_managed_sftp.id
  user_name      = "testuser"
  role           = aws_iam_role.sftp_user.arn
  home_directory = "/${aws_s3_bucket.stfp_sample.bucket}/testuser"
}

home_directory를 지정하지 않으면 /가 되는데 여기서는 사용자의 username으로 홈 디렉터리를 지정했다. 여기서는 사용자의 IAM Role을 하나만 만들었고 이 Role이 해당 S3 버킷의 전체 접근 권한을 가지고 있음으로 홈 디렉터리를 만들었을 뿐 다른 디렉터리의 접근 권한을 막은 것은 아니다.

SFTP 서버에 추가된 testuser 사용자

AWS 콘솔에 들어가면 사용자가 생성된 것을 볼 수 있다. 이 사용자가 로그인하려면 SSH 키가 필요하므로 테스트를 위해서 키를 생성한다.

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/outsider/.ssh/id_rsa): ./mykey
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ./mykey.
Your public key has been saved in ./mykey.pub.
The key fingerprint is:
SHA256:Vk3hLaHQkSyyq/qS7R918ICTRUPAX/BPN8huHOUudiE outsider@Outsiderui-Mac-Pro.local

mykeymykey.pub이라는 파일이 생성되는데 "Add SSH public key"를 눌러서 공개키 파일인 mykey.pub의 내용을 복사해서 붙여넣으면 공개키가 등록된다.

testuser에 등록한 SSH 공개키

이제 비공개키를 이용해서 sftp.outsider.dev에 SFTP로 접속할 수 있고 지정한 대로 홈 디렉터리 위치로 접속된다.

SFTP 프로그램으로 서버에 접속해서 파일을 업로드

업로드한 파일이 S3에 잘 올라갔음을 확인할 수 있다.

지정한 경로에 파일이 S3에 올라가 있는 것을 확인

Service managed AWS Transfer는 쉽게 설정할 수 있지만, 실사용에서의 문제가 좀 있다. 사용자를 웹 콘솔에서 쉽게 설정할 수 있지만(여기서는 Terraform을 이용했지만...) 로그인을 비밀키/공개키 방식으로만 해야 한다는 점이다. 정상적으로 생각하면 SFTP에 접속하는 사람이 SSH 키를 생성하고 공개키만 나한테 전달해 주어야 내가 등록해 줄 수 있는데 회사에서 보통 SFTP를 쓰는 사람은 개발에 대한 이해도가 낮을 거로 생각되어서 SSH 키를 생성하도록 안내하기는 쉽지 않을 수 있다. 내가 비밀키/공개키를 모두 만들어서 줘도 되긴 하지만 비밀키를 쓰는 장점이 사라지고 사용자들은 아무래도 username/password 방식이 훨씬 익숙할 텐데 이 방식을 제공하지 않는다.

다른 방식의 로그인을 이용하려면 Service managed가 아니라 Custom AWS Transfer를 이용해야 한다. 글이 길어져서 Custom AWS Transfer는 다음 편에 다룰 예정이다.

2020/09/30 17:07 2020/09/30 17:07