Outsider's Dev Story

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

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

AWS Transfer 패밀리로 SFTP 구성하기 #1에서 이어진 글이다.


Custom AWS Transfer for SFTP 구성

AWS Transfer 서버에서 제공하는 인증 외에 다른 인증을 사용하려면 Custom AWS Transfer 서버로 구성해야 한다. Custom 서버를 이용하면 API Gateway와 연결해서 구성해야 한다.

사용자가 SFTP 접속을 요청하면 Transfer 서버가 API Gateway에 이 인증 정보를 전달하고 API Gateway에서 적절한 응답을 돌려주면 SFTP에 접속하게 할 수 있다. 그래서 API Gateway에 Lambda를 연결해서 Secrets Manger 혹은 Cognito와 연결해서 로그인하게 할 수 있다. 이를 Identity Provider라고 부른다.

FTP Client - AWS SFTP - API Gateway - Secret Manger로 이어지는 흐름

위 흐름은 Enable password authentication for AWS Transfer for SFTP using AWS Secrets Manager에서 가져온 이미지다. 요청의 처리가 위와 같은 순서로 이루어지고 Lambda를 연결했으므로 Lambda 코드에 따라 원하는 다른 Identity Provider를 얼마든지 연결할 수 있다.

나는 간단히 ID/Password로 로그인하게 하고 싶어서 Enable password authentication for AWS Transfer for SFTP using AWS Secrets Manager
를 참고해서 Lambda와 Secrets Manager를 사용했다.

일단 1편처럼 S3 버킷이 있다고 해보자.

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

사용자 인증 정보를 관리할 Secret Manager를 만들자. Secret Manager는 한 시크릿당 한 달에 $0.40이고 API 호출 만 건당 $0.05다. 여기서는 한 시크릿으로 JSON으로 사용자 정보를 관리할 것이므로 비용은 크게 신경 쓰지 않아도 된다.

resource "aws_secretsmanager_secret" "sftp_users" {
  name        = "sftp-users"
  description = "SFTP users"
}

당연히 AWS에 잘 생성되었고 특별히 다른 설정은 하지 않았다.

시크릿 매니저에 생성된 시크릿

API Gateway

ID 제공자와 협력 문서를 참고하면 Transfer 서버가 API Gateway에 요청할 때 /servers/:serverId/users/:username/config로 요청을 보내는데 여기서 :serverId는 Transfer 서버의 아이디가 들어가고 :username는 SFTP 사용자의 유저네임이 들어간다.

그러므로 API 게이트웨이가 이 요청을 받도록 설정해야 한다. API Gateway가 꽤 복잡하기 때문에 Terraform 코드도 상당히 복잡해진다. API Gateway를 별로 좋아하진 않는데 Transfer 서버가 API Gateway로만 커스텀 아이덴티티 프로바이더를 지원하기 때문에 어쩔 수가 없다.

