LoginSignup
19
8

More than 5 years have passed since last update.

CircleCI + AWS Lambda function + Slackを使ったChatOps環境構築

Last updated at Posted at 2018-12-03

自己紹介

ZOZOテクノロジーズでPB(プライベートブランド)のインフラを構築しているエンジニアです。
所属としてはSREを担当しています。

なぜ ChatOps なのか

その前にChatOpsとはどのようなものなのか知らないかたは以下をご参照ください

ChatOpsを導入理由は、デプロイを早くしたい、運用系の仕事を減らしたいからです。
デプロイが早くなれば、テスト期間が短くなり、結果的にサービスの改善が早くなります。
サービスが成長している段階においては、スピード感が重要です。
また、運用系の仕事も減り、開発やその他の作業に回せる時間も増えます。
運用の時間を減らし、開発の時間を増やすのもSREのモチベーションの1つとなります。

おすすめの本:

  • Hacking Growth グロースハック完全読本

  • SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

なぜ、この構成なのか

ZOZOテクノロジーズのPB部では、既にCIrcleCIを導入し自動デプロイまでは完成していました。
「ChatOpsやりたいよね」という話があり、CircleCIのAPIを利用できないかと思った次第です。
ChatOpsの導入というと敷居が高いように感じますが、今回作ったChatOpsはそんなに難しくはありません、みなさんもぜひ導入してみてください。

どのように構築するのか

では本題に入っていきます。

前提

  • AWS環境を使える
    • CloudFormation (CFn)
    • Lambda Function (lambda)
    • Key Management Service (KMS)
    • API Gateway
    • AWS CLI
  • CircleCIで何かしらのジョブが既に作られている
    • APIでジョブを実行します
  • GithubとCircleCIを連携させている
  • Slackを使用している

構築方法は基本的に以下を参考にしています。

Slack Commandsの作成

  1. https://[slack名].slack.com/apps にアクセスして「Slash Commands」を検索
  2. Slash Commands を作成する(図1)
  3. 「Choose a Command」にSlackチャンネルで呼び出すコマンド名を入力
  4. 「Token」をメモにコピーする(図2)

図1: Slash Commands

図1

図2: Slash Commands

図2

CloudFormation (CFn) で AWS環境を構築

  1. ディレクトリとファイルの作成(図3)
  2. chatops.py に(図4)の内容をコピー
    1. <デプロイできるSlackのチャンネル名> はChatOpsを実行するSlackチャンネル名に変更する
      1. 例)general
  3. chatops.yaml に(図5)の内容をコピー
  4. AWS CLI で CFn を実行(図6)

図3: Bash

$ mkdir chatops && cd $_
$ mkdir src && touch src/chatops.py
$ touch chatops.yaml
$ tree
.
├── chatops.yaml
└── src
    └── chatops.py

図4: chatops.py


import boto3
import json
import logging
import os
import urllib
import pprint

from base64 import b64decode

ENCRYPTED_EXPECTED_TOKEN = os.environ['kmsEncryptedToken']
CIRCLECI_TOKEN = os.environ['circleciToken']
BASE_URL = os.environ['baseUrl']

kms = boto3.client('kms')
expected_token = kms.decrypt(CiphertextBlob=b64decode(
   ENCRYPTED_EXPECTED_TOKEN))['Plaintext'].decode()

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def circle_trigger_job(BRANCH, JOB):
   circleci_url = BASE_URL + '/tree/' + urllib.parse.quote(BRANCH) + '?circle-token=' + CIRCLECI_TOKEN
   payload = {
       'build_parameters': {
           'CIRCLE_JOB': JOB
       }
   }

   method = "POST"
   headers = {"Content-Type" : "application/json"}
   json_data = json.dumps(payload).encode("utf-8")

   request = urllib.request.Request(circleci_url, data=json_data, method=method, headers=headers)
   with urllib.request.urlopen(request) as response:
       string = response.read().decode('utf-8')
       list = string.split()
       return list[list.index(':build_url') + 1]

def respond(err, res=None):
   return {
       'statusCode': '400' if err else '200',
       'attachments': [
           {
               "text": err.message if err else res
           }
       ],
       'headers': {
           'Content-Type': 'application/json',
       },
       'response_type': 'in_channel'
   }

