aws CDK!

IaC (코드형 인프라: Infrastructure as Code)

AWS 에서 리소스를 생성하는 방법은 Web Console, AWS CLI 등이 있다.

Console의 경우 UI 가 항상 변경되며 너무많은 클릭과 입력값 사용자가 일일이 입력해야 한다는 단점이 있고
AWS CLI 의 경우 스크립트로 제작시 일련의 프로세스(절차)를 따라야지만 해당 커맨드가 동작하는 등의 단점이 있다.

이러한 문제로 대규모 프로젝트의 경우 원할환 리소스의 빌드와 배포(CI/CD)를 위해 코드로 인프라를 관리(IaC)하는 개념이 나오게 되었으며 AWS Cloudformation, AWS CDK 등의 서비스가 나오게 되었다.

서드파티 어플리케이션으로 가장 유명한 Terraform 이 있으며 만약 하이브리드 클라우드에서 서비스를 동작시킬 경우 각 클라우드 서비스에서 제공하는 IaC 를 모두 학습할 필요 없이 Terraform을 사용하면 된다.
Yaml 또는 HCL(HashiCorp Configuration Language) 문법으로 각종 클라우드 서비스(GCP, AWS, Azure)에서 IaC 를 지원한다.

하지만 AWS 클라우드 서비스만을 사용한다면 개발자 입장에선 현재 출시된 CDK 가 가장 디테일하고 효율적인 방안이다.

그리고 AWS 에서 k8s 를 사용하기 위해 CDK8s 프로젝트가 생성되었는데 해당 프로젝트가 CNCFSandbox Project로 등록되었다.

CDKTerraform 처럼 각종 클라우드 서비스의 IaC 툴로 자리잡을 가능성도 있디.

CDK(클라우드 개발 키트: Cloud Development Kit)

각종 문서
DevDoc: https://docs.aws.amazon.com/cdk/latest/guide/home.html
ApiDoc: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html
Workshop: https://cdkworkshop.com

IaC 개발 프레임워크로 AWS에서 2019년 7월에 공개되었으며 Node(JS) 를 통해 구현됨.

https://github.com/aws/aws-cdk

