Tech
AWS IoT Core Resource Deployment via CDK
2023.01.05

AWS Cloud Development Kit(이하 AWS CDK)는 익숙한 프로그래밍 언어를 사용하여 클라우드 애플리케이션 리소스를 정의할 수 있는 오픈 소스 소프트웨어 개발 프레임워크 (CDK official page)입니다. 이러한 코드를 통해 인프라를 관리하는 방식을 Infrastructure as Code, 줄여서 IaC라고 부릅니다. CDK는 작성된 코드를 모두 CloudFormation 템플릿으로 변환하여 리소스들을 생성합니다. 비슷한 툴 중 가장 보편적으로 쓰이는 것은 테라폼입니다. 테라폼은 HCL(HashiCorp Configuration Language)이라는 언어를 사용하여 다른 IaC 툴에 비해 진입 장벽이 조금 높습니다. 이에 반해 CDK는 AWS에서 제공하는 공식적인 IaC 툴이며 널리 쓰이는 다양한 언어로 IaC를 가능하게 한다는 장점이 있습니다. AWS에서 처음으로 IaC를 구현하고자 하는 분들 중 terraform에 대한 사용경험이 없다면 AWS가 제공하는 CDK가 좋은 선택지가 될 것입니다.


1. AWS IoT Core


AWS IoT Core는 IoT 디바이스들을 연결 및 관리하고, 다른 AWS 서비스들과 연동이 가능한 AWS의 IoT 서비스입니다. AWS는 IoT Device SDK(SDK official page)를 제공하며, 이를 기반으로 개발된 디바이스는 IoT Core를 쉽게 사용할 수 있도록 해 줍니다. IoT Core는 디바이스와의 통신을 담당하여 문자 그대로 AWS IoT 서비스에서 중심 역할을 합니다. IoT Core는 메시지 라우팅을 통해 S3 같은 저장소, MSK 등과 같은 데이터 파이프라인과 함께 사용될 수 있도록 합니다. 이와 더불어 Greengrass, FleetWise, SiteWise와 같은 서비스들을 활용하여 IoT 디바이스 운영 및 관리의 효율성을 높이는 방법도 있습니다.


이번 포스팅에서는 CDK를 활용해 AWS IoT Core를 활용한 간단한 시스템 인프라를 구성해 보고자 합니다.



2. 간단한 CDK 사용법


2.1 CDK 설치


설치 방법에 대해서는 AWS CDK 설치하기를 참조하시기 바랍니다. 터미널에서 npm을 통해 cdk를 설치합니다.

npm install -g aws-cdk


설치 후에는 다음의 명령어를 통해 설치 여부와 버전을 확인 가능합니다.

cdk --version



2.2 CDK 프로젝트 시작하기


CDK 코드를 작성할 빈 디렉터리를 하나 만들고, 터미널에서 다음 명령어를 통해 CDK 프로젝트를 시작합니다. CDK는 TypeScript(JavaScript), Python, Go, .NET 등 여러 가지 언어를 지원합니다. 본 포스팅에서는 TypeScript를 이용해 CDK 코드를 구현해 보겠습니다.

mkdir cdk-test-project && cd cdk-test-project
cdk init --language typescript


Root directory 내에 초기화된 코드 트리가 생깁니다.

cdk-test-project
├── README.md
├── bin
│  └── cdk-test-project.ts
├── cdk.json
├── jest.config.js
├── lib
│  └── cdk-test-project-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
└── tsconfig.json
Shell


여기서 저희가 주로 작업해야 할 코드는

/bin/cdk-test-project.ts, /lib/cdk-test-project-stack 입니다. cdk init 명령어를 통해 프로젝트가 초기화 되면, /lib/cdk-test-project-stack.ts라는 샘플 코드를 자동으로 생성합니다. stack 은 리소스들의 모음을 뜻하는 오브젝트이며, stack 내부에 원하는 리소스들을 정의할 수 있습니다. 이렇게 정의된 stack은 /bin/cdk-test-project.ts 에서 app으로 패키징 되어 배포가 가능하도록 구조가 구성되어 있습니다. 만일, stack을 여러 개 정의할 경우, /lib 디렉터리 하위에 다른 stack을 정의하고, stack을 /bin/cdk-test-project.ts 코드에서 app에 추가하면 됩니다.



2.3 CDK 프로젝트 AWS와 연결하기


로컬에서 작업한 코드와 AWS 클라우드 환경을 연결해 주려면, bootstrap이라는 과정을 거쳐야 합니다. AWS 계정 번호와 region 정보를 넣어 다음의 명령어를 구성합니다.

cdk bootstrap aws://ACCOUNT-NUMBER/REGION



2.4 CDK 프로젝트 배포하기


코드 작성이 끝나면 터미널 프로젝트 디렉터리에서 cdk synth 명령어를 통해 AWS CloudFormation template으로 구성해 볼 수 있습니다. 해당 명령어의 실행 결과는 root 디렉토리에 cdk.out 디렉토리가 생성되며 결과들이 저장됩니다.

cdk synth


cdk diff 명령어를 통해 기존에 배포된 내역과 현재 코드에 의해 정의된 리소스를 비교할 수 있습니다.

cdk diff


cdk deploy 명령어는 구성된 리소스 코드들을 AWS CloudFormation에 배포하고, 순차적으로 리소스들을 생성합니다. 이때, Stack 하나 혹은 전부를 배포할 수도 있습니다.

cdk deploy # stack이 하나 일경우
cdk deploy STACK_NAME # 여러개의 stack 중 특정 stack 하나만 배포 할경우
cdk deploy --all # 여러개의 stack 을 모두 배포할 경우