def lambda_handler(event, context):
   print(event)
   params = urllib.parse.parse_qs(event['body'],encoding='utf-8')
   token = params['token'][0]
   if token != expected_token:
       logger.error("Request token (%s) does not match expected", token)
       return respond(Exception('Invalid request token'))

   user = params['user_name'][0]
   command = params['command'][0]
   channel = params['channel_name'][0]
   text = params['text'][0].split(" ")
   branch = text[0]
   job = text[1]
   response = ''

   if channel == '<デプロイできるSlackのチャンネル名>':
       response = 'build_url: ' + circle_trigger_job(branch, job).strip(',"')
   else:
       response = 'use in `<デプロイできるSlackのチャンネル名>` slack channel'

   print(response)
   return respond(None, "%s \n user: %s" % (response, user)

図5: chatops.yaml


AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
  kmsEncryptedToken:
    Type: String
    Default: ''
  circleciToken:
    Type: String
    Default: ''
  baseUrl:
    Type: String
    Default: ''
Resources:
  Api: 
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      DefinitionBody:
        swagger: 2.0
        info:
          title: chatops
          description: chatops
          version: 1.0.0
        schemes:
          - https
        basePath: /Prod
        paths:
          /chatops:
            post:
              consumes:
                - application/json
              produces:
                - application/json
              responses:
                "200":
                  description: 200 response
                  schema:
                    $ref: "#/definitions/Empty"
              x-amazon-apigateway-integration:
                responses:
                  default:
                    statusCode: 200
                    responseTemplates:
                      application/json: ""
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations
                passthroughBehavior: when_no_templates
                httpMethod: POST
                type: aws
                requestTemplates:
                  application/x-www-form-urlencoded: |
                    {
                        "body": $input.json("$")
                    }
        definitions:
          Empty:
            type: object
            title: Empty Schema

  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: chatops.lambda_handler
      Runtime: python3.6
      CodeUri: ./src
      Environment:
        Variables:
          kmsEncryptedToken: !Ref kmsEncryptedToken
          circleciToken: !Ref circleciToken
          baseUrl: !Ref baseUrl
      Policies:
        - !Ref KMSDecriptPolicy
        - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
      Events:
        PostResource:
          Type: Api
          Properties:
            RestApiId: !Ref Api
            Path: /chatops
            Method: POST

  KMSDecriptPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      Description: "Policy for kms decrypt"
      Path: "/"
      PolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: "Allow"
            Action: "kms:Decrypt"
            Resource: "*"

  KeyId:
    Type: AWS::KMS::Key
    Properties:
      KeyPolicy:
        Version: "2012-10-17"
        Id: key-default-1
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Join 
                - ''
                - - 'arn:aws:iam::'
                  - !Ref 'AWS::AccountId'
                  - ':root'
            Action: 'kms:*'
            Resource: '*'

  KeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/key-alias-for-chatops
      TargetKeyId:
        !Ref KeyId

図6: Bash


$ cd chatops

# S3バケットの作成
$ aws  --profile <プロファイル> --region <リージョン> \
   s3 mb s3://chatops-<リージョン>-<AWSアカウントID>

# デプロイ用ファイルの作成
$ aws --profile <プロファイル> --region <リージョン> \
   cloudformation package \
   --template-file $(pwd)/chatops.yaml \
   --output-template-file $(pwd)/output-chatops.yaml \
   --s3-bucket chatops-<リージョン>-<AWSアカウントID>

# デプロイ
$ aws --profile <プロファイル> --region <リージョン> \
   cloudformation deploy \
   --template-file $(pwd)/output-chatops.yaml \
   --stack-name chatops \
   --capabilities CAPABILITY_IAM

CircleCIでトークンを作成

  1. CircleCIにログイン: https://circleci.com/gh/<ユーザ名>/<githubのリポジトリ名>/edit#api
  2. 「Create Token」を押して、トークンを作成(図7)
    1. First choose a scope and then create a label.: ALL
    2. Token label: all
  3. 作成されたトークンの値をメモにコピー

図7: CircleCI

図7

AWS CLI (AWS KMS) でトークンを作成

  1. Slack Commands 作成時にメモにコピーしたトークンを用意
  2. AWS CLIを使ってトークンを暗号化(図8)
    1. AWS KMSに作成された鍵を使ってトークンを暗号化します
  3. 図8のコマンド実行結果にある「CiphertextBlob」の値をメモにコピー

図8: Bash


$ aws --profile <プロファイル> --region <リージョン> \
   kms encrypt \
   --key-id alias/key-alias-for-chatops \
   --plaintext <Slack Commands 作成時のトークン>

{
    "CiphertextBlob": "XXXX", # これ
    "KeyId": "XXXX"
}

AWS Lambda に環境変数を設定

  1. Lambdaの「Environment variables」にある以下の環境変数に値を入力(図9)
    1. baseUrl: https://circleci.com/api/v1.1/project/github/<ユーザ名>/<Githubのリポジトリ名>
    2. circleciToken: <CircleCIで作成したトークン>
    3. kmsEncryptedToken: <aws cliで作成したトークン>

図9: Lambda

図9

Slash Commands で API Gateway のURLを入力

  1. API Gatewayに移動し「chatops」という名前のAPIをクリック
    1. Stage → Prod → ChatOps → POST と移動して URL をコピー(図10)
  2. 最初に作ったSlash Commandsに移動
    1. URLに上記のURLを貼り付け(図11)
    2. 「Show this command in the autocomplete list」にチェックを入れる(図12)
    3. 「Save Integration」を押して完了

図10: API Gateway

図10

図11: Slash Commands

図11

図12: Slash Commands

図12

Slackで確認

  1. chatops.py で書き直した「<デプロイできるSlackのチャンネル名>」のチャンネルで以下を実行
    1. /chatops <Githubのブランチ名> <CircleCI Job名>
  2. レスポンスが返ってきてURLにアクセスして、CircleCIのジョブが実行されていることを確認
19
8
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
19
8