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

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

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

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

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

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

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

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

AWS Config 규칙(Rule)

AWS에서는 AWS의 리소스에 적용할 수 있는 많은 규칙을 미리 만들어 두었는데 이를 AWS 관리형 규칙 이라 한다. 몇 가지 예로 AWS의 액세스키의 로테이션 여부를 체크하는 규칙, 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를 넣었다.

기타

  • 알림 내용은 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 에서 규칙을 선택해 규칙 내용을 살펴보면 현재 규칙을 준수하지 않은 리소스를 확인할 수 있고 이메일로 규칙 위반에 대한 내용을 전달해준다.

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

WRITTEN BY
트위니 RND 팀장 | Certified Professional AWS Solutions Architect/Devops Engineer

댓글