5가지의 언어(TypeScript, JavaScript, Python, Java, C#)를 사용해 프로그래밍 패턴을 이용해 코드형 인프라를 재사용하고 유연하고 동적으로 변경할 수 있다.

ddd1

그림처럼 코드로 구현된 CDK AcpplicationCDK CLI 로 실행되어 CloudFormation Template 으로 변환후 AWS CloudFormation 에 배포된다.

CDK Acpplication

설치

$ npm install -g aws-cdk

$ cdk --version
2.2.0 (build 4f5c27c)

$ cdk init app --language java

Java 기반으로 CDK Acpplication 를 설치

구성

ddd1

maven 으로 구성되며 software.amazon.awscdk:aws-cdk-lib dependency 를 사용해 각종 IaC 코드를 작성할 수 있다.

$ cdk ls
MyProjectStack
$ cdk diff
# deploy 시 어떤 서비스들이 올라가는지 출력

부트스트래핑

처음 CDKAWS 에 배포하려면 CDK 가 배포를 수행하는 데 필요한 초기 설정(CDK 를 위한 IAM Role 등)을 프로비저닝 해야한다.

이러한 초기 리소스를 프로비저닝하는 프로세스를 부트스트랩 이라고 합니다.

cdk bootstrap

빌드 및 배포

빌드하려면 우선 AWS

$ cdk synth

cdk.out 디렉토리에 cloudformation 을 위한 template json 파일이 생성되는지 확인

$ mvn compile -q
$ cdk deploy

삭제

cdk destroy

Concepts

AWS CDK 에서는 여러가지 Concepts 객체들을 사용해서 AWS CloudFormation Template 을 구성하고 AWS 리소스들을 프로비저닝한다.

Construct

AWS CloudFormation Template 에 들어가는 구성요소들은 Construct 의 하위객체로서
각종 AWs 리소스 생성시에 사용되는 Concepts 객체들의 기본 빌딩 객체이다.

App

CDK Acpplication 을 표현하기 위한 객체로서 Stack 의 모음


public class MyProjectApp {
    public static void main(final String[] args) {
        App app = new App();
        new MyProjectStack(app, "MyProjectStack");
        app.synth();
    }
}


위 코드처럼 각종 Stack 생성시 App 객체를 넣어 AWS CloudFormation Template 생성시에 같이 포함되도록 설정할 수 있다.

생명주기는 아래처럼 contruct(구성), prepare(준비), validate(검증), synthesize(합성) 단계로 나뉘고 마지막 deploy(배포) 된다.

ddd1

Stack

AWS CDK에서 기본적인 배포 단위, 모든 AWS 리소스는 Stack의 범위 내에서 정의/프로비저닝되고 하나의 CDK Application 에 의해 AWS CloudFormation Template 로 각각 변경된다.

Environment

EnvironmentStack 을 배포할 AWS 계정/리전 을 지정하는 클래스
StackEnvironment 는 명시적으로 또는 암시적으로 연결된다.

public class MyProjectApp {

    static Environment makeEnv(String account, String region) {
        account = (account == null) ? System.getenv("CDK_DEPLOY_ACCOUNT") : account;
        region = (region == null) ? System.getenv("CDK_DEPLOY_REGION") : region;
        account = (account == null) ? System.getenv("CDK_DEFAULT_ACCOUNT") : account;
        region = (region == null) ? System.getenv("CDK_DEFAULT_REGION") : region;

        return Environment.builder()
                .account(account)
                .region(region)
                .build();
    }

    public static void main(final String[] args) {
        Environment envEU = makeEnv("8373873873", "eu-west-1");
        App app = new App();
        new MyProjectStack(app, "MyProjectStack");
        app.synth();
    }
}

꼭 위와같은 코드가 없더라도 aws configure 값을 확인후에 자동으로 StackEnvironment 가 설정되어 리소스들이 프로비저닝 된다.

Tokens

Tokens은 수명주기를 관여하는 문자열로 Lambda와 같이 AWS 리소스들을 서로 연관지어야 할때 아직 리소스가 프로비저닝되진 않았지만 Tokens 을 사용해 향후 접근할 수 있도록 지정할 수 있다.

final Bucket bucket = new Bucket(this, "MyBucket");

Function fn = Function.Builder.create(this, "MyLambda")
    .environment(new HashMap<String, String>() {
        put("BUCKET_NAME", bucket.getBucketName());
        // BUCKET_NAME = "${TOKEN[Bucket.Name.1234]}"
    }).build();

위 코드와 같이 Bucket 이 아직 생성되지 않았더라도 연관되는 Lambda 를 미리 정의할 수 있다.

Token Concepts 클래스를 토큰이 유효한지 등의 여러기능이 지원되니 확인바람.

Parameters

$ cdk deploy --parameters uploadBucketName=UploadBucket

위 명령어 처럼 cdk 명령을 통해 외부에서 파라미터 주입을 원할경우 사용

CfnParameter uploadBucketName = CfnParameter.Builder.create(this, "uploadBucketName")
    .type("String")
    .description("The name of the Amazon S3 bucket where uploaded files will be stored")
    .build();

Bucket bucket = Bucket.Builder.create(this, "myBucket")
    .bucketName(uploadBucketName.getValueAsString())
    .build();

Tags

모든 CDK Contruct 클래스들은 태깅설정이 가능하다.

Tags.of(myConstruct).add("key", "value");
Tags.of(myConstruct).remove("key");

myConstructStack 클래스를 적용하면 재귀적으로 하위 Contruct 클래스들도 재귀적으로 태그가 적용되며 포함된 내부 클래스들의 세세한 태깅설정을 처리하려면 TagProps 클래스를 사용해야 한다.

만약 재귀적 진행에 있어서 태그간의 충돌이 있을경우 TagProps.priority 속성을 사용하여 어떤 태그값을 우선시할지 지정 가능

Tags.of(myConstruct).add("key", "value", TagProps.builder()
    .priority(300).build());

Assets

CDK Application 으로 리소스를 배포할때 Assets 을 통해 번들로 제공할 파일 혹은 도커 이미지를 지정할 수 있다.

일반적으로 Lambda 함수를 정의한 파일을 S3Assets 으로 등록하고 AWS Lambda 함수를 생성하는 방법이 사용된다.

public class HelloAssetStack extends Stack {

    public HelloAssetStack(final App scope, final String id) {
        this(scope, id, null);
    }

    public HelloAssetStack(final App scope, final String id, final StackProps props) {
        super(scope, id, props);

        File startDir = new File(System.getProperty("user.dir"));
        String path = new File(startDir, "handler").toString();
        Code code = Code.fromAsset(path);
        Function.Builder.create(this, "myLambdaFunction")
                .code(code)
                .runtime(Runtime.PYTHON_3_6)
                .handler("index.lambda_handler").build();
    }
}

ECS Repository 를 사용해야 한다면 Repository 생성과 동시에 로걸에 있는 docker image 를 같이 프로비저닝 할 수 있도록 할 수 있다.

public class HelloAssetStack extends Stack {

    public HelloAssetStack(final App scope, final String id) {
        this(scope, id, null);
    }

    public HelloAssetStack(final App scope, final String id, final StackProps props) {
        super(scope, id, props);

        File startDir = new File(System.getProperty("user.dir"));
        DockerImageAsset asset = DockerImageAsset.Builder.create(this, "MyBuildImage")
                .directory(new File(startDir, "my-image").toString())
                .buildArgs(new HashMap<String, String>() ) // 빌드시 환경변수 지정 가능  
                .build();
        // my-image 디렉토리 안에 Dockerfile 포함 필수
    }
}

Permission

액세스 및 권한을 관리하기 위해 IAM을 사용해야 한다.

대부분 리소스에서 grant...() 함수를 통해 IGrantable 을 상속하는 리소스의 접근권한을 설정할 수 있다.

grantRead() 함수를 통해 LambdaS3 버킷 읽기 권한을 설정

public class MyProjectStack extends Stack {
    public MyProjectStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public MyProjectStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);
        Permission
        File startDir = new File(System.getProperty("user.dir"));
        String path = new File(startDir, "handler").toString();
        Code code = Code.fromAsset(path);
        Function function = Function.Builder.create(this, "myLambdaFunction")
                .code(code)
                .runtime(Runtime.PYTHON_3_6)
                .handler("index.lambda_handler").build();
        new Bucket(this, "MyBucket").grantRead(function);
    }
}