resource "aws_api_gateway_rest_api" "sftp_auth" {
  name        = "SFTP-Auth"
  description = "SFTP User authentication with Secret Manager and Lambda"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

위 설정은 API Gateway의 리소스 설정이다. 보통 웹 프레임워크에서는 /servers/:serverId/users/:username/config 같은 경로를 한 번에 설정할 수 있지만, Terraform으로 API Gateway를 다룰 때는 경로 단계마다 리소스를 생성해서 parent_id로 연결해 줘야 한다.

// /servers
resource "aws_api_gateway_resource" "sftp_auth_servers" {
  rest_api_id = aws_api_gateway_rest_api.sftp_auth.id
  parent_id   = aws_api_gateway_rest_api.sftp_auth.root_resource_id
  path_part   = "servers"
}

// /servers/{serverId}
resource "aws_api_gateway_resource" "sftp_auth_serverid" {
  rest_api_id = aws_api_gateway_rest_api.sftp_auth.id
  parent_id   = aws_api_gateway_resource.sftp_auth_servers.id
  path_part   = "{serverId}"
}

// /servers/{serverId}/users
resource "aws_api_gateway_resource" "sftp_auth_users" {
  rest_api_id = aws_api_gateway_rest_api.sftp_auth.id
  parent_id   = aws_api_gateway_resource.sftp_auth_serverid.id
  path_part   = "users"
}

// /servers/{serverId}/users/{username}
resource "aws_api_gateway_resource" "sftp_auth_username" {
  rest_api_id = aws_api_gateway_rest_api.sftp_auth.id
  parent_id   = aws_api_gateway_resource.sftp_auth_users.id
  path_part   = "{username}"
}

// /servers/{serverId}/users/{username}/config
resource "aws_api_gateway_resource" "sftp_auth_config" {
  rest_api_id = aws_api_gateway_rest_api.sftp_auth.id
  parent_id   = aws_api_gateway_resource.sftp_auth_username.id
  path_part   = "config"
}

/servers/:serverId/users/:username/config를 GET 요청으로 받아야 하므로 위에서 만든 aws_api_gateway_resource에 GET 메서드를 추가하고 이 요청을 Transfer 서버에서 요청해야 하므로 authorizationAWS_IAM으로 지정했다. 이 IAM Role은 뒤에서 생성할 것이다.

resource "aws_api_gateway_method" "sftp_auth_get" {
  rest_api_id   = aws_api_gateway_rest_api.sftp_auth.id
  resource_id   = aws_api_gateway_resource.sftp_auth_config.id
  http_method   = "GET"
  authorization = "AWS_IAM"
}

이 요청은 결국 Lambda를 호출하게 되는데 Lambda가 호출한 뒤에 돌려줄 응답을 먼저 정의하자. 복잡한 오류 상황을 모두 정의할 필요는 없음으로 여기서는 200 OK에 대한 응답만 정의했다.

resource "aws_api_gateway_method_response" "sftp_auth_get_200" {
  rest_api_id = aws_api_gateway_rest_api.sftp_auth.id
  resource_id = aws_api_gateway_resource.sftp_auth_config.id
  http_method = aws_api_gateway_method.sftp_auth_get.http_method
  status_code = "200"

  response_models = {
    "application/json" = aws_api_gateway_model.sftp_auth_user.name
  }
}

resource "aws_api_gateway_model" "sftp_auth_user" {
  rest_api_id  = aws_api_gateway_rest_api.sftp_auth.id
  name         = "userinfo"
  description  = "API response for sftp user"
  content_type = "application/json"

  schema = <<EOF
  {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title" : "user authencation result data structure",
    "type": "object",
    "properties": {
      "Role": { "type": "string" },
      "Policy": { "type": "string" },
      "HomeDirectory": { "type": "string" },
      "HomeBucket": { "type": "string" }
    }
  }
  EOF
}

연결된 aws_api_gateway_model에서 정의한 모델대로 이 응답은 다음과 같은 JSON을 돌려준다. 이 정보가 Transfer 서버가 사용할 정보인데 사용자에게 할당할 IAM Role과 여기에 추가로 부여할 정책, Transfer 서버가 사용한 S3 버킷, 홈 디렉터리를 지정해서 돌려줄 수 있다. 다시 말하면 Lambda의 코드에 따라 응답을 동적으로 만들어서 돌려줄 수 있고 그에 따라 Transfer 서버의 사용자에게 다양한 설정과 권한을 줄 수 있다.

{
  "Role": "",
  "Policy": "",
  "HomeDirectory": "",
  "HomeBucket": ""
}


Lambda

API Gateway의 요청을 받아서 Secret Manager에서 정보를 조회에서 사용자 로그인 정보를 확인하는 Lambda 코드를 lambda/index.js파일로 작성해 보자.

// lambda/index.js
const AWS = require('aws-sdk');

const client = new AWS.SecretsManager({
  region: process.env.REGION,
});

exports.handler = (event, context, callback) => {
  console.log('Event:', JSON.stringify(event));
  const username = event.pathParameters.username;
  const serverId = event.pathParameters.serverId;
  const password = event.headers.Password;

  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({}),
    isBase64Encoded: false
  };

  if (!username || !serverId || !password) {
    console.log('Error: required fields are missing');
    return callback(null, response);
  }

  client.getSecretValue({ SecretId: process.env.SECRET_ID }, (err, data) => {
    if (err) {
      console.log('Error: cannot retriving secrets')
      return callback(null, response);
    } else {
      const secrets = JSON.parse(data.SecretString);

      for (const user in secrets) {
        secrets[user] = JSON.parse(secrets[user]);
      }

      const user = secrets[username];
      if (user && user.password === password) {
        const policy = {
          Version: '2012-10-17',
          Statement: [
            {
              Sid: 'AllowListingOfUserFolder',
              Effect: 'Allow',
              Action: ['s3:ListBucket'],
              Resource: [`arn:aws:s3:::${process.env.BUCKET_NAME}`],
              Condition: {
                StringLike: { 's3:prefix': [
                  '${transfer:UserName}/*',
                  '${transfer:UserName}'
                ] }
              }
            },
            {
              Sid: 'AWSTransferRequirements',
              Effect: 'Allow',
              Action: ['s3:ListAllMyBuckets', 's3:GetBucketLocation'],
              Resource: '*'
            },
            {
              Sid: 'HomeDirObjectAccess',
              Effect: 'Allow',
              Action: [
                's3:PutObject',
                's3:GetObject',
                's3:DeleteObject',
                's3:DeleteObjectVersion',
                's3:GetObjectVersion',
                's3:GetObjectACL',
                's3:PutObjectACL'
              ],
              Resource: ['arn:aws:s3:::${transfer:HomeDirectory}/*']
            }
          ]
        };

        const responseBody = {
          Role: process.env.S3_ROLE,
          HomeDirectory: `/${process.env.BUCKET_NAME}/${username}`,
        };

        if (user.type !== 'admin') {
          responseBody.Policy = JSON.stringify(policy);
        } else {
          responseBody.HomeDirectory = `/${process.env.BUCKET_NAME}/`;
        }

        response.body = JSON.stringify(responseBody);
        console.log('Success:', JSON.stringify(response));
        return callback(null, response);
      } else {
        console.log('Error: username and password not matched');
        return callback(null, response);
      }
    }
  });
};

