前提条件
いろいろごちゃ混ぜなことをしてしまったので、以下の前提。
Lambda, Python, SAMはセットみたいなものだから、ごちゃ混ぜのようでそんなに敷居は高くないか。
- Pythonをちょっと書いたことがある
- SAMテンプレートをちょっと書いたことがある
- Lambdaのイベントハンドラまわりの仕様をそれなりに理解している
- CloudWatch Eventsをなんとなく知ってる
- SonarQubeをなんとなく知ってる
いきなりIaC
マネージメントコンソールの画面ポチポチで作るのは面倒なので、いきなりSAMテンプレートを書いて追って解説をする。
こんなPythonのコードをインラインで書くなよ……というツッコミはしない。
あと、どう考えてもこれはStepFunctionsで実装した方がカッコイイ気がするけど、SAMでまとめてデプロイとかできなくて面倒なので、一旦このかたちにする。
本題のSAMテンプレートは以下。
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Create Pipeline Lambda for Branch Make
Parameters:
Prefix:
Description: "Project name prefix"
Type: "String"
Default: "SonarAdd"
LambdaFunctionNameSuffix:
Description: "Lambda function name suffix"
Type: "String"
Default: "-LambdaFunction"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Project name prefix"
Parameters:
- Prefix
- Label:
default: "Lambda Configuration"
Parameters:
- LambdaFunctionNameSuffix
Globals:
Function:
Timeout: 60
Resources:
SonarAdd:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${Prefix}${LambdaFunctionNameSuffix}
Handler: index.lambda_handler
Runtime: python3.7
MemorySize: 128
# ★1
Role: [適当なLambdaのサービスロール]
# ★2
Events:
SonarAddEvent:
Type: CloudWatchEvent
Properties:
Pattern:
source: [ aws.codecommit ]
detail-type: [ CodeCommit Repository State Change ]
detail:
event: [ referenceCreated ]
InlineCode: |
# ↓のコードをインラインで書いておく
import json
import pprint
import base64
import urllib.request
import urllib.parse
from urllib.error import URLError, HTTPError
import boto3
def lambda_handler(event, context):
# Configure
sonarqube_url = 'http://[SonarQubeの起動してるドメイン]/'
### SonarQube Project Add ###
# URL Create
key_value=event['detail']['repositoryName']+"-"+event['detail']['referenceName']
data = urllib.parse.urlencode('')
data = data.encode('ascii')
request = urllib.request.Request(sonarqube_url+'api/projects/create?key='+key_value+'&name='+key_value, data)
try:
response = urllib.request.urlopen(request)
except HTTPError as e:
pprint.pprint('SonarQube Project Add Failed.')
pprint.pprint('Error code: '+str(e.code))
print(e.reason)
print(e.read())
except URLError as e:
pprint.pprint('SonarQube Project Add Failed.')
pprint.pprint('Reason: '+e.reason)
return {
'statusCode': 500,
'body': json.dumps('SonarQube Project Add Failed')
}
# For Debug
pprint.pprint(response.read().decode('utf-8'))
### SonarQube Create Key ###
# Basic Authentication
basic_user_and_pasword = base64.b64encode('{}:{}'.format('[SonarQubeのユーザID]', '[SonarQubeのユーザパスワード]').encode('utf-8'))
# URL Create
key_value=event['detail']['repositoryName']+"-"+event['detail']['referenceName']
data = urllib.parse.urlencode('')
data = data.encode('ascii')
request = urllib.request.Request(sonarqube_url+'api/user_tokens/generate?name='+key_value, data, headers={"Authorization": "Basic " + basic_user_and_pasword.decode('utf-8')})
try:
response = urllib.request.urlopen(request)
except HTTPError as e:
pprint.pprint('SonarQube Create Key Failed.')
pprint.pprint('Error code: '+str(e.code))
print(e.reason)
print(e.read())
return {
'statusCode': 500,
'body': json.dumps('SonarQube Create Key Failed')
}
except URLError as e:
pprint.pprint('SonarQube Create Key Failed.')
pprint.pprint('Reason: '+e.reason)
return {
'statusCode': 500,
'body': json.dumps('SonarQube Create Key Failed')
}
response_body = json.loads(response.read().decode('utf-8'))
# Put token to ParameterStore
ssm = boto3.client('ssm')
response = ssm.put_parameter(
Name = key_value+"-token",
Type = 'String',
Value = response_body['token'],
Overwrite=True
)
return {
'statusCode': 200,
'body': json.dumps('SonarQube Create Key Succeeded!')
}
流し込むためのCLIの例は以下のような感じで。
$ aws cloudformation deploy --template-file SAM_Lambda_MakePipeline.yml --stack-name SonarAdd --parameter-overrides ParameterKey=Prefix,ParameterValue=SonarAdd
★1 適当なサービスロール
事前にRole
で指定しているLambdaのサービスロールに以下の権限を持ったポリシを付与しておく。最後にParameter Storeに書き込むところでAssumeRoleした先のロールにssm:PutParameter
が必要になる。というか、これがないと権限エラーのハマりポイントとなる。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"sts:AssumeRole"
],
"Resource": "*"
}
]
}
★2 CodeCommitでブランチ作成を検知するCloudWatch Eventsのイベント
以下がそれに該当するIaC。
Events:
SonarAddEvent:
Type: CloudWatchEvent
Properties:
Pattern:
source: [ aws.codecommit ]
detail-type: [ CodeCommit Repository State Change ]
detail:
event: [ referenceCreated ]
これを読ませることで、Lambdaのイベントトリガが以下の様に設定される。
生成されるイベントパターンはこんな感じ。
{
"detail-type": [
"CodeCommit Repository State Change"
],
"source": [
"aws.codecommit"
],
"detail": {
"event": [
"referenceCreated"
]
}
}
この中のreferenceCreated
がブランチの作成を意味している。
イベントには他にも色々な種別があるので、目的に合わせて変更することができる。
すべてのイベントでトリガしたい場合は、逆にこのプロパティを消せば良い。
書いてあることで絞り込みをするイメージだ。
【AWS公式】Amazon EventBridge および Amazon CloudWatch Eventsでの CodeCommit イベントのモニタリング
CloudWatchイベントのルールにもちゃんと登録されている。
index.py
↑のイベントモニタリングのリンクでは、各イベントを指定した場合にイベントハンドラにどんなJSONが渡ってくるかが記載されている。
今回は、
-
[リポジトリ名]-[ブランチ名]
なプロジェクト名とキーを作成し -
[リポジトリ名]-[ブランチ名]-token
を払い出してParameter StoreにPutする
という仕様にしている。
そのために、イベントハンドラ(JSONディクショナリ形式)から
key_value=event['detail']['repositoryName']+"-"+event['detail']['referenceName']
といった感じで、情報を抜き出してから
request = urllib.request.Request(sonarqube_url+'api/projects/create?key='+key_value+'&name='+key_value, data)
としてSonarQubeのAPIに情報を渡している。
ちなみに、Pythonのurllibの仕様については、以下のリンクが分かりやすかった。
urllib パッケージを使ってインターネット上のリソースを取得するには
SonarQubeのAPI仕様は、SonarQubeのルートパス/web_api
から確認できるので、それぞれ見ておいた方がよい。微妙に、SonarQubeのAPI仕様はデフォルト設定での認証要否の仕様等分かりにくく、curlしてエラーコードを確認しつつなやり方であった。
今回であれば、
request = urllib.request.Request(sonarqube_url+'api/user_tokens/generate?name='+key_value, data, headers={"Authorization": "Basic " + basic_user_and_pasword.decode('utf-8')})
の部分がBASIC認証要だったので、事前に以下のコードでBASIC認証の情報を作っている。
# Basic Authentication
basic_user_and_pasword = base64.b64encode('{}:{}'.format('[SonarQubeのユーザID]', '[SonarQubeのユーザパスワード]').encode('utf-8'))
今回はおためしで作っているのでテキトーに書いているが、認証情報をハードコーディングするのはアンチパターンなので、良い子はちゃんとParameter StoreからGetしてくるようにすること。
最後にParameter Storeに取得したトークンを突っ込んであげれば完成。
boto3のput_parameter
の仕様はこちら