IoT 시스템 및 시나리오


1. IoT 시스템 아키텍쳐

본 포스팅을 위해서 간단한 간단한 IoT system을 구성해 보았습니다. 차량이 상태 관련 데이터를 서버로 보내고, 이 데이터를 S3에 저장하는 시나리오입니다. 좀 더 기술적으로 시나리오를 기술해 보면 다음과 같습니다.


  1. 차량에 장착된 Device는 mqtt 프로토콜을 이용해 IoT core 서버로 차량 데이터를 송신합니다.
  2. 메시지를 수신한 IoT Core 서버는 수신한 메시지를 Basic Ingest 기능을 통해 S3 bucket에 저장합니다.


Basic ingest는 AWS IoT core가 제공하는 Message routing 기능으로, 메시징 비용 없이 편리하게 다른 S3 서비스로의 전송을 가능하게 해줍니다. 구성하고자 하는 시스템에서 사용할 서비스는 표면적으로는 AWS IoT Core 와 S3 두 가지입니다. 그러나 두 가지 서비스의 사용을 위한 프로비저닝은 이보다는 조금 더 복잡합니다.



2. 디바이스 프로비저닝


2.1 시나리오에 따른 디바이스 프로비저닝


AWS 설명서에는 다음과 같은 세 가지 시나리오에서의 디바이스 프로비저닝을 설명하고 있습니다.


  • 인증서를 전달하기 전에 IoT 디바이스에 설치할 수 있습니다

첫 번째 방법은 가장 간편한 방법으로 이 방법은 디바이스의 제조사가 사용자와 사용 시점을 특정할 수 있을 때 활용 가능합니다. 디바이스 제조사가 사용자를 특정할 수 있기 때문에 디바이스에 최종적으로 사용할 인증서(PC, Permanent Certificate)를 저장하여 출고하는 것이 가능합니다. 이 방법은 세부적으로 Bulk regislation, JITR(Just In Time Regislation), JITP(Just In Time Provisioninig) 등의 방법이 있습니다.


  • 최종 사용자 또는 설치 관리자는 앱을 사용하여 IoT 디바이스에 인증서를 설치할 수 있습니다.

두 번째 방법은 디바이스 제조사가 디바이스를 관리자를 특정 및 신뢰 할 수 있고, 이 관리자가 최종적으로 PC를 발급해 줄 수 있는 시나리오에서 가능한 방법입니다.


  • 최종 사용자는 앱을 사용하여 IoT 디바이스에 인증서를 설치할 수 없습니다.

마지막 방법은 디바이스 제조사가 관리자와 최종 사용자, 사용 시점을 파악할 수 없고, 최종 사용자가 디바이스 사용을 시작할 때 디바이스가 스스로 PC를 발급받아야 할 경우에 사용하는 방법입니다.


이번 포스팅에서는 마지막 시나리오일 경우 사용하는 방법인 클레임에 의한 프로비저닝 방법을 사용하려고 합니다. 이는 사용자를 특정하기 어려운 B2C 상품에서 주로 사용하게 되는 방법입니다. 다시 말해, 수많은 불특정 다수가 사용할 디바이스이지만 관리 주체가 서비스 제공 업체일 경우 매우 유용하다는 장점이 있습니다. 이번 포스팅에서는 이에 더해 사전 프로비저닝 훅 과 lambda를 통해 디바이스를 검증하는 방법도 구현해 보겠습니다.



2.2 클레임에 의한 프로비저닝


클레임에 의한 프로비저닝은 다음과 같은 과정을 거칩니다.



2.2.1 Before operation


1) 디바이스는 내부에 클레임 인증서(Claim Certificate, 이하 CC)를 저장하여 출고

2) 디바이스는 최초 사용 시 CC를 기반으로 AWS IoT Core를 통해 접속

3) CC를 이용한 접속이 완료되면 IoT Core는 최종적으로 사용할 영구 인증서(Permanent Certificate, 이하 PC)를 생성

4) IoT Core는 최종적으로 PC 와 키를 전달하고, 디바이스는 이를 저장

5) 디바이스는 AWS IoT Thing으로 등록 요청

6) AWS IoT는 사전 프로비저닝 훅으로 AWS Lambda Function을 call

7) Lambda는 단말의 유효성을 검증하기 위해 필요한 정보를 조회하여 IoT Core의 프로비저닝 서비스에 조회 성공 여부 전달

8) 조회에 성공할 경우 프로비저닝 서비스는 프로비저닝 템플릿에 정의된 대로 클라우드 리소스를 생성

9) 디바이스는 클레임 인증서를 이용한 접속을 해제하고, 새 인증서로 AWS IoT core에 접속→operation 시작



2.2.2 Operation


1) AWS IoT Thing으로 등록된 디바이스는 AWS IoT core에 메시지를 전달

2) AWS IoT Core는 수신한 메시지를 Rule engine을 통해 S3에 저장


위와 같은 절차를 순서도로 그려보면 다음과 같이 도식화할 수 있습니다.



시스템 구축에 필요한 인프라 리소스


이제 시스템 구축에 필요한 인프라 리소스를 정의해 보겠습니다. 필요한 인프라 리소스 Stack을 성격에 따라 크게 세 부분으로 나누었습니다.



Provisioning template과 pre-provisioning hook


Stack 이름 : AwsIotCoreProvisioningInfraStack


첫 번째로 구현해야 할 것은 클레임 인증서를 가지고 있는 AWS IoT Thing의 등록과 영구 인증서 발급, 이에 따르는 프로비저닝 서비스를 위한 템플릿 구현입니다.



Claim certificate의 생성 및 저장


