LoginSignup
0
0

More than 3 years have passed since last update.

CodeCommitのイベントから自動でSonarQubeのプロジェクトを設定するLambda関数のSAMテンプレート

Last updated at Posted at 2020-05-06

前提条件

いろいろごちゃ混ぜなことをしてしまったので、以下の前提。
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: |
        # ↓のコードをインラインで書いておく
index.py
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のイベントトリガが以下の様に設定される。

キャプチャ.PNG

生成されるイベントパターンはこんな感じ。

{
  "detail-type": [
    "CodeCommit Repository State Change"
  ],
  "source": [
    "aws.codecommit"
  ],
  "detail": {
    "event": [
      "referenceCreated"
    ]
  }
}

この中のreferenceCreatedがブランチの作成を意味している。
イベントには他にも色々な種別があるので、目的に合わせて変更することができる。
すべてのイベントでトリガしたい場合は、逆にこのプロパティを消せば良い。
書いてあることで絞り込みをするイメージだ。

【AWS公式】Amazon EventBridge および Amazon CloudWatch Eventsでの CodeCommit イベントのモニタリング

CloudWatchイベントのルールにもちゃんと登録されている。

キャプチャ2.PNG

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の仕様はこちら

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0