Role Constants 클래스를 이용해 IAM Role 에 해당하는 추상객체 생성 가능

public class TestPermissionStack extends Stack {
    public TestPermissionStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public TestPermissionStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);
        Bucket bucket = new Bucket(this, "MyBucket");
        Role role = Role.Builder.create(this, "Role")
                .assumedBy(new ServicePrincipal("ec2.amazonaws.com")).build();
        role.addToPolicy(PolicyStatement.Builder.create()
                .effect(Effect.DENY)
                .resources(Arrays.asList(bucket.getBucketArn()))
                .actions(Arrays.asList("ec2:SomeAction", "s3:AnotherAction"))
                .conditions(new HashMap<String, Object>() );
                }}).build());
    }
}

RoleaddToPolicy 함수를 통해 일반적인 Resource - Role - Policy 형태의 정책 구성

bucket.addToResourcePolicy(PolicyStatement.Builder.create()
    .effect(Effect.ALLOW)
    .actions(Arrays.asList("s3:SomeAction"))
    .resources(Arrays.asList(bucket.getBucketArn()))
    .principals(Arrays.asList(role))
    .build());

IGrantable 하위 클래스에는 Resource Based Policy 생성을 위한 addToResourcePolicy 함수를 제공한다.

Stack 간 리소스 공유

AWS리소스마다 생성에 필요한 시간이 각각 다르다.
EC2와 같은 리소스의 경우 요청이후 인스턴스가 생성되기 까지 매우많은 시간이 걸린다.

또한 변경되면 안되는 정보들이 몇가지 있다.
Loadbalacnerpublic DNSVPCpublic subnet 에 할당된 Elastic IP 의 경우 변경될 CDK Stack 을 어쩔수 없이 삭제하거나 재생성 되면 정보가 변경되는데 이미 각종 방화벽처리 등을 해놓은 상태라면 다시 해당정보를 업데이트해야 하는 상황이 발생할 수 있다.