100라인 정도 되는 Lambda 코드인데 알아야 할 부분을 정리해 두었다. 그리고 사용자에 type을 지정해서 어드민과 일반 사용자를 구분했다. 1편에서 SFTP로 로그인한 사용자가 다른 사용자의 디렉터리에도 접근할 수 있었던 sftp_user IAM Role(aws_iam_role.sftp_user)을 여기서도 그대로 사용할 예정인데 이 Role은 어드민으로 사용하고 추가 정책을 부여해서 일반 사용자는 다른 사용자의 디렉터리에 접근하지 못하도록 할 예정이다.

  • aws-sdk는 Lambda에 이미 설치되어 있음으로 따로 설치해서 패키징하진 않아도 된다.
  • 환경 변수로 지정한 REGION, SECRET_ID, BUCKET_NAME, S3_ROLE은 Lambda를 생성할 때 설정해 줄 것이다.
  • 10번 라인의 event.pathParameters.serverId, event.pathParameters.username는 위에서 /servers/:serverId/users/:username/config의 경로로 넘어온 serverId, username의 값인데 handlerevent 파라미터로 자동으로 넘어온다.
  • 12번 라인의 SFTP 사용자의 비밀번호는 헤더로 넘어오므로 event.headers.Password로 접근할 수 있다.
  • 29번 라인의 client.getSecretValue({ SecretId: process.env.SECRET_ID }, callback)으로 위에서 만든 시크릿 매니저에 저장된 값을 가져온다. 사용자마다 따로 가져올 수 있는 건 아니고 전체 JSON을 다 가져와서 비교해야 한다.
  • 42번 라인의 policy는 로그인한 사용자(유저네임과 비밀번호가 일치했을 때) 사용자에게 부여할 S3 정책이다. 위에서 설명한 대로 일반 사용자에게는 자신의 홈 디렉터리만 보이도록 IAM 정책을 추가하고 이를 응답에 돌려준다.

Lambda에 올리려면 zip 파일로 올려야 하므로 lambda 디렉터리에 들어가서 zip으로 압축을 한다.

$ zip -r ./sftp-auth.zip .
  adding: index.js (deflated 65%)
resource "aws_lambda_function" "sftp_auth" {
  function_name = "sftp-auth"
  description   = "SFTP User authentication with Secret Manager"
  role          = aws_iam_role.sftp_auth_for_lambda.arn

  runtime = "nodejs12.x"
  timeout = "30"

  filename         = "./lambda/sftp-auth.zip"
  source_code_hash = filebase64sha256("lambda/sftp-auth.zip")
  handler          = "index.handler"

  environment {
    variables = {
      SECRET_ID   = aws_secretsmanager_secret.sftp_users.name,
      REGION      = "ap-northeast-2",
      S3_ROLE     = aws_iam_role.sftp_user.arn,
      BUCKET_NAME = aws_s3_bucket.stfp_sample.id,
    }
  }
}