Stack 이름: AwsIotCoreProvisioningInfraStack


두 번째로 구현해야 할 것은 클레임 인증서의 발급과 저장입니다. 이렇게 생성된 인증서는 디바이스 펌웨어에 함께 저장되어 출고되고, 최초 사용 시 영구 인증서 발급과 프로비저닝을 요청합니다.

Claim certificate의 생성과 저장은 Provisioning template에 의존하기 때문에 AwsIotCoreProvisioningInfraStack 안에서 같이 구현해 보겠습니다.



Rule engine을 통한 메시지의 S3 저장


Stack 이름: AwsIotCoreRuleInfraStack


마지막으로는 운영에 들어간 디바이스에서 보고한 메세지의 전달과 저장입니다. 이 과정에서는 AWS IoT Core의 Rule engine을 사용하게 되며, Basic ingest을 통해 AWS의 저장소 서비스인 S3에 메시지들을 저장하게 됩니다. (AWS 개발자 안내서)



1. 코드와 함께 구현하기 - 들어가기 전에


1.1 json 개발 패턴


필요한 모든 정책과 템플릿의 json 문서들은 AWS 공식 설명서(클레임에 의한 프로비저닝) 기본으로 작성되었으며, 적절한 환경 변수를 코드 내에서 json 문서를 업데이트하는 방식으로 작업하였습니다. 왜냐하면 CDK 코드는 앞서 선언된 리소스의 property 들이 뒤에서 사용되는 경우가 잦은데, hard-coded json 문서를 사용할 경우 변경 시 업데이트가 누락되는 경우가 있기 때문입니다. json 문서의 업데이트 코드 때문에 다소 코드가 복잡해지나, json 업데이트 패턴이 궁극적으로는 코드의 오류를 줄일 수 있습니다.



1.2 Config 설정


Config/config.ts 에 적절히 Config를 설정합니다. 여기서 s3BucketName 은 목적에 따라 네이밍 하되, 유일해야 합니다. 이번 포스팅에서는 cdk-s3-test-bucket 라는 이름을 사용하겠습니다.


const Config = {
    aws: {
        account: YOUR_ACCOUNT_HERE,
        region: YOUR_REGION_HERE,
    },
    app: {
        service: 'cdk-test',
        application: 'iot',
        environment: 'dev'
      },
    s3BucketName : "cdk-s3-test-bucket"
};
export { Config };



1.3 Initiation 된 Stack 과 App


cdk init 명령어를 통해 초기화된 디렉토리에서 저희가 주로 다루게 될 코드는 다음의 두가지 파일입니다.

bin/cdk-test-project.ts → Stack 들을 묶어 cdk app으로 만듭니다.

lib/cdk-test-project-stack.ts → 각각의 stack 구성합니다.

cdk init 명령어를 통해 초기화된 코드의 기본적인 Stack 이름은 cdkTestProjectStack 입니다. 이번 포스팅에서는 AwsIotCoreProvisioningInfraStack 를 구성한 뒤, 추가적으로 AwsIotCoreRuleInfraStack 을 생성하여 두 개의 stack을 만들어 배포할 예정입니다. 먼저 기능적인 구별이 가능하도록 예제 stack의 이름을 바꿔보겠습니다. 나머지 주석 부분은 CDK 가 기본적으로 제공하는 예제 코드로, 참고를 위해 그대로 두도록 하겠습니다.


변경 내용

CdkTestProjectStack → AwsIotCoreProvisioningInfraStack

또한 헛갈리지 않도록 리소스 id (AwsIotCoreProvisioningInfraStack 의 두 번째 입력값) 도 바꾸어 줍니다.


변경된 코드

lib/cdk-test-project-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class AwsIotCoreProvisioningInfraStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'CdkTestProjectQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}


/bin/cdk-test-project.ts

import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkTestProjectStack } from '../lib/cdk-test-project-stack';

const app = new cdk.App();
new AwsIotCoreProvisioningInfraStack(app, 'AwsIotCoreProvisioningInfraStack', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});


이제 모든 준비가 완료되었고, cdk-test-project-stack.ts 에서 작업을 시작해 보겠습니다.



2. Provisioning template과 pre-provisioning hook


2.1 Policy for test device


영구 인증서를 발급받고 Thing 등록과 프로비저닝이 마무리된 디바이스에 대한 정책입니다. 정책 내용은 기본 AWS IoT Core 정책 변수 섹션을 기반으로 합니다.

device/device-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Publish",
        "iot:Receive",
        "iot:RetainPublish"
      ],
      "Resource": [""]
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": [""]
    }
  ]
}


위 정책 문서를 testDevicePolicyJson 이라는 이름으로 삽입하고, 환경 변수에 맞게 업데이트 합니다. 그리고 수정된 문서를 기반으로 aws_iot.CfnPolicy 명령어를 통해 AWS IoT 정책을 생성합니다. 이때, 주의할 것은 {Action: [iot:Publish, iot:Receive, iot:RetainPublish]} 에 해당하는 Resource와 {Action: iot:Subscribe} 에 해당하는 키워드가 topic과 topicfilter로 다르다는 점입니다. 이 정책 문서는 애플리케이션의 상황에 맞추어 수정하면 됩니다.


testDevicePolicyJson.Statement[1].Resource = [
    `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/$aws/rules/*`,
    `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/` + '${iot:ClientId}'
]
testDevicePolicyJson.Statement[2].Resource = [
    `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topicfilter/` + '${iot:ClientId}'
]

let testDevicePolicy = new iot.CfnPolicy(
    this, Config.app.service + "-" + Config.app.environment + "device-policy",
    {
        policyDocument: testDevicePolicyJson,
        policyName: Config.app.service + "-" + Config.app.environment + "-device-policy",
    }
);