따라서 immutableAWS리소스의 경우 별도로 Stack 을 구성하고 수시로 변경되는 Stack 에선 이를 참조하는 형식으로 사용하는 것이 권장된다.

vpc 만을 위한 Stack 하나 별도로 생성하고

@Getter
public class CustomNetworkStack extends Stack {
    private final Vpc vpc;

    public CustomNetworkStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);
        this.vpc = Vpc.Builder.create(this, "custom-vpc")
                .vpcName("custom-vpc")
                .maxAzs(3)  // Default is all AZs in region
                .cidr("10.0.0.0/16")
                .build();
    }
}

해당 VPC 를 참조하여 사용하는 Network Stack 을 별도로 만든다.

@Getter
public class CustomAppNetworkStack extends Stack {

    private final NetworkLoadBalancer nlb;
    private final RestApi restApiGateway;
    private final VpcLink vpcLink;

    public CustomAppNetworkStack(final Construct scope, final String id, final StackProps props, final Vpc vpc, final String profile, final String certificateArn) {
        super(scope, id, props);
        this.nlb = NetworkLoadBalancer.Builder.create(this, profile + "-custom-nlb")
                .loadBalancerName(profile + "-custom-nlb")
                .vpc(vpc)
                .internetFacing(false) // 인터넷 경계 설치
                .crossZoneEnabled(true)
                .build();
        this.vpcLink = new VpcLink(this, profile + "-rest-vpc-link", VpcLinkProps.builder()
                .vpcLinkName(profile + "-rest-vpc-link")
                .targets(Collections.singletonList(nlb))
                .build());
        this.restApiGateway = new RestApi(this, profile + "-rest-gateway", RestApiProps.builder()
                .defaultCorsPreflightOptions(CorsOptions.builder()
                        .allowOrigins(Collections.singletonList("*"))

                        .build())
                .restApiName(profile + "-rest-gateway")
                .deployOptions(StageOptions.builder()
                        .stageName(profile)
                        .build())
                .build());
        ICertificate certificate = Certificate.fromCertificateArn(this, profile + "-custom.test.com", certificateArn);
        DomainName domainName = new DomainName(this, profile + "-domain-name", DomainNameProps.builder()
                .domainName(profile + "-custom.test.com")
                .endpointType(EndpointType.REGIONAL)
                .certificate(certificate)
                .basePath("")
                .mapping(this.restApiGateway)
                .build());
        IFunction getTasksLambda = Function.Builder.create(this, profile + "-lambda-rest")
                .functionName(profile + "-lambda-rest")
                .vpc(vpc)
                .runtime(Runtime.NODEJS_14_X)
                .handler("index.handler")
                .code(Code.fromAsset("src/main/resources/lambda"))
                .build();
        restApiGateway.getRoot().addMethod("GET", LambdaIntegration.Builder.create(getTasksLambda).build());
    }
}

왠만하면 변경될일이 없어야 하는 immutableAWS리소스 3개만을 위한 Stack 이다.

  • NetworkLoadBalancer
  • RestApi
  • VpcLink

이제 수시로 변경가능성이 있는 Service 관련 Stack 을 생성한다.

StackProps prop = StackProps.builder()
                .env(devEnv)
                .build();
CustomNetworkStack networkStack = new CustomNetworkStack(app, "network-stack", prop);
CustomAppNetworkStack appNetworkStackDev = 
    new CustomAppNetworkStack(app, "dev-app-network-stack", prop, networkStack.getVpc(), "dev", devCertificateArn);
CustomAppNetworkStack appServiceStackDev = 
    new CustomAppServiceStack(app, "dev-app-service-customer", prop, appNetworkStackDev, clusterStackDev, "customer", 8080, "dev");

위와 같이 CustomNetworkStack, CustomAppNetworkStackimmutableAWS리소스 를 관리하고

CustomAppNetworkStack 의 경우 미리 정의해둔 Stack 으로부터 리소스를 가져올 수 있도록 파라미터에 참조시키면 된다.

$ cdk deploy dev-app-service-customer

해당 명령 수행시 자동으로 network-stack, dev-app-network-stack 의 상태를 체크하고 리소스를 참조하는 Cloudformation 파일이 생성되고 배포된다.

카테고리:

업데이트: