AWS의 여러 리소스를 관리하기 위해서는 태깅(Tag)은 매우 중요합니다. 각 리소스에 태깅을 통해 리소스를 그룹으로 묶어서 살펴볼수도 있고(Resource Group) 요금을 확인할때 태그로 묶어 요금을 확인할 수도 있습니다.
따라서 각 리소스에 태깅이 올바르게 이루어질 수 있도록 관리 감독하는 방법이 필요하며 AWS의 리소스가 설정된 규칙(태깅이 되어있는지, 암호화가 되어있는지 등)을 올바르게 준수하고 있는지를 확인할 수 있는 서비스가 AWS Config입니다.
이번에는 AWS 리소스가 지정한 이름의 태그가 붙어있지 않다면 알람을 전달하는 아키텍쳐를 소개하고 필요하신 분들이 쉽게 구성할 수 있도록 포스트 마지막에 Cloudformation 템플릿을 공유하려 합니다.
AWS Config AWS Config는 위에서 언급한대로 AWS의 모든 리소스를 측정하고 감사할수 있는 서비스입니다. Config는 현재 리소스의 모든 “상태” 를 데이터로 저장하고 이를 기반으로 리소스의 변화 및 구성관계를 모니터링합니다. 또한 리소스가 특정 상태인지 아닌지에 관한 규칙을 준수하고 있는지를 확인할 수 있습니다. 예를들어 “EBS 볼륨이 암호화가 되어있어야 한다”, “보안그룹(Sercurty Group)의 SSH(22)포트가 막혀있어야 한다” 등의 규칙을 설정하고 이를 각 해당 리소스가 준수하고 있는지를 검사해줍니다.
이 포스트에서는 먼저 Config를 설정하고 리소스에 올바른 태그가 설정되어있는지 확인하는 규칙을 만들어 규칙을 준수하지 않는 서비스에 대해 SNS 이벤트를 발생시키는 순서로 소개하겠습니다.
AWS Config 설정하기
포스트 맨 아래에 첨부한 CloudFormation 템플릿을 사용할 경우 자동으로 설정하기 때문에 한번 훑어보기만 해도 됩니다.
AWS Config를 처음 사용하기 위해서는 먼저 설정을 해야합니다. 먼저 AWS Config를 한번도 사용한 적이 없다면 AWS Config 서비스에 들어가면 다음 화면이 반겨줄것입니다.
위의 화면에서 우측 상단에 시작하기 버튼을 누릅니다.
다른 내용은 그대로 두고 전역 리소스를 체크하고 하단의 전송 방법에서 S3 버킷 이름을 넣어줍니다.(따로 정해주고 싶지 않으면 디폴트 버킷명을 사용해도 무방합니다.)
설정값을 입력했으면 다음 버튼을 누릅니다.
다음 화면에서는 AWS Config가 모니터링을 할 규칙 선택할 수 있는데 우선 우리는 커스텀 규칙을 사용할 예정이기 때문에 따로 선택하지 않고 다음을 눌러 설정을 완료하겠습니다.
AWS Config 규칙(Rule) AWS에서는 AWS의 리소스에 적용할 수 있는 많은 규칙을 미리 만들어 두었는데 이를 AWS 관리형 규칙 이라 합니다. 몇 가지 예로 AWS의 AccessKey 로테이션 여부를 체크하는 규칙, EBS 볼륨 혹은 S3 가 암호화되어있는지를 체크하는 규칙 등이 있습니다. AWS가 제공하는 규칙중에 required-tags라는 규칙이 있는데 이 규칙은 말 그대로 특정한 태그가 리소스에 붙어있느지를 체크하는 규칙입니다. 이 규칙이 우리가 체크하고 싶은 내용 그대로이긴 하지만 이 규칙을 사용하지 않는 이유가 있습니다.
required-tags 규칙이 지원하는 리소스는 한정되어있는데 그 리스트는 다음 링크에서 확인할 수 있습니다. 링크
제가 관리하고 있는 계정의 경우 체크하고 싶은 리소스중에는 Elastic IP도 있는데 이 리소스는 지원 리소스 목록에 없습니다. 따라서 모든 리소스를 확인하기 위해서는 직접 규칙을 짜서 적용하는 사용자 지정 규칙 이 필요합니다.
사용자 지정 규칙은 직접 리소스를 검사하는 람다함수 를 통해서 리소스를 검사합니다. 즉 내 맘대로 리소스의 규칙을 정해 규칙을 준수하고 있는지 확인한 후 해당 리소스가 규칙을 준수하고 있는지 아닌지만을 알려주는 함수를 만들고 이를 AWS Config와 연동시키면 됩니다.
AWS 람다 함수 리소스가 지정한 태그를 가지고 있는지 검사하는 람다 함수를 소개합니다. (node.js 기준)(awslab 을 참고하였습니다.)
이 함수는 ruleParameters의 입력값을 받아 ruleParameters에서 지정된 태그가 검사하려는 리소스에 존재하는지에 따라 해당 리소스를 ‘COMPLIANT’ 혹은 ‘NON_COMPLIANT’ 상태로 설정합니다.
code src/lambda/evaluateResourceLambda.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 'use strict' ;let aws = require ('aws-sdk' );let config = new aws.ConfigService();function isApplicable (configurationItem, event ) { const status = configurationItem.configurationItemStatus; const eventLeftScope = event.eventLeftScope; return ('OK' === status || 'ResourceDiscovered' === status) && false === eventLeftScope; } function evaluateCompliance (configurationItem, ruleParameters ) { console .log(configurationItem); console .log(configurationItem.tags); const tags = configurationItem.tags; let tagsToFind = [] for (var property in ruleParameters) { tagsToFind.push(ruleParameters[property]) } console .log(tagsToFind); let found = true ; tagsToFind.every(function (tag, index ) { found = (tags[tag] != undefined ) return (found); }) return (found) ? 'COMPLIANT' : 'NON_COMPLIANT' } exports .handler = async (event, context) => { const invokingEvent = JSON .parse(event.invokingEvent); const ruleParameters = "ruleParameters" in event ? JSON .parse(event.ruleParameters) : {}; const configurationItem = invokingEvent.configurationItem let compliance = 'NOT_APPLICABLE' ; if (isApplicable(configurationItem, event)) { compliance = evaluateCompliance(configurationItem, ruleParameters); } const evaluation = { ComplianceResourceType: configurationItem.resourceType, ComplianceResourceId: configurationItem.resourceId, ComplianceType: compliance, OrderingTimestamp: configurationItem.configurationItemCaptureTime }; const putEvaluationsRequest = { Evaluations: [evaluation], ResultToken: event.resultToken }; await config.putEvaluations(putEvaluationsRequest).promise(); }
CloudWatch Event Rule AWS Config 규칙이 규칙 위반인 리소스를 발견하면 이벤트를 발생시키는데 이 이벤트를 CloudWatch Event Rule을 통해 잡아서 SNS를 실행하겠습니다.
CloudWatch Event에서 잡아낼 이벤트 패턴은 이렇습니다.
1 2 3 4 5 6 7 8 9 10 11 { "detail-type" : ["Config Rules Compliance Change" ], "source" : ["aws.config" ], "detail" : { "configRuleName" : ["Config 규칙 이름" ], "messageType" : ["ComplianceChangeNotification" ], "newEvaluationResult" : { "complianceType" : ["NON_COMPLIANT" ] } } }
SNS SNS에서는 Fan-out 패턴을 실행할 수 있는데 이는 다음 링크 를 참조해주십시오.
여기서는 간단하게 이메일로만 전송하는 예시만을 구현하지만 실제로는 람다함수를 통해 Slack으로 알림을 보내거나 문자 메세지를 전송하는 등 다양한 시나리오를 구현할 수 있습니다.
가격 규칙 하나당 $0.003 달러 이며 처음 100만개까지 규칙을 검사한 리소스마다 $0.001달러입니다. 따라서 규칙을 검사할 리소스가 많다면 생각보다 비용이 많이 나올 수 있으니 주의가 필요합니다. AWS Config에서는 두 가지 트리거가 있는데, 하나는 리소스가 변경될때마다 검사하는 것이고 다른 하나는 주기적으로 검사하는 것입니다. 전자의 경우 한번 전체 리소스를 검사하고 나면 변경된 리소스만 검사하기에 그렇게 요금이 많이 나오지 않지만 후자의 경우 주기적으로 검사할때마다 모든 리소스에 대해 비용이 청구되기 때문에 주의해야 합니다. 아래 Cloudformation 템플릿의 경우 리소스가 변경될때만 검사하는 트리거를 사용하겠습니다.
위의 모든 내용을 담아 Cloudformation 템플릿을 구성하였습니다. AWS Config가 설정되어있을 경우와 처음부터 AWS Config를 설정하는 경우를 위한 두 가지 템플릿을 제공합니다. 아래 템플릿은
AWS Config를 설정하고
특정 태그가 존재하는지를 검사하는 규칙을 생성하여
규칙을 준수하지 않은 리소스가 발견될경우 이메일(SNS)로 전달합니다.
파라메터 값 소개
Frequency: 얼마나 자주 AWS Config가 설정을 저장할지 결정합니다. 추후 규칙을 생성할때 중요한 내용인데 주기적인 규칙 설정을 할 경우 규칙에서 설정한 주기보다 여기서 설정할 주기가 더 클경우 더 큰 주기를 따라 규칙이 평가됩니다. 자세한 내용은 링크 를 참조. 잘 모르겠으면 우선 24시간으로 설정하면 됩니다.
ConfigStorageBucketName: AWS Config가 기록한 리소스의 설정값들을 저장할 버킷명. 실제 생성시에는 버킷명-리전명으로 생성되며 생성시 “Service:devops” 라는 태그를 부여하고 스택이 삭제되어도 버킷은 삭제되지 않는 Retain 삭제 정책을 가지고 있습니다.
EmailAddress:해당 이메일로 규칙을 지키지 않은 리소스에 이메일을 보냅니다.
TagName: 검사할 태그 명.(예:Service)
ResourceTypes: 검사할 리소스의 종류. 예제로 기본값 EIP와 S3를 넣었습니다. (필요시 컴마로 구분해 넣으시면 됩니다.)
기타
알림 내용은 CustomRequiredTagFailedEvent.Targets.InputTransformer 에 내용을 수정합니다. InputTransformer는 알림 값을 바탕으로 알림 내용의 텍스트를 구성해줍니다.
아래 템플릿을 변경없이 적용하여 스택을 만들면 입력한 이메일로 SNS의 이메일 전송 허락을 요청하는 이메일이 발송됩니다. 이 요청에 응답(링크 클릭)해주어야 SNS 알림들을 받을 수 있습니다.
기존에 태깅이 되지 않은 S3버킷 많다면 이메일 전송 허락을 나중에 수락하세요. 태깅이 안된 S3버킷 숫자만큼 이메일을 받을수도 있습니다.
다운로드(AWS Config 설정 포함) 다운로드(이미 Config가 설정되어 있을경우 규칙만 생성)
config.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Delivery Channel Configuration Parameters: - ConfigStorageBucketName - DeliveryChannelName - Frequency - Label: default: Tag Evaluator Configuration Parameters: - ResourceTypes - TagName - EmailAddress Parameters: Frequency: Type: String Default: 24hours Description: The frequency with which AWS Config delivers configuration snapshots. AllowedValues: - 1hour - 3hours - 6hours - 12hours - 24hours ConfigStorageBucketName: Type: String Description: Name For Config Storage Bucket EmailAddress: Type: String Description: email address for notification Default: test@test.com TagName: Type: String Description: Tag to evaluate Default: Service ResourceTypes: Type: CommaDelimitedList Description: A list of valid AWS resource types to include in this recording group. EX AWS::EC2::Instance,AWS::EC2::EIP,AWS::CloudTrail::Trail Default: AWS::EC2::EIP,AWS::S3::Bucket # - AWS::AutoScaling::AutoScalingGroup # - AWS::DynamoDB::Table # - AWS::RDS::DBInstance # - AWS::RDS::DBSnapshot # - AWS::EC2::Instance # - AWS::CodeCommit::Repository # - AWS::EC2::Volume # - AWS::EC2::EIP # - AWS::CodeBuild::Project # - AWS::ElasticLoadBalancing::LoadBalancer # - AWS::ElasticLoadBalancingV2::LoadBalancer # - AWS::S3::Bucket # - AWS::SNS::Topic # - AWS::SQS::Queue # - AWS::Elasticsearch::Domain Mappings: Settings: FrequencyMap: 1hour: One_Hour 3hours: Three_Hours 6hours: Six_Hours 12hours: Twelve_Hours 24hours: TwentyFour_Hours Resources: ConfigBucket: DeletionPolicy: Retain Type: AWS::S3::Bucket Properties: Tags: - Key: "Service" Value: "devops" BucketName: !Sub - ${Bucketname}-${AWS::Region} - { Bucketname: !Ref ConfigStorageBucketName } ConfigBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref ConfigBucket PolicyDocument: Version: 2012-10-17 Statement: - Sid: AWSConfigBucketPermissionsCheck Effect: Allow Principal: Service: - config.amazonaws.com Action: s3:GetBucketAcl Resource: - !Sub "arn:${AWS::Partition}:s3:::${ConfigBucket}" - Sid: AWSConfigBucketDelivery Effect: Allow Principal: Service: - config.amazonaws.com Action: s3:PutObject Resource: - !Sub "arn:${AWS::Partition}:s3:::${ConfigBucket}/AWSLogs/${AWS::AccountId}/*" ConfigRecorderRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - config.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWS_ConfigRole" ConfigRecorder: Type: AWS::Config::ConfigurationRecorder DependsOn: - ConfigBucketPolicy Properties: RoleARN: !GetAtt ConfigRecorderRole.Arn RecordingGroup: AllSupported: True IncludeGlobalResourceTypes: True ConfigDeliveryChannel: Type: AWS::Config::DeliveryChannel DependsOn: - ConfigBucketPolicy Properties: ConfigSnapshotDeliveryProperties: DeliveryFrequency: !FindInMap - Settings - FrequencyMap - !Ref Frequency S3BucketName: !Ref ConfigBucket EvaluateExcuteRole: Type: AWS::IAM::Role Properties: Tags: - Key: "Service" Value: "devops" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: root PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "config:PutEvaluations" Resource: "*" - Effect: Allow Action: - logs:* Resource: arn:aws:logs:*:*:* EvaluateResourceLambda: Type: "AWS::Lambda::Function" DependsOn: EvaluateExcuteRole Properties: Tags: - Key: "Service" Value: "devops" Handler: "index.handler" Role: Fn::GetAtt: - "EvaluateExcuteRole" - "Arn" Runtime: "nodejs12.x" Timeout: 600 Code: ZipFile: | 'use strict'; let aws = require('aws-sdk'); let config = new aws.ConfigService(); // Check whether the the resource has been deleted. If it has, then the evaluation is unnecessary. function isApplicable(configurationItem, event) { const status = configurationItem.configurationItemStatus; const eventLeftScope = event.eventLeftScope; return ('OK' === status || 'ResourceDiscovered' === status) && false === eventLeftScope; } function evaluateCompliance(configurationItem, ruleParameters) { console.log(configurationItem); console.log(configurationItem.tags); const tags = configurationItem.tags; let tagsToFind = [] for (var property in ruleParameters) { tagsToFind.push(ruleParameters[property]) } console.log(tagsToFind); let found = true; tagsToFind.every(function (tag, index) { found = (tags[tag] != undefined) return (found); }) return (found) ? 'COMPLIANT' : 'NON_COMPLIANT' } exports.handler = async (event, context) => { // console.log('Received event:' + JSON.stringify(event, null, 4)); const invokingEvent = JSON.parse(event.invokingEvent); const ruleParameters = "ruleParameters" in event ? JSON.parse(event.ruleParameters) : {}; const configurationItem = invokingEvent.configurationItem let compliance = 'NOT_APPLICABLE'; if (isApplicable(configurationItem, event)) { compliance = evaluateCompliance(configurationItem, ruleParameters); } const evaluation = { ComplianceResourceType: configurationItem.resourceType, ComplianceResourceId: configurationItem.resourceId, ComplianceType: compliance, OrderingTimestamp: configurationItem.configurationItemCaptureTime }; const putEvaluationsRequest = { Evaluations: [evaluation], ResultToken: event.resultToken }; await config.putEvaluations(putEvaluationsRequest).promise(); } AWSConfigInvokeLambdaPermission: Type: AWS::Lambda::Permission DependsOn: EvaluateResourceLambda Properties: Action: "lambda:InvokeFunction" FunctionName: !GetAtt EvaluateResourceLambda.Arn Principal: "config.amazonaws.com" AWSConfigCustomRequiredTag: Type: AWS::Config::ConfigRule DependsOn: AWSConfigInvokeLambdaPermission Properties: InputParameters: tag1Key: !Ref TagName Scope: ComplianceResourceTypes: !Ref ResourceTypes Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt EvaluateResourceLambda.Arn CustomRequiredTagFailedEvent: Type: AWS::Events::Rule Properties: Description: CustomRequiredTagFailedEvent EventPattern: source: - aws.config detail-type: - Config Rules Compliance Change detail: messageType: - ComplianceChangeNotification configRuleName: - Ref: AWSConfigCustomRequiredTag newEvaluationResult: complianceType: - NON_COMPLIANT Targets: - Arn: Ref: AlarmTopic Id: AWSConfigCustomRequiredTag-failed InputTransformer: InputTemplate: | "AWS Config 규칙 <rule>이 <time>에 <awsAccountId> 어카운트의 <awsRegion> 리전에서 <resourceType> 타입의 <resourceId> 리소스의 규칙 준수가 <compliance> 상태인 것을 발견하였습니다. 자세한 사항은 다음 주소를 통해 콘솔에서 확인하십시오. https://console.aws.amazon.com/config/home?region=<awsRegion>#/timeline/<resourceType>/<resourceId>/configuration" InputPathsMap: awsRegion: "$.detail.awsRegion" resourceId: "$.detail.resourceId" awsAccountId: "$.detail.awsAccountId" compliance: "$.detail.newEvaluationResult.complianceType" rule: "$.detail.configRuleName" time: "$.detail.newEvaluationResult.resultRecordedTime" resourceType: "$.detail.resourceType" AlarmTopic: Type: AWS::SNS::Topic Properties: Tags: - Key: "Service" Value: "devops" Subscription: - Endpoint: !Ref EmailAddress Protocol: email AlarmTopicpolicy: Type: AWS::SNS::TopicPolicy Properties: PolicyDocument: Id: AlarmTopicpolicy Version: "2012-10-17" Statement: - Sid: state1 Effect: Allow Principal: Service: - events.amazonaws.com Action: sns:Publish Resource: "*" Topics: - !Ref AlarmTopic
결과 AWS Config 에서 규칙을 선택해 규칙 내용을 살펴보면 현재 규칙을 준수하지 않은 리소스를 확인할 수 있고 이메일로 규칙 위반에 대한 내용을 전달해줍니다.
AWS Config 자체는 CloudTrail을 기반으로 하기 때문에 리소스가 생성되고 규칙검사를 수행할때까지 조금 시간이 걸릴 수 있습니다.
마치며 AWS Config에는 규칙을 준수하지 않은 리소스의 경우 자동으로 규칙을 준수하도록 만들어주는 Remediation 을 지원합니다. 예를들어 S3 Bucket이 퍼블릭으로 풀려있다면 해당 버킷을 Private으로 자동으로 바꿔주는 것이죠. 다음에는 이러한 Remediation 액션에 대해서 포스팅 해보겠습니다.