AWS Config로 AWS 리소스 태깅 관리하기

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 서비스에 들어가면 다음 화면이 반겨줄것입니다.

AWS Config에 처음 들어갔을때 나오는 화면

위의 화면에서 우측 상단에 시작하기 버튼을 누릅니다.

전역 리소스를 체크하고 버킷명을 입력한다

다른 내용은 그대로 두고 전역 리소스를 체크하고 하단의 전송 방법에서 S3 버킷 이름을 넣어줍니다.(따로 정해주고 싶지 않으면 디폴트 버킷명을 사용해도 무방합니다.)

설정값을 입력했으면 다음 버튼을 누릅니다.

다음 화면에서는 AWS Config가 모니터링을 할 규칙 선택할 수 있는데 우선 우리는 커스텀 규칙을 사용할 예정이기 때문에 따로 선택하지 않고 다음을 눌러 설정을 완료하겠습니다.

AWS Config 규칙(Rule)

AWS에서는 AWS의 리소스에 적용할 수 있는 많은 규칙을 미리 만들어 두었는데 이를 AWS 관리형 규칙 이라 합니다. 몇 가지 예로 AWS의 AccessKey 로테이션 여부를 체크하는 규칙, EBS 볼륨 혹은 S3 가 암호화되어있는지를 체크하는 규칙 등이 있습니다. AWS가 제공하는 규칙중에 required-tags라는 규칙이 있는데 이 규칙은 말 그대로 특정한 태그가 리소스에 붙어있느지를 체크하는 규칙입니다. 이 규칙이 우리가 체크하고 싶은 내용 그대로이긴 하지만 이 규칙을 사용하지 않는 이유가 있습니다.

required-tags 규칙이 지원하는 리소스는 한정되어있는데 그 리스트는 다음 링크에서 확인할 수 있습니다. 링크

제가 관리하고 있는 계정의 경우 체크하고 싶은 리소스중에는 Elastic IP도 있는데 이 리소스는 지원 리소스 목록에 없습니다. 따라서 모든 리소스를 확인하기 위해서는 직접 규칙을 짜서 적용하는 사용자 지정 규칙 이 필요합니다.

required-tags 규칙이 지원하는 리소스 중 일부. EIP는 없네요.

사용자 지정 규칙은 직접 리소스를 검사하는 람다함수를 통해서 리소스를 검사합니다. 즉 내 맘대로 리소스의 규칙을 정해 규칙을 준수하고 있는지 확인한 후 해당 리소스가 규칙을 준수하고 있는지 아닌지만을 알려주는 함수를 만들고 이를 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();
// 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();
}

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

위의 모든 내용을 담아 Cloudformation 템플릿을 구성하였습니다. AWS Config가 설정되어있을 경우와 처음부터 AWS Config를 설정하는 경우를 위한 두 가지 템플릿을 제공합니다. 아래 템플릿은

  1. AWS Config를 설정하고
  2. 특정 태그가 존재하는지를 검사하는 규칙을 생성하여
  3. 규칙을 준수하지 않은 리소스가 발견될경우 이메일(SNS)로 전달합니다.

파라메터 값 소개

  • Frequency: 얼마나 자주 AWS Config가 설정을 저장할지 결정합니다. 추후 규칙을 생성할때 중요한 내용인데 주기적인 규칙 설정을 할 경우 규칙에서 설정한 주기보다 여기서 설정할 주기가 더 클경우 더 큰 주기를 따라 규칙이 평가됩니다. 자세한 내용은 링크를 참조. 잘 모르겠으면 우선 24시간으로 설정하면 됩니다.
  • ConfigStorageBucketName: AWS Config가 기록한 리소스의 설정값들을 저장할 버킷명. 실제 생성시에는 버킷명-리전명으로 생성되며 생성시 “Service:devops” 라는 태그를 부여하고 스택이 삭제되어도 버킷은 삭제되지 않는 Retain 삭제 정책을 가지고 있습니다.
  • EmailAddress:해당 이메일로 규칙을 지키지 않은 리소스에 이메일을 보냅니다.
  • TagName: 검사할 태그 명.(예:Service)
  • ResourceTypes: 검사할 리소스의 종류. 예제로 기본값 EIP와 S3를 넣었습니다. (필요시 컴마로 구분해 넣으시면 됩니다.)

기타

  • 알림 내용은 CustomRequiredTagFailedEvent.Targets.InputTransformer 에 내용을 수정합니다. InputTransformer는 알림 값을 바탕으로 알림 내용의 텍스트를 구성해줍니다.
  • 아래 템플릿을 변경없이 적용하여 스택을 만들면 입력한 이메일로 SNS의 이메일 전송 허락을 요청하는 이메일이 발송됩니다. 이 요청에 응답(링크 클릭)해주어야 SNS 알림들을 받을 수 있습니다.

이메일로 위와같은 구독 링크를 보냅니다.Confirm subscription을 눌러 승인이후에 이메일을 받을 수 있습니다.

기존에 태깅이 되지 않은 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을 기반으로 하기 때문에 리소스가 생성되고 규칙검사를 수행할때까지 조금 시간이 걸릴 수 있습니다.

두개의 EIP리소스가 올바르게 태그되지 않았네요.

마치며

AWS Config에는 규칙을 준수하지 않은 리소스의 경우 자동으로 규칙을 준수하도록 만들어주는 Remediation 을 지원합니다. 예를들어 S3 Bucket이 퍼블릭으로 풀려있다면 해당 버킷을 Private으로 자동으로 바꿔주는 것이죠. 다음에는 이러한 Remediation 액션에 대해서 포스팅 해보겠습니다.

WRITTEN BY
Dev Lead | Certified Professional AWS Solutions Architect/Devops Engineer

댓글