2.2 Role for pre-provisioning-lambda


사전 프로비저닝 훅에서 사용되는 lambda 함수의 AWS IAM Role입니다.

let rolePreProvisioningLambda = new iam.Role(
    this, Config.app.service + "-" + Config.app.environment + "-pre-provisioning-lambda-role",
    {
        assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
        description: "AWS IAM role for pre-provisioning lambda",
        roleName: Config.app.service + "-" + Config.app.environment + "-pre-provisioning-lambda-role",
    }
);



2.3 Lambda function for verifying devices


사전 프로비저닝 훅을 통해 invoke 되는, 디바이스의 시리얼 정보를 검증하는 람다 함수입니다. 이 함수가 True를 반환하기 위해서는 디바이스의 시리얼 정보가 다른 시스템을 통해 조회 가능하거나, 다른 방법을 통해 유효함이 판정되어야 합니다.

import json
import logging
import sys

# Configure logging
logger = logging.getLogger()

for h in logger.handlers:
    logger.removeHandler(h)
h = logging.StreamHandler(sys.stdout)

FORMAT = "[%(asctime)s - %(levelname)s - %(filename)s:%(lineno)s - %(funcName)s - %(message)s"
h.setFormatter(logging.Formatter(FORMAT))

logger.addHandler(h)
logger.setLevel(logging.INFO)

SERIAL_STARTSWITH = "297468"


def verify_serial(serial_number):
    if serial_number.startswith(SERIAL_STARTSWITH):
        logger.info("serial_number {} verification succeeded - starts with {}".format(serial_number, SERIAL_STARTSWITH))
        return True
        
    logger.error("serial_number {} verification failed - does not start with {}".format(serial_number, SERIAL_STARTSWITH))
    return False
    

def lambda_handler(event, context):
    response = {'allowProvisioning': False}
    logger.info("event: {}".format(json.dumps(event, indent=2)))

    if not "SerialNumber" in event["parameters"]:
        logger.error("SerialNumber not provided")
    else:
        serial_number = event["parameters"]["SerialNumber"]
        if verify_serial(serial_number):
            response = {'allowProvisioning': True}
    
    logger.info("response: {}".format(response))
    return response


lambda 함수의 리소스를 선언하기 전에. 먼저 실행할 함수를 device/verify-device-lambda.py 에 선언합니다. 위 예제 함수는 python으로 구성된 간단한 검증 함수입니다. 문제를 간단하게 만들기 위해 DB와 파일을 조회 대신 SerialNumber가 297468로 시작하는지 검증하고, 검증 결과에 따라 True/False 값을 반환하도록 구성합니다.

let lambdaPreProvisioningHook = new lambda.Function(
    this,
    Config.app.service + "-" + Config.app.environment +
    "-pre-provisioning-hook-lambda",
    {
        code: lambda.Code.fromAsset(path.join(__dirname, "device")),
        handler: "lambda_function.lambda_handler",
        runtime: lambda.Runtime.PYTHON_3_9,
        role: rolePreProvisioningLambda,
        description: "Lambda for pre-provisioning hook",
        functionName: Config.app.service + "-" + Config.app.environment + "-pre-provisioning-hook-lambda",
    }
);
lambdaPreProvisioningHook.addPermission("InvokePermission", {
    principal: new iam.ServicePrincipal("iot.amazonaws.com"),
    action: "lambda:InvokeFunction",
});


AWS lambda 함수의 리소스를 선언할 때는, code 경로와 handler 경로에 유의합니다. code는 lambda.Code.fromAsset 메시지를 통해 파일로 구성된 코드를 그대로 들고 올 수 있습니다. handler는 lambda 내에서 handler: {python 파일 이름}.{실제 동작하는 함수의 이름} 과 같이 작성합니다. 마지막으로 AWS IoT service가 해당 lambda 함수를 invoke 할 수 있도록 허용합니다.



2.4 Role for provisioning template


프로비저닝 서비스의 역할입니다. 프로비저닝 서비스에 대한 역할(Role)을 생성합니다. 프로비저닝 서비스는 실제로 iot 서비스에서 동작하므로, assume의 주체는 iot.amazonaws.com로 설정합니다.

let roleProvisioning = new iam.Role(
    this, Config.app.service + "-" + Config.app.environment + "-provisioning-template-role",
    {
        assumedBy: new iam.ServicePrincipal("iot.amazonaws.com"),
        description: "AWS IAM role for provisioning services",
        roleName: Config.app.service + "-" + Config.app.environment + "-provisioning-template-role",
    }
);
roleProvisioning.addManagedPolicy(
    iam.ManagedPolicy.fromAwsManagedPolicyName(
        "service-role/AWSIoTThingsRegistration"
    )
);



2.5 ProvisioningTemplate


프로비저닝 서비스를 정의하는 프로비저닝 템플릿입니다. 프로비저닝에서 가장 중요한 부분인 프로비저닝 템플릿을 정의해 보겠습니다. 프로비저닝 템플릿의 베이스 문서는 AWS 공식 개발자 안내서의 플릿 프로비저닝 템플릿 예를 참고합니다.

이번 포스팅에서는 플릿 프로비저닝 시에 ThingName 패턴을 test-thing-{SerialNumber}와 같이 지정하는 템플릿을 구성합니다. 기본 템플릿에서 ThingName 섹션을 패턴과 일치하도록 다음 내용을 삽입합니다.

"ThingName": {
  "Fn::Join": [
    "",
    [
      "test-thing-",
      {
        "Ref": "SerialNumber"
      }
    ]
  ]
}


