自己紹介
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のジョブが実行されていることを確認