はじめに
Microsoft365の監査ログの保存期間がデフォルト90日なので、それを長期間保存するためS3に出力するLambdaを作成しました。
参考
構成図
構成図は以下の様になります。
- 任意のサービスが、対象のLambdaをInvokeします。
- Step Functionsを想定
- 与える引数は以下
- 監査ログ取得対象開始日時
- 監査ログ取得対象終了日時
- 保存先バケット
- オブジェクトキー名
- Microsoft365への接続に使う情報はSystemsManagerのParameter Storeから取得します。
- 格納先のParameter Storeの名前は環境変数で指定
- 取得した内容は一度ローカルにJSONファイルで保存し、最後にS3へPUTします。
このLambda関数の仕組み
- Microsoft365の監査ログを取得するためにPowerShellを使います。
- 普通のLambdaのOS(Amazon Linux2)では取得に使うモジュールが入らないので、OSがUbuntuのコンテナイメージから作成します。
- Microsoft公式にあるPowerShellインストール済みのイメージを使用
- PowerShellとLambdaとやり取りするRICは無いので、Pythonに取り持ってもらいます。
- Pythonから、PowerShellの処理を実行する形
環境
いつものCloud9になります。詳細は以下と同じです。
やったこと
準備
ディレクトリ作成したり、よく使う文字列を環境変数に入れておきます。
# プロジェクトディレクトリ作成
mkdir sample-ecr-lambda
cd sample-ecr-lambda
# リージョンとアカウントIDを環境変数にセット
REGION="ap-northeast-1"
ACCOUNTID=$(aws sts get-caller-identity --output text --query Account)
# 保存先のバケット名。何度も使うのでセット
BUCKETNAME="<<BUCKET_NAME>>"
基本のフォルダ構成は以下のようになります。その他CFnファイルなどは都度作成していきます。
sample-ecr-lambda
├── script
│ ├── mycert.pfx
│ └── GetMs365AuditLog.ps1
├── function
│ └── app.py
└── Dockerfile
script/mycert.pfx
は、以下のページの認証方法を用いる際の認証ファイルになります。
他の方法もありますので、そちらを使う際は適宜script/GetMs365AuditLog.ps1
も書き換えてください。
接続情報をパラメータストアに登録
MS365との接続情報は、パラメータストアで管理します。Secrets Managerも検討しましたが、そこまでの機能はいらないと考えこちらにしました。
Connect-ExchangeOnlineで与える引数をまとめて、JSON文字列にしてファイルに保存します。
{
"MS365ORG":"<<.onmicrosoft.comでおわる組織>>",
"APPID":"<<アプリケーション ID>>",
"CERTPASSWORD":"<<認証ファイルのパスワード>>"
}
パラメータストアは、CloudFormationではSecureStringが使えないので、AWS CLIを使って登録します。
名前にスラッシュが入っていると、IAMポリシー設定時に指定できなくなるので、スラッシュがない名前にしています。
# パラメータストアの名前を格納
SSMPARAMETERNAME="GetMs365AuditLog-ExchangeOnlineParameters"
# 作成
aws ssm put-parameter --region ${REGION} --name ${SSMPARAMETERNAME} --type SecureString --value file://env.json
スクリプト・Dockerfile
MS365監査ログを取得するPoweshellファイルは以下になります。以下のページを参考にしました。
-
Connect-ExchangeOnline
でCommandName
を使って、用いるコマンドを制限しておくと早くなるらしいので指定しています。 - ファイルに出力する際は、Athenaで扱えるよう1行ずつ、改行区切りのJSON文字列にしています。
$AWS = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($args)) | ConvertFrom-Json
# Get from ParameterStore
# Json文字列で入っているため、ConvertFrom-Jsonでそのまま扱う
$ExchangeOnlineParameters = (Get-SSMParameter -Name $Env:SSMPARAMETERNAME -WithDecryption $True).Value | ConvertFrom-Json
$resultSize = 5000
# 繰り返し取得する際に使う、一意のセッションID
$sessionID = [Guid]::NewGuid().ToString() + "_" + "ExtractLogs" + (Get-Date).ToString("yyyyMMddHHmmssfff")
$OutputFile = "/tmp/" + $sessionID
# 空のファイル作成
New-Item $OutputFile -type file -Force
try
{
# Connect
# CommandNameオプションで使用するコマンドを制限しておくとよいらしい
Connect-ExchangeOnline `
-CommandName Search-UnifiedAuditLog `
-CertificateFilePath "/script/mycert.pfx" `
-CertificatePassword `
(ConvertTo-SecureString -String $ExchangeOnlineParameters.CERTPASSWORD -AsPlainText -Force) `
-AppID $ExchangeOnlineParameters.APPID `
-Organization $ExchangeOnlineParameters.MS365ORG `
-ErrorAction Stop
do
{
$each_result = Search-UnifiedAuditLog `
-StartDate $AWS.event.StartDate -EndDate $AWS.event.EndDate `
-SessionId $sessionID `
-SessionCommand ReturnLargeSet `
-ResultSize $resultSize `
-ErrorAction Stop
if (($each_result | Measure-Object).Count -ne 0)
{
$results = $results + $each_result
}
}
while (($each_result | Measure-Object).Count -ne 0)
# ファイルに出力
## Athenaで扱えるよう、各行1データ
foreach($eachLog in $results)
{
ConvertTo-Json -Compress $eachLog | Out-File $OutputFile -Encoding utf8 -Append
}
# Copy to S3
Write-S3Object -BucketName $AWS.event.BucketName -Key "$($AWS.event.Key)" -File $OutputFile
}
finally
{
Disconnect-ExchangeOnline -Confirm:$false
}
Powershellを起動するだけのPythonは以下になります。
CloudWatch Logsに出力される際、ひとかたまりにするためloggingを使っています。(これでもあまり見栄えよくないです、どなたかわかる方教えてください)
import subprocess
import json
import base64
from logging import getLogger, INFO
logger = getLogger(__name__)
logger.setLevel(INFO)
def convertto_base64(input_object):
return str(base64.b64encode(json.dumps(input_object).encode('utf-8')), 'utf-8')
def handler(event, context):
encoded_event_and_context = convertto_base64({
'event': event
})
cmd = ['pwsh', '-Command', f'/script/GetMs365AuditLog.ps1 {encoded_event_and_context}']
o = subprocess.run(cmd, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = o.stdout.strip()
stderr = o.stderr.strip()
logger.info( json.dumps(stdout, ensure_ascii=False, indent=2) )
logger.info( json.dumps(stderr, ensure_ascii=False, indent=2) )
return {
"status": len(stderr)==0 ,
"body": json.dumps(
{
"target": event,
"stdout": stdout,
"stderr": stderr
}
)
}
Dockerfileは以下になります。
FROM mcr.microsoft.com/powershell:lts-7.2-ubuntu-20.04 AS pip
RUN apt-get update && apt-get install -y python3-pip
RUN mkdir /function && \
pip install --target /function awslambdaric
############################################################
FROM mcr.microsoft.com/powershell:lts-7.2-ubuntu-20.04
RUN apt-get update && apt-get install -y python3
# Install powershell modules
RUN pwsh -command 'Install-Module -Name ExchangeOnlineManagement -Scope AllUsers -Force \
&& Install-Module -Name PSWSMan -Scope AllUsers -Force \
&& Install-Module -Name AWS.Tools.Installer -Scope AllUsers -Force \
&& Install-AWSToolsModule AWS.Tools.SimpleSystemsManagement,AWS.Tools.S3 -Scope AllUsers -Force \
&& Install-WSMan'
COPY --from=pip /function /function
COPY function/app.py /function
RUN mkdir /script
COPY script/mycert.pfx script/GetMs365AuditLog.ps1 /script/
WORKDIR /function
ENTRYPOINT [ "/usr/bin/python3", "-m", "awslambdaric" ]
CMD [ "app.handler" ]
できたらBuildします。
docker build -t func1 .
ローカルでテスト
作成したイメージをローカルで動かす際は、以前書いた記事のようにします。
まずは動かします。エントリポイントは無効化して、使う環境変数を持ってきます。
docker run --rm -it \
--name=testGetAuditLog \
--entrypoint= \
-e SSMPARAMETERNAME=$SSMPARAMETERNAME \
-e BUCKETNAME=$BUCKETNAME \
func1 bash
PowerShellのファイルを実行
コンテナ内で先のPS1ファイルを動かす際には以下のように、渡すeventを作成して、それを引数として実行しました。
$ pwsh
PS > $event = ConvertTo-Json @{ 'event' = @{
'StartDate'='2022-07-25 09:00:00'
'EndDate'='2022-07-25 09:59:59'
'BucketName'=$Env:BUCKETNAME
'Key'='testwrite/20220725090000.csv'
}}
PS > /script/GetMs365AuditLog.ps1 $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($event)))
Pythonのファイルを実行
コンテナ内でPythonファイルで動かす際は以下のようにします。
$ python3
>>> import os
>>> import app
>>> event = {
'StartDate':'2022-07-26 09:00:00',
'EndDate':'2022-07-26 09:59:59',
'BucketName':os.environ['BUCKETNAME'],
'Key':'testwrite/20220726090000.csv'
}
>>> app.handler(event,None)
修正したい場合はホストのファイルを修正して、docker cp
でコンテナ内へコピー
# 別のターミナルから
CURID=$(docker ps -q --filter "name=testGetAuditLog")
docker cp function/app.py $CURID:/function/app.py & docker cp script/GetMs365AuditLog.ps1 $CURID:/script/GetMs365AuditLog.ps1
修正が終わったらもう一度docker build
を忘れずに。
ローカルでLambdaをエミュレート
前回書いた記事と同様、RIEをホストにダウンロードしコンテナと共有して、エミュレートします。
# RIEダウンロード
mkdir aws-lambda-rie
curl -Lo aws-lambda-rie/aws-lambda-rie \
https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie
chmod +x aws-lambda-rie/aws-lambda-rie
# コンテナ起動
docker run --rm -p 9000:8080 \
--name=testGetAuditLog \
-v `pwd`/aws-lambda-rie:/aws-lambda \
-e SSMPARAMETERNAME=$SSMPARAMETERNAME \
--entrypoint /aws-lambda/aws-lambda-rie \
func1:latest \
/usr/bin/python3 -m awslambdaric app.handler
# テスト
## 別ターミナルから
### ターミナルが別なので、そちらでも環境変数をセット
BUCKETNAME="<<BUCKET_NAME>>"
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d "{
\"StartDate\":\"2022-07-27 09:00:00\",
\"EndDate\":\"2022-07-27 09:59:59\",
\"BucketName\":\"${BUCKETNAME}\",
\"Key\":\"testwrite/20220727090000.csv\"
}"
レジストリに登録
以前と同様にECRをCFnで作成します。
# ECRリポジトリ作成CFn
touch createECRRepository.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Create ECR Stack Test
Resources:
########################################################
### ECR Repository
########################################################
TestEcrPoc:
Type: AWS::ECR::Repository
Properties:
RepositoryName: func1
# ECR作成
aws cloudformation create-stack --stack-name tempecrrepo --template-body file://createECRRepository.yaml --region ${REGION}
# タグ付与
docker tag func1:latest \
${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1:latest
# 認証
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com
# PUSH サイズは250MB程度
docker push ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1:latest
Lambada作成・実行
Lambadaを作成するCFnファイルを記述します。
# Lambda作るCFn
touch createLambda.yaml
たくさん書いてありますが、以下を作っています。
- Lambda
- イメージのサイズが大きいので、メモリサイズも大きいものにしています。
- 接続に時間がかかるので、タイムアウトは最大値で設定。
- Lambda用ロググループ
- Lambda用IAMロール
- ロググループ書き込み
- S3書き込み
- パラメータストア取得
CFnのパラメータは以下になります。
- Lambdaの名前
- ECRのイメージURI
- ファイルを置くバケット名
- パラメータストアの名前
クリックで表示
AWSTemplateFormatVersion: "2010-09-09"
Description: Get MS365 Audit Log Function
Parameters:
LambdaFunctionName:
Type: String
ImageUri:
Type: String
TargetBucket:
Type: String
TargetParameterStore:
Type: String
Resources:
########################################################
### Log Group
########################################################
FunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${LambdaFunctionName}"
RetentionInDays: 3653
########################################################
### IAM Role
########################################################
FunctionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "for-lambdafunction-${LambdaFunctionName}"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
Path: '/service-role/'
Policies:
# CloudWatch
- PolicyName: write-cloudwatchlogs
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:*"
# S3
- PolicyName: write-s3object
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 's3:PutObject'
Resource: !Sub "arn:aws:s3:::${TargetBucket}/*"
# SSM Parameter Store
- PolicyName: get-ssmparameter
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'ssm:GetParameter'
Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${TargetParameterStore}"
########################################################
### Lambda Function
########################################################
TargetFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Ref LambdaFunctionName
Role: !GetAtt FunctionRole.Arn
MemorySize: 384
Timeout: 900
PackageType: Image
Code:
ImageUri: !Ref ImageUri
Environment:
Variables:
SSMPARAMETERNAME: !Ref TargetParameterStore
作成したCFnのファイルから、Lambdaをデプロイし実行します。
# CFnのパラメータに使う文字列を生成
DIGEST=$(aws ecr list-images --repository-name func1 --region ${REGION} --out text --query 'imageIds[?imageTag==`latest`].imageDigest')
IMAGEURI=${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1@${DIGEST}
# LambdaデプロイCFn実行。IAM作るのでcapabilities指定
aws cloudformation create-stack --stack-name tempecrlambda \
--template-body file://createLambda.yaml \
--region ${REGION} \
--parameters \
ParameterKey=LambdaFunctionName,ParameterValue=get-ms365auditlog \
ParameterKey=ImageUri,ParameterValue=${IMAGEURI} \
ParameterKey=TargetBucket,ParameterValue=${BUCKETNAME} \
ParameterKey=TargetParameterStore,ParameterValue=${SSMPARAMETERNAME} \
--capabilities CAPABILITY_NAMED_IAM
# 実行
aws lambda invoke \
--function-name get-ms365auditlog \
--payload "{
\"StartDate\":\"2022-07-28 09:00:00\",
\"EndDate\":\"2022-07-28 09:59:59\",
\"BucketName\":\"${BUCKETNAME}\",
\"Key\":\"testwrite/20220728090000.csv\"
}"\
--region ${REGION} output ; cat output
片づけ
# Lambda削除
aws cloudformation delete-stack --stack-name tempecrlambda --region ${REGION}
# ECR上イメージ削除
aws ecr batch-delete-image --repository-name func1 --region ${REGION} --image-ids imageDigest=${DIGEST}
# ECRリポジトリ削除
aws cloudformation delete-stack --stack-name tempecrrepo --region ${REGION}
# dockerイメージ削除
docker rmi <<対象イメージ>>
# その他作られたイメージを削除
docker image prune
おわりに
今回はLambdaでMicrosoft365の監査ログを取得する関数を作ってみました。
Azure Functions使った方が楽かも…とも思いましたが、AWSのほうが使い勝手がいいので、何とか実装しました。
同様の課題をお持ちの方の参考になれば幸いです。