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

# 前提条件
いろいろごちゃ混ぜなことをしてしまったので、以下の前提。
Lambda, Python, SAMはセットみたいなものだから、ごちゃ混ぜのようでそんなに敷居は高くないか。

- Pythonをちょっと書いたことがある
- SAMテンプレートをちょっと書いたことがある
- Lambdaのイベントハンドラまわりの仕様をそれなりに理解している
- CloudWatch Eventsをなんとなく知ってる
- SonarQubeをなんとなく知ってる

# いきなりIaC
マネージメントコンソールの画面ポチポチで作るのは面倒なので、いきなりSAMテンプレートを書いて追って解説をする。
こんなPythonのコードをインラインで書くなよ……というツッコミはしない。
あと、どう考えてもこれはStepFunctionsで実装した方がカッコイイ気がするけど、SAMでまとめてデプロイとかできなくて面倒なので、一旦このかたちにする。

本題のSAMテンプレートは以下。

```YAML
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: |
        # ↓のコードをインラインで書いておく
```

```Python: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```が必要になる。というか、これがないと権限エラーのハマりポイントとなる。

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ssm:PutParameter",
                "sts:AssumeRole"
            ],
            "Resource": "*"
        }
    ]
}
```

# ★2 CodeCommitでブランチ作成を検知するCloudWatch Eventsのイベント
以下がそれに該当するIaC。

```YAML
      Events:
        SonarAddEvent:
          Type: CloudWatchEvent
          Properties:
            Pattern: 
              source: [ aws.codecommit ]
              detail-type: [ CodeCommit Repository State Change ]
              detail:
                event: [ referenceCreated ]
```

これを読ませることで、Lambdaのイベントトリガが以下の様に設定される。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/d58b6bd3-9f69-88ed-ee75-e49cb60056e9.png)

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

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

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

【AWS公式】[Amazon EventBridge および Amazon CloudWatch Eventsでの CodeCommit イベントのモニタリング](https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/monitoring-events.html#referenceCreated)

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

![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/51ef379c-eecd-3f49-e45f-830addd20ee7.png)

# index.py
↑のイベントモニタリングのリンクでは、各イベントを指定した場合にイベントハンドラにどんなJSONが渡ってくるかが記載されている。
今回は、
- ```[リポジトリ名]-[ブランチ名]```なプロジェクト名とキーを作成し
- ```[リポジトリ名]-[ブランチ名]-token```を払い出してParameter StoreにPutする
という仕様にしている。

そのために、イベントハンドラ(JSONディクショナリ形式)から

```Python
    key_value=event['detail']['repositoryName']+"-"+event['detail']['referenceName']
```

といった感じで、情報を抜き出してから

```Python
request = urllib.request.Request(sonarqube_url+'api/projects/create?key='+key_value+'&name='+key_value, data)
```

としてSonarQubeのAPIに情報を渡している。

ちなみに、Pythonのurllibの仕様については、以下のリンクが分かりやすかった。

[urllib パッケージを使ってインターネット上のリソースを取得するには](https://docs.python.org/ja/3/howto/urllib2.html)

SonarQubeのAPI仕様は、```SonarQubeのルートパス/web_api```から確認できるので、それぞれ見ておいた方がよい。微妙に、SonarQubeのAPI仕様はデフォルト設定での認証要否の仕様等分かりにくく、curlしてエラーコードを確認しつつなやり方であった。
今回であれば、

```Python
    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認証の情報を作っている。

```Python
    # Basic Authentication
    basic_user_and_pasword = base64.b64encode('{}:{}'.format('[SonarQubeのユーザID]', '[SonarQubeのユーザパスワード]').encode('utf-8'))
```

今回はおためしで作っているのでテキトーに書いているが、認証情報をハードコーディングするのはアンチパターンなので、良い子はちゃんとParameter StoreからGetしてくるようにすること。

最後にParameter Storeに取得したトークンを突っ込んであげれば完成。

boto3の```put_parameter```の仕様は[こちら](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.put_parameter)