AWS의 여러 리소스를 관리하기 위해서는 태깅은 매우 중요하다. 각 리소스에 태깅을 통해 리소스를 그룹으로 묶어서 살펴볼수도 있고(Resource Group) 요즘을 확인할때 태그로 묶어 요금을 확인할 수도 있다.
따라서 각 리소스에 태깅이 올바르게 이루어질 수 있도록 관리 감독하는 방법이 필요하며 AWS의 리소스가 설정된 규칙(태깅이 되어있는지, 암호화가 되어있는지 등)을 올바르게 준수하고 있는지를 확인할 수 있는 서비스가 AWS Config이다.
AWS에 리소스가 지정한 태그가 없다면 알람을 전달하는 아키텍쳐를 소개하고 쉽게 구성할 수 있도록 포스트 마지막에 Cloudformation 템플릿을 공유하려 한다.
AWS Config AWS Config는 위에서 언급한대로 AWS의 모든 리소스를 측정하고 감사할수 있는 서비스다. Config는 현재 리소스의 모든 “상태” 를 데이터로 저장하고 이를 기반으로 리소스의 변화 및 구성관계를 모니터링한다. 또한 리소스가 특정 상태인지 아닌지에 관한 규칙을 준수하고 있는지를 확인할 수 있다. 예를들어 “EBS 볼륨이 암호화가 되어있어야 한다”, “보안그룹(Sercurty Group)의 SSH포트가 막혀있어야 한다” 등의 규칙을 설정하고 이를 각 해당 리소스가 준수하고 있는지를 검사해준다.
이 포스트에서는 먼저 Config를 설정하고 리소스에 올바른 태그가 설정되어있는지 확인하는 규칙을 만들어 규칙을 준수하지 않는 서비스에 대해 SNS 이벤트를 발생시키는 과정을 담았다.
AWS Config 설정하기 (포스트 맨 아래에 첨부한 CloudFormation 스택을 사용할 경우 자동으로 설정하기 때문에 한번 훑어보기만 해도 된다. ) AWS Config를 처음 사용하기 위해서는 먼저 설정을 해야한다. 먼저 AWS Config를 한번도 사용한 적이 없다면 AWS Config 서비스에 들어가면 다음 화면이 반겨줄것이다.
위의 화면에서 우측 상단에 시작하기 버튼을 누른다.
다른 내용은 그대로 두고 전역 리소스를 체크하고 하단의 전송 방법에서 S3 버킷 이름을 넣어준다.(따로 정해주고 싶지 않으면 디폴트 버킷명을 사용해도 무방하다.)
설정값을 입력했으면 다음 버튼을 누른다.
다음 화면에서는 AWS Config가 모니터링을 할 규칙 선택할 수 있는데, 우선 우리는 커스텀 규칙을 사용할 예정이기 때문에 따로 선택하지 않고 다음을 눌러 설정을 완료한다.
AWS Config 규칙(Rule) AWS에서는 AWS의 리소스에 적용할 수 있는 많은 규칙을 미리 만들어 두었는데 이를 AWS 관리형 규칙 이라 한다. 몇 가지 예로 AWS의 액세스키의 로테이션 여부를 체크하는 규칙, 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를 넣었다.
기타
알림 내용은 CustomRequiredTagFailedEvent.Targets.InputTransformer 에 내용을 수정한다. InputTransformer는 알림 값을 바탕으로 알림 내용의 텍스트를 구성해준다.
아래 템플릿을 변경없이 적용하여 스택을 만들면 입력한 이메일로 SNS의 이메일 전송 허락을 요청하는 이메일이 발송된다. 이 요청에 응답(링크 클릭)해주어야 SNS 알림들을 받을 수 있다.
다운로드(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 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::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 에서 규칙을 선택해 규칙 내용을 살펴보면 현재 규칙을 준수하지 않은 리소스를 확인할 수 있고 이메일로 규칙 위반에 대한 내용을 전달해준다.