위에서 압축한 lambda/sftp-auth.zip를 여기서 지정해서 Lambda를 생성한다. 사실 Terraform으로 Lambda를 관리하는 건 별로 편하진 않은데(API Gateway도 마찬가지) 자주 바꾸진 않을 것 같기도 하고 리소스를 Terraform으로 설정하던 중이라 Serverless를 섞어 쓰고 싶지 않아서 Lambda까지 Terraform으로 설정했다.

이제 API Gateway에서 이 Lambda를 호출하고 Lambda가 Secret Manage에 접근할 수 있도록 aws_iam_role.sftp_auth_for_lambda를 생성하고 권한을 부여한다.

resource "aws_lambda_permission" "sftp_auth" {
  depends_on = [
    aws_api_gateway_method.sftp_auth_get,
    aws_api_gateway_method_response.sftp_auth_get_200
  ]
  statement_id  = format("AllowExecution-%s-%s", aws_lambda_function.sftp_auth.function_name, "ap-northeast-2")
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.sftp_auth.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.sftp_auth.execution_arn}/*/*/*"
}

resource "aws_iam_role" "sftp_auth_for_lambda" {
  name               = "sftp-auth-for-lambda"
  description        = "authentication lambda for SFTP"
  assume_role_policy = data.aws_iam_policy_document.sftp_auth_for_lambda_assume.json
}

data "aws_iam_policy_document" "sftp_auth_for_lambda_assume" {
  version = "2012-10-17"
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_policy" "sftp_auth_for_lambda" {
  name        = "sftp-auth-lambda-executor"
  path        = "/"
  description = "IAM policy to run lambda"

  policy = data.aws_iam_policy_document.sftp_auth_for_lambda.json
}

data "aws_iam_policy_document" "sftp_auth_for_lambda" {
  version = "2012-10-17"
  statement {
    effect = "Allow"
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = ["arn:aws:logs:*:*:*"]
  }
  statement {
    effect = "Allow"
    actions = [
      "secretsmanager:Get*",
      "secretsmanager:DescribeSecret",
      "secretsmanager:ListSecretVersionIds"
    ]
    resources = [aws_secretsmanager_secret.sftp_users.arn]
  }
}

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


Transfer 서버, API Gateway, Lambda, Secret Manager 연동

API 게이트웨이와 lambda를 연동하자. aws_api_gateway_integration으로 연동을 해야 API 게이트웨이로 들어온 GET 요청이 Lambda를 호출할 수 있다.

resource "aws_api_gateway_integration" "sftp_auth_get" {
  depends_on              = [aws_api_gateway_method.sftp_auth_get]
  rest_api_id             = aws_api_gateway_rest_api.sftp_auth.id
  resource_id             = aws_api_gateway_resource.sftp_auth_config.id
  http_method             = aws_api_gateway_method.sftp_auth_get.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = "arn:aws:apigateway:ap-northeast-2:lambda:path/2015-03-31/functions/${aws_lambda_function.sftp_auth.arn}/invocations"
}

API Gateway 설정이 완료되었으므로 이를 배포한다. API 게이트웨이는 배포해야 실제로 사용할 수 있다.

resource "aws_api_gateway_deployment" "sftp_auth_prod" {
  depends_on        = [aws_api_gateway_integration.sftp_auth_get]
  rest_api_id       = aws_api_gateway_rest_api.sftp_auth.id
  stage_name        = "prod"
}

너무 많은 설정을 했는데 드디어 Transfer 서버를 만들 차례다. 1편에서 다룬 것과 달리 identity_provider_typeAPI_GATEWAY로 지정했고 커스텀 호스트네임을 사용하기 위해 null_resource를 사용했다.(1편 참고)

resource "aws_transfer_server" "custom_sftp" {
  endpoint_type          = "PUBLIC"
  identity_provider_type = "API_GATEWAY"
  invocation_role        = aws_iam_role.sftp_auth_api_invocation.arn
  url                    = aws_api_gateway_deployment.sftp_auth_prod.invoke_url
}

