自己紹介
ZOZOテクノロジーズでPB(プライベートブランド)のインフラを構築しているエンジニアです。
所属としてはSREを担当しています。
なぜ ChatOps なのか
その前にChatOpsとはどのようなものなのか知らないかたは以下をご参照ください
- ChatOpsの参考事例
ChatOpsを導入理由は、デプロイを早くしたい、運用系の仕事を減らしたいからです。
デプロイが早くなれば、テスト期間が短くなり、結果的にサービスの改善が早くなります。
サービスが成長している段階においては、スピード感が重要です。
また、運用系の仕事も減り、開発やその他の作業に回せる時間も増えます。
運用の時間を減らし、開発の時間を増やすのもSREのモチベーションの1つとなります。
おすすめの本:
- 
Hacking Growth グロースハック完全読本 - ショーン・エリス
- 固定リンク: http://amzn.asia/d/3LdCecn
 
- 
SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム - 澤田 武男
- 固定リンク: http://amzn.asia/d/9gO5uSn
 
なぜ、この構成なのか
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を使用している
構築方法は基本的に以下を参考にしています。
- https://dev.classmethod.jp/cloud/aws/slack-integration-blueprint-for-aws-lambda/
- https://dev.classmethod.jp/server-side/slash-commands-to-lambda/
Slack Commandsの作成
- https://[slack名].slack.com/apps にアクセスして「Slash Commands」を検索
- Slash Commands を作成する(図1)
- 「Choose a Command」にSlackチャンネルで呼び出すコマンド名を入力
- 「Token」をメモにコピーする(図2)
図1: Slash Commands
 
図2: Slash Commands
 
CloudFormation (CFn) で AWS環境を構築
- ディレクトリとファイルの作成(図3)
- chatops.py に(図4)の内容をコピー
- 
<デプロイできるSlackのチャンネル名>はChatOpsを実行するSlackチャンネル名に変更する- 例)general
 
- chatops.yaml に(図5)の内容をコピー
- AWS CLI で CFn を実行(図6)
- 参考URL
- CircleCI API: https://circleci.com/docs/api/v1-reference/#new-build
 
図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でトークンを作成
- CircleCIにログイン: https://circleci.com/gh/<ユーザ名>/<githubのリポジトリ名>/edit#api
- 「Create Token」を押して、トークンを作成(図7)
- 
First choose a scope and then create a label.: ALL
- 
Token label: all
- 作成されたトークンの値をメモにコピー
図7: CircleCI
AWS CLI (AWS KMS) でトークンを作成
- Slack Commands 作成時にメモにコピーしたトークンを用意
- AWS CLIを使ってトークンを暗号化(図8)
- AWS KMSに作成された鍵を使ってトークンを暗号化します
- 図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 に環境変数を設定
- Lambdaの「Environment variables」にある以下の環境変数に値を入力(図9)
- baseUrl: https://circleci.com/api/v1.1/project/github/<ユーザ名>/<Githubのリポジトリ名>
- circleciToken: <CircleCIで作成したトークン>
- kmsEncryptedToken: <aws cliで作成したトークン>
図9: Lambda
Slash Commands で API Gateway のURLを入力
- API Gatewayに移動し「chatops」という名前のAPIをクリック
- Stage → Prod → ChatOps → POST と移動して URL をコピー(図10)
- 最初に作ったSlash Commandsに移動
- URLに上記のURLを貼り付け(図11)
- 「Show this command in the autocomplete list」にチェックを入れる(図12)
- 「Save Integration」を押して完了
図10: API Gateway
図11: Slash Commands
図12: Slash Commands
Slackで確認
- chatops.py で書き直した「<デプロイできるSlackのチャンネル名>」のチャンネルで以下を実行
- /chatops <Githubのブランチ名> <CircleCI Job名>
- レスポンスが返ってきてURLにアクセスして、CircleCIのジョブが実行されていることを確認