device/provisioning-template.json

{
  "Parameters": {
    "SerialNumber": {
      "Type": "String"
    },
    "AWS::IoT::Certificate::Id": {
      "Type": "String"
    }
  },
  "Resources": {
    "certificate": {
      "Properties": {
        "CertificateId": {
          "Ref": "AWS::IoT::Certificate::Id"
        },
        "Status": "Active"
      },
      "Type": "AWS::IoT::Certificate"
    },
    "policy": {
      "Properties": {
        "PolicyName": ""
      },
      "Type": "AWS::IoT::Policy"
    },
    "thing": {
      "Type": "AWS::IoT::Thing",
      "OverrideSettings": {
        "AttributePayload": "MERGE",
        "ThingGroups": "DO_NOTHING",
        "ThingTypeName": "REPLACE"
      },
      "Properties": {
        "AttributePayload": {},
        "ThingGroups": [],
        "ThingName": {
          "Fn::Join": [
            "",
            [
              "test-thing-",
              {
                "Ref": "SerialNumber"
              }
            ]
          ]
        }
      }
    }
  }
}


위에서 구성된 문서에 CDK 코드 내에서는 프로비저닝 서비스에 대한 정책을 연결하고, CfnProvisioningTemplate 명령어를 통해 프로비저닝 템플릿 리소스로 변환합니다. 사전 프로비저닝 훅은 CfnProvisioningTemplate 명령어 내부의 preProvisioningHook 파라미터를 통해 설정합니다. 프로비저닝 프로세스마다 매번 lambda 함수를 호출해야 하므로, preProvisioningHook` 옵션에는 간단히 payloadVersion 과 위에서 정의한 lambda의 targetArn을 파라미터로 설정합니다.


testProvisioningTemplateJson.Resources.policy.Properties.PolicyName = testDevicePolicy.policyName!

let testProvisioningTemplate = new iot.CfnProvisioningTemplate(
    this, Config.app.service + "-" + Config.app.environment + "-provision-template",
    {
        provisioningRoleArn: roleProvisioning.roleArn,
        templateBody: JSON.stringify(testProvisioningTemplateJson),
        enabled: true,
        preProvisioningHook: {
            "payloadVersion": "2020-04-01",
            "targetArn": lambdaPreProvisioningHook.functionArn
        },
        description: "AWS IoT Provisioning Template",
        templateName: Config.app.service + "-" + Config.app.environment + "-provision-template",
    }
);



3. Claim Certificate와 관련된 리소스


3.1 CDK를 통해 구현되지 않는 리소스


AWS IoT Core의 경우 일반적으로 Console을 이용하여 Infra를 구축합니다. 리소스 요소가 많고, 서로 얽혀 있어 콘솔을 통해 직관적으로 구현하는 것이 가장 편하기 때문입니다. 또한 AWS IoT 가 다른 서비스에 비해 비교적 신생 서비스다 보니 CDK 공식 문서에도 설명이 충분하지 않는 아쉬움이 있습니다. 만일, 매뉴얼상의 콘솔 명령어와 CDK 명령어가 1:1로 매칭되지 않으며, 콘솔 명령어가 CDK Code 조합으로 구성되지 않을 경우에는 아래 리소스를 참고할 수 있습니다.


  • ConstructHub
  • AWS에서 운영하는 IaC를 위한 Hub로, AWS CDK 뿐만 아니라 Terraform 등 다양한 도구들의 construct 들을 찾을 수 있습니다.


  • AWS custom resources
  • CDK 에서 지원하는 custom 리소스
  • 명령어가 좀 더 풍부한 SDK API를 활용하여 원하는 리소스를 생성 가능합니다.


이번 섹션에서는 CDK로 구현이 되지 않는 AWS IoT 인증서 생성에 대해서 AWS Custom resource를 활용해 보도록 하겠습니다.



3.2 S3 bucket


먼저, 클레임 인증서를 저장할 AWS S3 bucket 을 만듭니다. config.ts 에서 정의한 S3bucektName변수를 S3의 이름으로 사용합니다. 기존에 사용하던 bucket을 그대로 사용하려면 bucket class의 다른 method(fromBucketArn, fromBucketAttributes, fromBucketName)들을 활용하시면 됩니다.

let cdkTestS3Bucket = new s3.Bucket(this, 'cdkTestS3Bucket', {
    blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    versioned: true,
    removalPolicy: RemovalPolicy.DESTROY,
    autoDeleteObjects: true,
    bucketName: `${Config.s3BucketName}`
    }
    );



3.3 Policy for Claim Certificate


디바이스에서 영구 인증서 발급 전까지 임시로 사용하는 클레임 인증서에 대한 정책을 정의합니다. 클레임 인증서에 대한 정책도 기본적인 IoT 정책을 기반으로 합니다. (cf. device/device-policy.json) 클레임 인증서에 대한 정책 정의는 AWS 개발자 안내서의 클레임에 의한 프로비저닝 을 참고하시면 됩니다.


device/cc-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["iot:Connect", "iot:RetainPublish"],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": ["iot:Publish","iot:Receive", "iot:RetainPublish"],
      "Resource": [""]
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": [""]
    }
  ]
}


위에서 정의된 베이스 정책 문서에 필요한 내용을 업데이트하고, CfnPolicy 명령어를 통해 testDeviceClaimCertificatePolicy라는 이름의 정책을 생성합니다. 클레임 인증서를 통해 프로비저닝을 요청하기 때문에, provision과 같은 정책 정의가 필요합니다.


let templateTopicCreate = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/$aws/certificates/create/*`
let templateTopicProvisioning = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/$aws/provisioning-templates/${testProvisioningTemplate.templateName}/provision/*`
testDeviceClaimCertificatePolicyJson.Statement[1].Resource = [templateTopicCreate, templateTopicProvisioning]

let templateTopicFilterCreate = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topicfilter/$aws/certificates/create/*`
let templateTopicFilterProvisioning = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topicfilter/$aws/provisioning-templates/${testProvisioningTemplate.templateName}/provision/*`
testDeviceClaimCertificatePolicyJson.Statement[2].Resource = [templateTopicFilterCreate, templateTopicFilterProvisioning]

let testDeviceClaimCertificatePolicy = new iot.CfnPolicy(
    this, Config.app.service + "-" + Config.app.environment + "-claim-certificate-policy",
    {
        policyDocument: testDeviceClaimCertificatePolicyJson,
        policyName: Config.app.service + "-" + Config.app.environment + "-claim-certificate-policy",
    }
);



3.4 Claim Certificate(CustomResource)


이제 클레임 인증서를 발급해 보겠습니다. AWS IoT Core Console에는 보안 - 인증서 탭을 통해 쉽게 인증서를 만들 수 있으나, 안타깝게도 CDK에는 콘솔 명령어와 매칭되는 명령어가 없습니다. 인증서는 Open SSL 등을 활용하여 생성하는 방법도 가능하나, IaC를 위한 통합이 어려운 점이 있습니다.


앞서 말씀드린 것처럼 AwsCustomResource를 활용하여 인증서를 생성하고, 이를 S3에 저장해 보도록 하겠습니다. AWS SDK의 CreateKeysAndCertificate API를 활용합니다. API를 호출함으로써 outputPath 에 정의되어 있는 네 가지 object certificateArn, certificatePem, keyPair.PublicKey, keyPair.PrivateKey 를 얻게 됩니다.

let createKeysAndCertificateForClaimCertificate = new AwsCustomResource(
    this, Config.app.service + "-" + Config.app.environment + "-create-keys-and-certificate-for-claim-certificate",
    {
        onUpdate: {
            service: "Iot",
            action: "createKeysAndCertificate",
            parameters: {setAsActive: true},
            physicalResourceId: PhysicalResourceId.fromResponse("certificateId"),
            outputPaths: ["certificateArn", "certificatePem", "keyPair.PublicKey", "keyPair.PrivateKey"],
        },
        policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}),
    }
);



3.5 Key Deployment to S3(CustomResource)


s3에 object를 저장하는 과정입니다. 위 과정에서 얻은 Claim certificate object 들을 위에서 만든 S3 bucket에 저장합니다. CDK 상에서 bucket 리소스를 cdkTestS3Bucket 로 정의하였고, 이를 destinationBucket 의 claim-certificate key에 저장하도록 합니다.


let keyDeploymentForDeviceClaimCertificate = new aws_s3_deployment.BucketDeployment(
    this, Config.app.service + "-" + Config.app.environment +  "put-key-to-s3",
    {
        destinationBucket: cdkTestS3Bucket,
        sources: [
            aws_s3_deployment.Source.data(
                "claim-certificate/claim.pem",
                createKeysAndCertificateForClaimCertificate.getResponseField(
                    "certificatePem"
                )
            ),
            aws_s3_deployment.Source.data(
                "claim-certificate/claim.public.key",
                createKeysAndCertificateForClaimCertificate.getResponseField(
                    "keyPair.PublicKey"
                )
            ),
            aws_s3_deployment.Source.data(
                "claim-certificate/claim.private.key",
                createKeysAndCertificateForClaimCertificate.getResponseField(
                    "keyPair.PrivateKey"
                )
            ),
        ],
    }
);



3.6 Policy attachment for claim Certificate


마지막으로, 생성한 클레임 인증서와 클레임 인증서 정책을 연결시켜 줍니다.

let PolicyPrincipalAttachmentForClaimCertificate =
    new iot.CfnPolicyPrincipalAttachment(this, Config.app.service + "-" + Config.app.environment + "policy-principal-attachment", {            policyName: testDeviceClaimCertificatePolicy.policyName!,            principal: createKeysAndCertificateForClaimCertificate.getResponseField("certificateArn")});



4. Rule engine을 통한 메세지의 S3 저장


Rule engine을 통한 메세지의 S3 저장 부분에서는 lib/aws-iot-core-rule-infra-stack.ts파일을 추가하고, 여기에 AwsIotCoreRuleInfraStack라는 이름의 Stack을 추가후 작업 진행하겠습니다. 여기서는 rulePolicyJson과 ruleKeysJson의 두가지 Json 파일을 사용할 예정입니다. 이들 파일은 미리 import 해두겠습니다. 파일 경로는 import 문에 명시되어 있습니다.

import {
    Stack,
    StackProps,
    aws_iot as iot,
    aws_iam as iam,
    aws_s3 as s3,
} from "aws-cdk-lib";
import { Construct } from "constructs";
import { Config } from "../config/config";
import rulePolicyJson from "./rule/rule-policy.json";
import ruleKeysJson from "./rule/rule-keys.json";

export class AwsIotCoreRuleInfraStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);
    }
}



4.1 Role for rule Engine


AWS IoT Core 내부 서비스 중 하나로 Rule engine 이 있습니다. Rule engine은 다른 서비스로의 메시지 전달이 가능하도록 해줍니다. 이 Rule engine을 위한 AWS IAM 역할을 만듭니다. IoT Core에서 사용할 역할로, assume 주체는 iam.ServicePrincipal(iot.amazonaws.com) 을 사용합니다.

//  Create role for Rule engine
let roleRuleEngine = new iam.Role(
    this, Config.app.service + "-" + Config.app.environment + "-rule-engine-role", {
        assumedBy: new iam.ServicePrincipal("iot.amazonaws.com"),
        description: "AWS I AM role for IoT rule engine",
        roleName: Config.app.service + "-" + Config.app.environment + "-rule-engine-role",
    }
);


4.2 s3 bucket (revisited)


다음으로 S3 bucket을 불러옵니다. S3 bucket은 Claim Certificate를 저장하기 위해 AwsIotCoreProvisioningInfraStack에서 이미 생성되었습니다. 버킷을 그대로 사용하도록 하겠습니다. s3.Bucket.fromBucketName 메서드를 활용하여 bucket 이름으로부터 기존에 생성된 bucket 리소스를 불러옵니다. 여기서 ${Config.s3BucketName} 은 config/config.ts에서 정의한 대로 cdk-s3-test-bucket 입니다.


// Get S3 bucket from bucket name
let cdkTestS3Bucket = s3.Bucket.fromBucketName(this, "cdkTestBucket",`${Config.s3BucketName}`);



4.3 Policy for role for rule


Rule engine에 필요한 역할의 정책을 정의합니다. s3 bucket으로 메시지를 저장하기 위해 s3:PutObject 권한이 필요합니다. 이 정책은 다음의 Json 문서를 기반으로 합니다.

rule/rule-policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:PutObject",
            "Resource": [""],
            "Effect": "Allow"
        }
    ]
}


rule-policy.json 정의된 베이스 정책 문서에 S3 bucket 리소스를 업데이트하고, 정책을 만듭니다. 정책을 만든 뒤에는 정책을 앞서 만든 IAM 역할에 할당합니다.

// Create policy and attach it to IoT Role.
rulePolicyJson.Statement[0].Resource = [cdkTestS3Bucket.bucketArn + "/*"]
let iotCoreRolePolicy = iam.PolicyDocument.fromJson(rulePolicyJson);

new iam.Policy(
    this,
    Config.app.service + "-" + Config.app.environment + "-iot-core-role-policy",
    {
        document: iotCoreRolePolicy,
        policyName: "iotCoreRolePolicy",
    }
).attachToRole(roleRuleEngine);



4.4 Rule for pre-defined keys


이제 IoT Core rule을 만들어보겠습니다. 본 포스팅에서는 rule1, rule2, rule3 세 가지의 Rule을 만들려고 합니다. For 문을 통해 여러 개의 rule을 동시에 만들기 위해 다음의 Json 문서를 활용하겠습니다.

rule/rule-keys.json

{
    "testRules": [
        "rule1",
        "rule2",
        "rule3"
    ]
}


각각의 rule은 다음의 cdk 코드를 통해 test-rule/{key} 라는 이름으로 생성됩니다. AWS CDK 개발자 안내서에서는 key 는 데이터가 기록되는 파일의 s3 경로로서, Timestamp 와 같이 고유한 식별자를 사용하는 것이 좋다고 안내하고 있습니다. 안내서대로 key에는 ${topic()}/${timestamp()} 를 사용하였습니다. 또한 rule einge이 조회할 sql의 구문은 각각의 토픽이 각자 맞는 key에 해당하는 데이터를 조회할 수 있도록 `SELECT * FROM 'test-rule/${key}'` 를 사용하였습니다. 이와 같은 과정을 testRules 에 나열되어 있는 룰에 대해 각각 생성하기 위해, for loop를 사용합니다.

// Get rules from ruleKeysJson
let testRuleKeys = ruleKeysJson.testRules;

// Create Rules in IoT Core
testRuleKeys.forEach((key) => {
    new iot.CfnTopicRule(
        this, Config.app.service + "-" + Config.app.environment + `-topic-rule-${key}`,
        {
            topicRulePayload: {
                actions: [
                    {
                        s3: {
                            roleArn: roleRuleEngine.roleArn,
                            bucketName: `cdk-s3-test-bucket`,
                            key: "${topic()}/${timestamp()}",
                        },
                    }
                ],
                sql: `SELECT * FROM 'test-rule/${key}'`,
            },
            // iot does not allow rule '-' (dash).
            ruleName: `test_rule_${key}`,
        }
    );
});


이제 모든 과정이 완성되었습니다. IoT Device 에서 보내는 메시지를 topic-rule/${key} 토픽에 맞추어 메시지를 전송하면, AWS는 바로 S3 bucket 내의 ${topic()}/${timestamp()} 경로에 메시지를 저장하게 됩니다. 이와 같이 간단한 Rule 정의를 통해 다른 AWS 서비스로 메시지를 전달하는 기능을 Basic ingest라고 하며, 이는 AWS에서 과금되지 않습니다. log와 같이 단순 저장이 필요하거나, S3를 데이터 레이크로써 활용할 때, Basic ingest를 활용하면 과금에서 자유로운 메시지 라우팅이 가능한 장점이 있습니다.



5. 작업을 통해 구현된 cdk app의 구조


cdk init 후 이번 포스팅에서 작업한 파일들을 트리구조로 나타내보면 다음과 같습니다.

cdk-test-project
├── README.md
├── bin
│   └── aws-iot-infra.ts
├── cdk.json
├── config
│   └── config.ts
├── jest.config.js
├── lib
│    ├── aws-iot-core-provisioning-infra-stack.ts
│    ├── aws-iot-core-rule-infra-stack.ts
│    └── device
│    │    ├── device/device-cc-policy.json
│    │    ├── device/device-policy.json
│    │    ├── device/provisioning-template.json
│    │    └── device/verify-devices-lambda.py
│    └── rule
│         ├── rule/rule-keys.json
│         └── rule/rule-policy.json
├── node_modules
├── package-lock.json
├── package.json
├── test
├── tsconfig.json
└── yarn.lock


이번 포스팅에서는 AWS IoT Core를 기반으로 한 디바이스 데이터 수집 시스템을 구상하고, 다음과 같은 시나리오와 리소스를 구성해 보았습니다. 구성한 시나리오와 리소스를 요약하자면 다음과 같습니다.


* 운영 시나리오


  1. Device 에서 생성된 메시지를 AWS IoT Core로 전달
  2. 클레임에 의한 인증서 발급 방법으로 인증서를 발급하여 사용
  3. 디바이스 검증을 위해 사전 프로비저닝 훅을 사용 CDK 를 통해 IoT Core를 기반으로 한 서비스의 IaC를 구현함 + 디바이스를 이용해 검증
  4. IoT Core는 Rule engine의 basic ingest 기능을 활용하여 S3에 메시지 저장


  • AWS CDK를 통해 생성한 리소스들


Provisioning template과 pre-provisioning hook 구현

디바이스 정책, 디바이스 검증용 람다 프로비저닝 서비스의 IAM 역할, 프로비저닝 템플릿 등 정의


Claim certificate의 생성 및 저장

S3 bucket, 클레임 인증서에 대한 정책, AwsCustomResource를 활용한 클레임 인증서 발급, 발급된 인증서의 저장, 인증서와 정책 연결


디바이스가 전달한 Message를 AWS IoT Core Rule Engine의 Basic ingestion을 통해 S3에 저장

Rule engine 역할, Rule engine 역할 정책 및 역할 연결, Rule 정의


  • 결론


이번 포스팅에서는 AWS IoT Core system을 이용하여 디바이스 등록부터 데이터 저장까지 가능한 인프라를 AWS CDK를 이용해 구성해 보았습니다. 처음 서비스를 위한 인프라를 구축하려 할 때 EC2나 S3 등 개별 서비스 리소스의 기능들을 이해하고 나서, 리소스 생성과 관리 작업으로 넘어가게 됩니다. 이때 CDK는 AWS 가 제공하는 툴로써 가장 높은 호환성을 보장하며, AWS 아키텍처를 IaC로 구축하는데 기초적인 도구가 될 수 있습니다. IoT 서비스는 장비 운영자와 서비스 개발자가 눈으로 확인된 동작에 기반해 결정 및 변경되어야 할 사항이 많기 때문에 클라우드 인프라에서 미리 준비해 두어야 할 구성요소가 많고 변경 이력에 대해 민감한 편입니다. 이를 콘솔에서 편리하게 생성하는 것도 가능은 하겠으나, 코드를 기반으로 디테일하게 구현하며 기능을 더 엄밀하게 뜯어보고 클라우드 서비스의 구조를 살펴볼 수 있는 좋은 기회였습니다.


아쉬운 점으로는 AWS IoT Core는 AWS 서비스 내에서도 비교적 신생 서비스로, 공식적인 문서의 설명이 아직 채워지지 않은 부분이 있거나 혹은 순환 참조를 이루는 경우가 많았습니다. 이런 부분들로 인해 개발하는 데에 시행착오를 많이 겪었습니다. 또한 일부 명령어들은 Console이나 AWS CLI에서 실행되는 명령어가 CDK에는 존재하지 않는 경우도 있었습니다. 이 부분은 SDK의 기능을 CDK에서 활용할 수 있게 지원해 주는 AwsCustomResource 기능을 활용하여 이를 해결하였습니다. 이 부분을 AWS에서 개선해 주었으면 하는 바람이 있습니다.


이 포스팅을 통해 개발자 여러분들께 도움이 되기를 바랍니다. 감사합니다.


이승범 | Fleet Platform

Fleet Platform 소프트웨어 및 IFS 솔루션 개발을 담당하고 있습니다.


김진형 | Fleet Platform

Fleet Platform 소프트웨어 개발을 담당하고 있습니다.


알렉 지브릭 | Cloud Infrastructure

자율주행 기술 클라우드 인프라 구축 및 쿠버네티스 개발을 담당하고 있습니다.


References


1) Official

AWS CDK Documentation

AWS CDK API reference

AWS CDK Examples github


2) Blogs

Managing Missing CloudFormation Support with the AWS CDK

My experience on Infra as Code with AWS CDK + Tips & Tricks

AWS CDK에서 Terraform으로 by 선비(sunB)

IaC / 형상관리 / 이미지빌드 by hyun6ik

코드로 인프라 관리: AWS CDK by musma 기술블로그

AWS CDK 사용법 : 삐멜 소프트웨어 엔지니어

코드로 AWS 인프라 구축하기 - AWS CDK

영지식 증명과 블록체인 그리고 SDV, 모빌리티
Tech
2024.02.27
Team 42dot Wins 2nd Place in the Autonomous Driving Challenge at CVPR 2023
Tech
2024.02.27
Joint Unsupervised and Supervised Learning for Context-aware Language Identification
Publication
2024.02.27
ML Data Platform for Continuous Learning
Tech
2024.02.27
속도와 보안이 강화된 OTA 업데이트
Tech
2024.02.27
Self-Supervised Surround-View Depth Estimation with Volumetric Feature Fusion
Publication
2024.02.27
Foros : 자동차에 합의 알고리즘을?
Tech
2024.02.27
42dot MCMOT(Multi-Camera Multi-Object Tracking) 챌린지
Tech
2024.02.27
42dot이 그리는 미래 모빌리티 세상
Insight
2024.02.27