はじめに
AWS Config記事第2弾。
前回は、AWS管理リソースに対するルールと修復アクションの作成を自動化したが、今回は、外部のリソースをカスタムルールで検査してみる。
前回記事
本記事に必要な前提知識は以下の通り。
- AWS+Terraformの基本的な知識
- AWS Configの基本的な知識
- AWS SDK for Python (Boto3)の基本的な知識
カスタムリソースの定義
まずは、検出する対象のカスタムリソースを定義する必要がある。
カスタムリソースはAWS公式の開発者ガイドを参考にしながら作成する。
なお、ドキュメント中に突如mvn package
というコマンドが登場するが、おそらく間違っていて、cfn package
が正しいと思われる。
上記ページに書いてあるcfn init
後に、mycustomnamespace-testing-wordpress.jsonの代わりにmyresource-gitlab-repository.json
を配置して、cfn package
まで実行しよう。
{
"typeName": "MyResource::GitLab::Repository",
"description": "GitLab Repository Custom Resource",
"properties": {
"Name": {
"description": "Repository Name",
"type": "string",
"pattern": "^[a-zA-Z0-9/]{1,255}$",
"minLength": 1, "maxLength": 255
},
"Endpoint": {
"description": "Endpoint of GitLab",
"type": "string"
}
},
"required": [ "Name", "Endpoint" ],
"primaryIdentifier": [ "/properties/Name" ],
"readOnlyProperties": [ "/properties/Name", "/properties/Endpoint" ],
"additionalProperties": false
}
cfn package
すると、myresource-gitlab-repository.zipというファイルが作られているはずだ。
この後、cfn submit
まで実行しても良いが、cfnコマンドで作ってしまうと消し方がよく分からなくないため、Terraformで作成することを推奨する。パッケージしたZipファイルはAmazon S3からアップロードするため、適当なAmazon S3バケットを作っておこう。
resource "aws_s3_object" "config_custom_resource" {
bucket = aws_s3_bucket.example.id
source = "myresource-gitlab-repository.zipのファイルパス"
key = "myresource-gitlab-repository.zip"
etag = filemd5(myresource-gitlab-repository.zipのファイルパス)
}
resource "aws_cloudformation_type" "config_custom_resource" {
schema_handler_package = "s3://${aws_s3_object.config_custom_resource.bucket}/${aws_s3_object.config_custom_resource.key}"
type = "RESOURCE"
type_name = "MyResource::GitLab::Repository"
lifecycle {
create_before_destroy = true
}
}
カスタムルールの定義
カスタムルールは以下のようにAWS Lambdaでルール定義する。
今回は、以下の仕様のPythonスクリプトを作成する。
- GitLabの全ブランチの保護状態を確認し、mainブランチのマージ権限を持っているのがMaintainerのみになっているかチェックをする
- GitLabのアクセストークンはシークレット情報であるため、AWS Secrets Managerから取得をする
- PutEvaluationsのAPIをラップした
put_evaluations()
で、各カスタムリソースの評価情報を配列で渡す
ブランチの保護状態については、以下のAPI仕様を確認して作成しよう。
AWS LambdaでAWS Configの情報を返すスクリプトは以下のAWS公式の開発者ガイドを参考にする。
import base64
import boto3
from botocore.exceptions import ClientError
import pprint
import json
import requests
import os
import sys
def get_secrets():
AWS_SECRETSMANAGER_CLIENT = boto3.client('secretsmanager')
try:
get_secret_value_response = AWS_SECRETSMANAGER_CLIENT.get_secret_value(SecretId = os.environ['GITLAB_TOKEN'])
except ClientError as e:
raise e
else:
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString']
else:
secret = base64.b64decode(get_secret_value_response['SecretBinary'])
return secret
def get_project_list():
project_list = []
try:
response = requests.get(
'https://' + GITLAB_ENDPOINT + '/api/v4/projects',
verify = False,
headers = {
'Authorization': 'Bearer ' + GITLAB_TOKEN
},
)
except Exception as e:
print(e)
sys.exit(-1)
response_json = json.loads(response.text)
for project in response_json:
project_list.append({ 'id': project['id'], 'name': project['path_with_namespace']})
return project_list
def build_evaluation(resource_id, compliance_type, event, resource_type, annotation=None):
eval_cc = {}
if annotation:
eval_cc['Annotation'] = annotation
eval_cc['ComplianceResourceType'] = resource_type
eval_cc['ComplianceResourceId'] = resource_id
eval_cc['ComplianceType'] = compliance_type
eval_cc['OrderingTimestamp'] = str(json.loads(event['invokingEvent'])['notificationCreationTime'])
return eval_cc
#--------------------
RESOURCE_TYPE = 'MyResource::GitLab::Repository'
GITLAB_TOKEN = get_secrets()
GITLAB_ENDPOINT = os.environ['GITLAB_ENDPOINT']
def lambda_handler(event, context):
pprint.pprint(event)
AWS_CONFIG_CLIENT = boto3.client('config')
evaluations = []
project_list = get_project_list()
for project in project_list:
try:
response = requests.get(
'https://' + GITLAB_ENDPOINT + '/api/v4/projects/' + str(project['id']) + '/protected_branches',
verify = False,
headers = {
'Authorization': 'Bearer ' + GITLAB_TOKEN
},
)
protected_branches = json.loads(response.text)
compliance_value = 'NON_COMPLIANT'
for protected_branch in protected_branches:
if protected_branch['name'] == 'main':
if len(protected_branch['merge_access_levels']) == 1 and protected_branch['merge_access_levels'][0]['access_level'] == 40:
compliance_value = 'COMPLIANT'
evaluations.append(build_evaluation(project['name'], compliance_value, event, RESOURCE_TYPE))
except Exception as e:
print(e)
response = AWS_CONFIG_CLIENT.put_evaluations(Evaluations=evaluations, ResultToken=event['resultToken'])
AWS Lambdaの関数は以下の通り定義する。
今回、スクリプト中でrequestsモジュールを使用していて、標準のPythonのランタイムには今は入っていない(3.7までは入っていたが今は使えない)ため、Lambda Layersを使用する。自分で作るのは面倒なので、AWS SDK Pandasのマネージドレイヤーを取り込む。
Lambdaのタイムアウトは別にいくつでも良いが、将来的にリポジトリが増えてきたときにタイムアウトしないよう、上限の900を設定している
data "archive_file" "lambda_config_customrule" {
type = "zip"
source_dir = "Lambdaの格納パス"
output_path = "出力パス"
}
resource "aws_lambda_function" "config_customrule" {
depends_on = [
aws_cloudwatch_log_group.lambda_config_customrule,
]
function_name = local.lambda_function_config_customrule_name
filename = data.archive_file.lambda_config_customrule.output_path
role = aws_iam_role.lambda_config_customrule.arn
handler = "configcustomrule.lambda_handler"
source_code_hash = data.archive_file.lambda_config_customrule.output_base64sha256
runtime = "python3.10"
memory_size = 128
timeout = 900
layers = ["arn:aws:lambda:us-east-1:336392948345:layer:AWSSDKPandas-Python310:15"]
environment {
variables = {
GITLAB_TOKEN = aws_secretsmanager_secret.example.name
GITLAB_ENDPOINT = "GitLabのエンドポイントのドメイン部"
}
}
}
resource "aws_lambda_permission" "allow_config" {
statement_id = "AllowExecutionFromConfig"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.config_customrule.function_name
principal = "config.amazonaws.com"
}
################################################################################
# CloudWatch Logs #
################################################################################
resource "aws_cloudwatch_log_group" "lambda_config_customrule" {
name = "/aws/lambda/${local.lambda_function_config_customrule_name}"
}
################################################################################
# IAM #
################################################################################
resource "aws_iam_role" "lambda_config_customrule" {
name = local.iam_lambda_config_customrule_role_name
assume_role_policy = data.aws_iam_policy_document.lambda_config_customrule_assume.json
}
data "aws_iam_policy_document" "lambda_config_customrule_assume" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy" "lambda_config_customrule" {
name = local.iam_lambda_config_customrule_policy_name
role = aws_iam_role.lambda_config_customrule.id
policy = data.aws_iam_policy_document.lambda_config_customrule_custom.json
}
data "aws_iam_policy_document" "lambda_config_customrule_custom" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = [
aws_cloudwatch_log_group.lambda_config_customrule.arn,
"${aws_cloudwatch_log_group.lambda_config_customrule.arn}:log-stream:*",
]
}
statement {
effect = "Allow"
actions = [
"config:PutEvaluations",
]
resources = [
"*"
]
}
statement {
effect = "Allow"
actions = [
"secretsmanager:GetSecretValue",
]
resources = [
aws_secretsmanager_secret_version.example.arn
]
}
}
AWS Configの定義
AWS Configの定義は前回の記事と大体同じなので割愛する。
下記に加えて、IAMロールやAmazon S3バケットの設定を行っておこう(内容は前回記事と同じ)。
aws_config_config_rule
のsource
ブロックのみ異なるので確認しておいていただきたい。
resource "aws_config_configuration_recorder" "example" {
name = local.config_recorder_name
role_arn = aws_iam_role.config.arn
}
resource "aws_config_configuration_recorder_status" "example" {
depends_on = [aws_config_delivery_channel.example]
name = aws_config_configuration_recorder.example.name
is_enabled = true
}
resource "aws_config_delivery_channel" "example" {
depends_on = [aws_config_configuration_recorder.example]
name = local.config_delivery_channel_name
s3_bucket_name = aws_s3_bucket.example.bucket
snapshot_delivery_properties {
delivery_frequency = "One_Hour"
}
}
resource "aws_config_config_rule" "example" {
depends_on = [
aws_config_configuration_recorder.example,
aws_lambda_permission.allow_config,
]
name = local.config_rule_name
source {
owner = "CUSTOM_LAMBDA"
source_identifier = aws_lambda_function.config_customrule.arn
source_detail {
message_type = "ScheduledNotification"
maximum_execution_frequency = "One_Hour"
}
}
}
これをApplyしてしばらく待つと、AWS Lambdaが実行され、以下のようにリポジトリごとの準拠状況が確認できるようになった!
なお、リソースのタイムラインを追うには、カスタムリソースの1つ1つをAWS Configに登録する必要がある。GitLab側でリポジトリが作成されたり、保護設定が変更されたときにフックするには、EventBridgeが必要だが、GitLabはAmazon Eventbridgeのクイックスタートの対応がまだ行われていないため、また別途検討を行う必要がある。