AWS 노트

Lambda에서 특정 네트워크 환경에서만 리퀘스트를 보낼 수 있는 API를 사용하기

Jonchann 2020. 11. 19. 17:12

하고 싶은 것

VPN으로 회사 네트워크에 접속해 서버1에 접속하고 서버2에 접속해야 리퀘스트에 성공하는 API-1를 Lambda함수에서 사용하고 싶다.

배경

  • API는 ECS로 관리되고 있다.
  • 사용하고 싶은 API-1은 API-0을 통해서만 리퀘스트를 보낼 수 있다.
  • API-0은 AWS의 LoadBalancer와 VPC를 이용해 구축되어있다.
  • API-1도 AWS의 LoadBalancer와 VPC를 이용해 구축되어있다.
  • API-1의 LoadBalancer는 API-0의 LoadBalancer에 설정되어있는 SecurityGroups를 통해서만 접속할 수 있다.
  • 따라서 두 LoadBalancer의 TargetGroup의 VPC는 동일하다.
  • Lambda함수은 API-0을 통해서 API-1을 사용하고 싶지 않다.

VPC, Subnet, SecurityGroups에 대해 알기 쉽게 정리하신 분이 있음

만들면서 배우는 아마존 버추얼 프라이빗 클라우드(Amazon VPC)

방법

  • API-1의 LoadBalancer에 API-0의 LoadBalancer와 상관 없는 SecurityGroup을 만들어 InbountRule에 추가한다.
  • InboundRule에 추가한 SecurityGroup을 Lambda의 SecurityGroup에 추가한다.
  • Lambda의 VPC는 API-1의 ECS에 설정되어있는 VPC를 설정한다.
  • Lambda의 subnet은 API-1의 ECS에 설정되어있는 subnet을 전부 설정한다.

CDK로 구현

// lambda-security-group.ts
import * as ec2 from "@aws-cdk/aws-ec2";
import * as cdk from "@aws-cdk/core";

import { toKebabCase } from "../../../../utils/utils";

export function createLambdaSecurityGroup(
    stack: cdk.Stack,
    vpc: ec2.IVpc
): ec2.ISecurityGroup {
    const resourceName = "LambdaSecurityGroup";
    const securityGroupName = toKebabCase(resourceName);
    const securityGroup = new ec2.SecurityGroup(stack, securityGroupName, {
        vpc: vpc,
        allowAllOutbound: true,
        securityGroupName: securityGroupName,
    });
    cdk.Tags.of(securityGroup).add("Name", securityGroupName);
    return securityGroup;
}
// api-one-load-balancer-security-group.ts
export function createApiOneLoadBalancerSecurityGroup(
    stack: cdk.Stack,
    prefix: string,
    baseName: string,
    apiZeroSecurityGroupId: string,
    lambdaSecurityGroup: ec2.ISecurityGroup,
    vpc: ec2.IVpc
): ec2.ISecurityGroup {
    const resourceName = "ApiOneSecurityGroup";
    const securityGroupName = toKebabCase(resourceName);
    const apiZeroSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(
        stack,
        "api-zero-security-group",
        apiZeroSecurityGroupId
    );
    const securityGroup = new ec2.SecurityGroup(stack, securityGroupName, {
        vpc: vpc,
        allowAllOutbound: true,
        securityGroupName: securityGroupName,
    });
    securityGroup.addIngressRule(apiZeroSecurityGroup, ec2.Port.tcp(80));
    securityGroup.addIngressRule(apiZeroSecurityGroup, ec2.Port.tcp(443));
    securityGroup.addIngressRule(lambdaSecurityGroup, ec2.Port.tcp(80));
    securityGroup.addIngressRule(lambdaSecurityGroup, ec2.Port.tcp(443));
    cdk.Tags.of(securityGroup).add("Name", securityGroupName);
    return securityGroup;
}

빠질수도 있는 함정

apiZeroSecurityGroup처럼 lambdaSecurityGroupsecurityGroupId를 취득해서 fromSomeServiceId할 수 있지 않나? 할 수도 있지만 그러면 아래와 같은 에러가 발생한다.
이미 동일 Stack 안에 정의되어있는 리소스는 직접 전해받아 사용해야 한다는 것이다.

There is already a Construct with name 'lambda-security-group' in SomeStack [some-stack]
Subprocess exited with error 1

Lambda에 구현

import requests

def lambda_handler(event, context):
    return get('api_url')

def get(self, api_url: str) -> Dict:
    response = requests.get(api_url).text
    return json.loads(response)

필요한 권한

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface"
            ],
            "Resource": [
                "arn:aws:ec2:{region}:{account_id}:network-interface/{vpc_id}",
                "arn:aws:ec2:{region}:{account_id}:network-interface/{ecs_subnet_id_0}",
                "arn:aws:ec2:{region}:{account_id}:network-interface/{ecs_subnet_id_1}",
                "arn:aws:ec2:{region}:{account_id}:network-interface/{lambda_security_group_id}"
            ]
        }
    ]
}

fyi

Lambda 레이어에 requests를 추가해도 import할 수 없다는 에러가 나올 수 있는데 Slacker를 대신 레이어로 추가하면 해결된다(hack이라 근본적인 이유를 찾는게 나을 수 있지만 난 아직 대안을 찾지 못했다).