resource "null_resource" "custom_sftp_custom_hostname" {
  depends_on = [aws_transfer_server.custom_sftp]

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

Transfer 서버에서 API Gateway를 호출할 수 있도록 aws_iam_role.sftp_auth_api_invocation을 생성한다. 위에서 API Gateway의 인증을 AWS_IAM으로 설정했고 여기서 Role을 지정한 것이다.

resource "aws_iam_role" "sftp_auth_api_invocation" {
  name               = "sftp-auth-api-invocation"
  description        = "invoke API gateway from tranfer SFTP"
  assume_role_policy = data.aws_iam_policy_document.sftp_auth_api_invocation_assume.json
}

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

    actions = ["sts:AssumeRole"]

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

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

resource "aws_iam_policy" "sftp_auth_api_invocation" {
  name   = "sftp-auth-api-invocation"
  policy = data.aws_iam_policy_document.sftp_auth_api_invocation.json
}

data "aws_iam_policy_document" "sftp_auth_api_invocation" {
  version = "2012-10-17"
  statement {
    effect  = "Allow"
    actions = ["execute-api:Invoke"]
    resources = [
      "${aws_api_gateway_rest_api.sftp_auth.execution_arn}/*/*/*"
    ]
  }
  statement {
    effect    = "Allow"
    actions   = ["apigateway:GET"]
    resources = ["*"]
  }
}

시크릿 매니저에서 사용자를 추가해보자. 키/밸류 형식으로 값을 설정해서 추가할 수 있는데 추가 필드를 설정하기 위해서 밸류에도 JSON으로 값을 지정했다.

시크릿 매니저에 시크릿 값으로 설정한 사용자 정보

전체를 JSON으로 지정하면 아래처럼 표현이 된다. 이 전체 값을 가져와서 위에서 작성한 Lambda가 필요한 필드를 가져다가 값을 비교할 수 있다.

{
  "user1": "{ \"password\": \"test1\" }",
  "admin1": "{ \"password\": \"admin1\", \"type\":\"admin\" }"
}

FTP 클라이언트로 user1로 로그인을 하면 패스워드를 이용해서 잘 로그인되고 파일 업로드가 가능한 걸 볼 수 있다. Lambda에서 정책으로 사용자 홈 디렉터리만 사용 가능하도록 했기 때문에 상위 디렉터리로 이동해도 다른 디렉터리는 보이지 않는다.

user1로 로그인한 SFTP

사용자 비밀번호를 잘못 입력하면 로그인이 되지 않는다.

SFTP 사용자 비밀번호가 틀리면 로그인 오류가 발생

admin1로 로그인하면 루트가 홈디렉터리로 설정되고 모든 폴더에 접근 가능한 것을 알 수 있다.

admin1로 로그인하면 루트로 접속되어 전체 폴더를 확인할 수 있다

이 모든 과정을 보면 그냥 FTP 서버를 띄우면 되지 이렇게 복잡한 설정으로 할 필요가 있나 싶은 생각이 들 수도 있다. 설명하느라고 더 복잡해지긴 했지만, Terraform으로 한번 설정하면 AWS가 다 관리해 주므로 별도로 운영할 필요는 없고(Transfer 서버가 비싸긴 하지만...) 사용자도 Secret Manager에서 쉽게 추가/삭제할 수 있다.

2020/10/02 19:27 2020/10/02 19:27

기술 뉴스 #159 : 20-10-01

웹개발 관련

  • Introducing visx from Airbnb : 기존에도 시각화 라이브러리가 많이 있지만 얼마나 쉽게 배울 수 있는지, 표현력이 얼마나 좋은지, 성능이 얼마나 좋은지가 모두 중요한데 쉬우면서 표현력 좋기는 쉽지 않다. 이미 많은 프론트엔드 개발자가 React에 익숙하기 때문에 React 식으로 만들어서 3가지 목표를 다 이룬 시각화 라이브러리 visx를 Airbnb에서 1.0으로 공개했다. 3년간 개발하고 2년 반을 Airbnb 프로덕션에서 사용했으며 초기에는 JavaScript로 만들었지만, TypeScript로 재작성했다고 한다.(영어)
  • Introducing the New JSX Transform : React 17에서 도입된 새 JSX 변환 방식을 소개한다. 기존 JSX 변환도 그대로 동작하지만 새로운 방식은 React.createElement 대신 특수한 함수를 사용하므로 React를 임포트하지 않아도 되고 번들 크기도 다소 줄어들었다고 한다. 새로운 방식을 사용하려면 새로운 변환 방식을 지원하는 React를 사용해야 하고(지금은 17에 추가되었지만 14, 15, 16에도 추가될 예정이다.) 호환되는 컴파일러를 사용해야 한다.(영어)
  • Moment.js Project Status : 2011년에 만들어져서 2020년 9월에도 1,200만 다운로드가 이뤄지고 있지만, 지금과 맞지 않는 부분이 있고 버전 3을 만들 수도 있지만 이미 다른 라이브러리가 그런 부분을 충족할 수 있음으로 Moment.js를 메인테이넌스 모드로 운영한다고 한다. 메이저 버전 업그레이드나 새로운 기능 추가는 없을 예정이고 주요한 보안 업데이트나 타임존 데이터 업데이트만 이뤄질 것이라고 한다. 프로젝트가 죽은 건 아니고 완료된 것이라고 하면서 다른 대안 프로젝트를 추천하고 있다.(영어)

그 밖의 개발 관련

  • Lessons Learned from Running Postgres 13: Better Performance, Monitoring & More : 아직 베타인 Postgres 13을 스테이지 환경에서 몇 달간 운영해 보면서 알게 된 개선점을 설명한 글이다. 인덱스에서 중복을 제거해 주는 deduplicate_items를 사용해서 인덱스가 1/3로 줄었고 통계가 더 개선되었고 VACUUM을 병렬로 실행할 수 있지만, 아직 autovacuum에서는 병렬이 지원되지 않는다. 그밖에 모니터링이나 로깅 등 개선된 부분을 설명하고 있다.(영어)
  • 뱅크샐러드는 어떻게 레거시 서비스를 박살 내는가 : 뱅크샐러드에서 수년간 서비스를 지탱했지만 유지 보수하기 어려우신 레거시 메인 백엔드를 개선하기 위해서 MSA로 서비스를 15개로 나누어서 개선 작업을 시작했고 기존 요청이 새 서비스에서 같은 응답을 주는지 모니터링하면서 클라이언트에서는 변경 없이 변경할 수 있도록 작업을 진행했다고 한다. 작업을 직행하면서 목표와 목표가 아닌 것을 명확하게 정의해서 공유했고 프로젝트가 후반이 가면서는 매일 진행하는 스탠드업에서 더 나아가 2시간마다 스탠드업을 진행하면서 조직 간의 공유를 활발히 하면서 레거시 정리 작업을 진행했다고 한다.(한국어)

인프라 관련

  • Docker Github Actions : GitHub Actions에서 setup-buildx-action을 이용해서 Buildkit으로 빌드한 뒤 도커 이미지 레이어를 캐시 해서 속도를 개선하는 방법을 설명한다.(영어)
  • Announcing HashiCorp’s Homebrew Tap : HashiCorp 도구를 macOS에서 쉽게 사용할 수 있도록 공식 Homebrew 탭을 운영하기 시작했다. HashiCrop가 직접 운영하기 때문에 릴리스 사이트에 있는 것과 동일하게 배포되고 커뮤니티가 운영 중인 brew install terraform 대신 이제 brew install hashicorp/tap/{vault|consul|nomad|terraform}를 이용하면 공식 배포 버전을 이용할 수 있다.(영어)

볼만한 링크

  • 나는 블루홀에서 무엇을 배웠나 : 일할 때 작업할 내용뿐 아니라 하고자 하는 의도(이유)를 설명해야 의도와 다른 결과가 나오는 문제나 작업 내용에 다 적지 못한 예외 상황들에 대해서 작업자가 제대로 의사 결정을 할 수 있다고 설명하고 있다.(한국어)
  • 개발자를 위한 정보 검색 팁 : 개발을 하다 보면 검색할 일이 많은데 검색을 잘하는 것도 중요한 능력이라서 어떻게 해야 검색을 잘할 수 있는지를 정리한 글이다. 구글을 이용하고 영문을 이용하라는 팀 외에도 검색을 잘하려면 관련된 키워드와 지식이 어느 정도 필요하고 이런 정보는 단기간에 습득하기도 어렵기 때문에 이런 부분을 학습하기 위해 키워드를 찾는 방법, 신뢰할 사이트, 시의성 따지기 등에 대해 잘 정리되어 있다.(한국어)

IT 업계 뉴스

프로젝트

  • iOS14 의 Back Tap 기능을 이용한 Covid-19 QR체크인 간소화 : iOS 14에서 본체의 뒤를 탭 해서 특정 기능을 실행할 수 있는 Back Tap 기능과 Shortcut을 연결해서 Kakao나 Naver의 코로나 관련 신원 인증 QR을 바로 띄울 수 있게 하는 방법을 설명한다.
  • umami : Google Analytics 대신 사용할 수 있는 오픈소스 웹사이트 분석 도구.

버전 업데이트

2020/10/01 03:18 2020/10/01 03:18