Upgrade to Pro — share decks privately, control downloads, hide ads and more …

プロジェクト管理とサーバーレスとテスト

daiki.mori
September 06, 2017

 プロジェクト管理とサーバーレスとテスト

Backlogを使ったプロジェクト管理をする際、初期にすることは決まってます。Wikiやチケットなど。そのあたりを自動化したり、その自動化したコードをテストしたりというお話し。
※内容は2017/09/06時点のものです

daiki.mori

September 06, 2017
Tweet

More Decks by daiki.mori

Other Decks in Technology

Transcript

  1. Who am I ? ◦ 株式会社サーバーワークス 技術3課 グッド・ルッキング・エンジニア ◦ 元アプリケーションエンジニア

    ◦ 呼称:「本物の⼤樹」 →経理課の「元祖 ⼤樹」と対決中 ◦ AWS Lambda(Python) ◦ ⼤阪⽣まれ⼤阪育ちの後厄(お祓い済) • 森 ⼤樹 (2017年01⽉JOIN)
  2. AWS Step Functionsのおさらい 35 l先⽇、ヤマムギで話したAWS Step Functionsの話 l要点 l Lambda

    Functionをワークフローで定義 l なお、ワークフローは可視化される l シーケンシャル/分岐/並列処理できる l 実⾏結果も可視化
  3. backlogのWikiをコピーしたい 〜実装〜 lAPI Gateway 48 項⽬ 値 メソッド POST 統合タイプ

    Lambda関数 Lambdaプロキシ統合の使⽤ True Lambdaリージョン ap-northeast-1 Lambda関数 <<作成した関数名>>
  4. backlogのWikiをコピーしたい 〜実装〜 lLambda(前処理)/wikiページをbacklog GitからClone 51 class BacklogWikiCopyPreProcessing:↲ @staticmethod↲ def gitClone(backlog,

    s3):↲ github_repo = backlog['github_repo']↲ clone_dir = '/tmp/temp'↲ sshbucket = backlog['sshkey']['bucketname']↲ sshkeyfile = backlog['sshkey']['keyfile']↲ s3.download(sshbucket, sshkeyfile, '/tmp/id_rsa')↲ ↲ cmd = "chmod 600 /tmp/id_rsa"↲ output = subprocess.check_output(cmd.split(" "))↲ dulwich.client.get_ssh_vendor = git.KeyParamikoSSHVendor↲ repo = dulwich.porcelain.clone(github_repo, clone_dir)↲ class S3Access:↲ def download(self, bucketname, key, downloadPath):↲ self.s3.download_file(bucketname, key, downloadPath)↲
  5. backlogのWikiをコピーしたい 〜実装〜 lLambda(前処理)/wikiページのパス情報をリストに格納 52 # -*- coding: utf-8 -*-↲ import

    os↲ import fnmatch↲ ↲ class FileSearchWithExtension:↲ @staticmethod↲ def search(basepath, extension):↲ list = []↲ searchfile = "*." + extension↲ for dirpath, dirs, files in os.walk(basepath):↲ for name in files:↲ if fnmatch.fnmatch(name, searchfile):↲ list.append(os.path.join(dirpath, name))↲ return list↲
  6. backlogのWikiをコピーしたい 〜実装〜 lLambda(前処理)/wikiページをS3にアップロード 53 class BacklogWikiCopyPreProcessing:↲ @staticmethod↲ def uploadWikiPage(projectId, s3,

    bucketname, wikiList):↲ wikiInfoList = []↲ for wikiPage in wikiList:↲ wikiInfo = s3.upload(wikiPage, bucketname, projectId, '/tmp/temp/')↲ wikiInfoList.append(wikiInfo)↲ return wikiInfoList↲ class S3Access:↲ def upload(self, filepath, bucketname, leadkey, excludePath):↲ if excludePath is None:↲ filename = filepath↲ else:↲ filename = filepath.replace(excludePath)↲ if filename.startswith('/'):↲ filename = filename[1:]↲ if leadkey is None:↲ filename = filename↲ elif leadkey = '':↲ filename = filename↲ else:↲ filename = leadkey + '/' + filename↲ self.s3.upload_file(filepath, bucketname, filename)↲ s3fileInfo = { 'bucket': bucketname, 'filename': filename }↲ return s3fileInfo↲
  7. backlogのWikiをコピーしたい 〜実装〜 l Lambda(StepFunctions作成)/States/Pallarel/Branch句を作成 56 def main(event, context):↲ projectId =

    event['projectId']↲ wikiInfoList = event['wikiInfoList']↲ functionName = 'BacklogWikiCopyMain'↲ ↲ lambda = Lambda()↲ lambdaArn = lambda.getArn(functionName)↲ roleName = 'BacklogWikiSFRole'↲ iam = IAMResource()↲ roleArn = iam.getRoleArn(roleName)↲ ↲ name = "parallelExec"↲ definition = createDefinition(projectId, wikiInfoList, lambdaArn)↲ stateMachineArn = createStepFunctions(name, definition, roleArn)↲ return { "stateMachineArn": stateMachineArn, "projectId": projectId }↲ ↲
  8. backlogのWikiをコピーしたい 〜実装〜 l Lambda(StepFunctions作成)/States/Pallarel/Branch句を作成 57 def createDefinition(projectId, wikiInfoList, lambdaArn):↲ definition["Comment"]

    = projectId↲ definition["StartAt"] = projectId↲ definition["States"] = createStates(projectId, wikiInfoList, lambdaArn)↲ return definition↲ ↲ def createStates(projectId, wikiInfoList, lambdaArn):↲ statesData[projectId] = createParallel(projectId, wikiInfoList, lambdaArn)↲ return statesData↲ def createParallel(prijectId, wikiInfoList, lambdaArn):↲ i = 0↲ branches = []↲ for wikiInfo in wikiInfoList:↲ branch = createBranch(projectId, i, wikiInfo, lambdaArn)↲ branches.append(branch)↲ i += 1↲ parallelData["Type"] = "Parallel"↲ parallelData["Branches"] = branches↲ parallelData["End"] = true↲ return parallelData↲
  9. backlogのWikiをコピーしたい 〜実装〜 l Lambda(StepFunctions作成)/States/Pallarel/Branch句を作成 58 def createBranch(projectId, count, wikiInfo, lambdaArn):↲

    stateName = str(projectId) + str(count)↲ ↲ stateDetail["Type"] = "Task"↲ stateDetail["Resource"] = lambdaArn↲ dictWikiInfo = json.loads(wikiInfo)↲ dictWikiInfo["projectId"] = projectId↲ jsonWikiInfo = json.dumps(dictWikiInfo)↲ stateDetail["InputPath"] = jsonWikiInfo['filename']↲ stateDetail["End"] = true↲ ↲ stateData[stateName] = stateDetail↲ ↲ branchData["StartAt"] = stateName↲ branchData["States"] = stateData↲ ↲ return branchData↲
  10. backlogのWikiをコピーしたい 〜実装〜 l Lambda(StepFunctions作成)/States/Pallarel/Branch句を作成 59 { "Comment": "backlog Wiki Copy",

    "StartAt": "PROJECT_ID", "States": { "PROJECT_ID": { "Type": "Parallel", "Next": "Final State", "Branches": [ ], "End": true }, "Final State": { "Type": "Pass", "End": true } } } { "StartAt": "PROJECT_ID0", "States": { "PROJECT_ID0": { "Type": "Task", “Resource”: ”<<Lambda関数のARN>>", "InputPath": "PROJECT_ID/Home.md", "End": true } } },...... Wikiのページ数分存在する
  11. backlogのWikiをコピーしたい 〜実装〜 l Lambda(StepFunctions作成)/定義を作成し、Step Functionsを作成 60 def createStepFunctions(pName, pDefinition, pRoleArn):↲

    stateMachineArn = StepFunctions.createStateMachine(pName, pDefinition, pRoleArn)↲ return stateMachineArn↲ class StepFunctions:↲ @staticmethod↲ def createStateMachine(pName, pDefinition, pRoleArn):↲ sfn = boto3.client('stepfunctions')↲ response = sfn.create_state_machine(name=pName, definition=pDefinition, roleArn=pRoleArn)↲ stateMachineArn = response['stateMachineArn']↲ return stateMachineArn↲
  12. backlogのWikiをコピーしたい 〜実装〜 l Lambda(StepFunctions実⾏) 62 def createStepFunctions(pName, pDefinition, pRoleArn):↲ stateMachineArn

    = StepFunctions.createStateMachine(pName, pDefinition, pRoleArn)↲ return stateMachineArn↲ class StepFunctions:↲ @staticmethod↲ def createStateMachine(pName, pDefinition, pRoleArn):↲ sfn = boto3.client('stepfunctions')↲ response = sfn.create_state_machine(name=pName, definition=pDefinition, roleArn=pRoleArn)↲ stateMachineArn = response['stateMachineArn']↲ return stateMachineArn↲
  13. backlogのWikiをコピーしたい 〜実装〜 l Lambda(StepFunctions開始) 63 def main(event, context):↲ pStateMachineArn =

    event["stateMachineArn"]↲ pProjectId = event["projectId"]↲ ↲ response = StepFunctions.startExecution(pStateMachineArn, pProjectId)↲ ↲ return event↲ class StepFunctions:↲ @staticmethod↲ def startExcecution(pStateMachineArn, pProject):↲ sfn = boto3.client('stepfunctions')↲ response = sfn.start_execution(stateMachineArn=pStateMachineArn, name=pProject)↲ return response↲
  14. backlogのWikiをコピーしたい 〜実装〜 l Lambda(Wikiコピー)/ S3から対象のWikiページをDL 66 def downloadWikiPage(bucketname, filename):↲ downloadfile

    = "/tmp/wikifile/"↲ downloadpath = downloadfile + filename↲ dirpath = os.path.dirname(downloadpath)↲ if not os.path.exists(dirpath):↲ os.makedirs(dirpath)↲ s3 = S3Access()↲ s3.download(bucketname, filename, downloadpath)↲
  15. backlogのWikiをコピーしたい 〜実装〜 l Lambda(Wikiコピー)/ APIを呼び出すため、URL作成 67 def createUrlAndParam(bucketname, filename):↲ backlogurl

    = “https://<<スペース名>>.backlog.jp"↲ apiurl = "/api/v2/wikis"↲ #APIKeyは共通的な物をとる必要あり。⼀旦、森のを使う↲ apikeybase = "?apiKey="↲ apikey = ”<<backlogのAPIキー>>"↲ #↲ baseurl = backlogurl + apiurl + apikeybase + apikey↲ return base url
  16. backlogのWikiをコピーしたい 〜実装〜 l Lambda(Wikiコピー)/ APIを呼び出し 68 def main(event, content):↲ bucketname

    = event['bucket']↲ filename = event['filename']↲ downloadWikiPage(bucketname, filename) ↲ baseurl = createUrlAndParam(bucketname, filename) ↲ projectId = event["projectId"]↲ name = readFirstLine(downloadpath)↲ content = readFile(downloadpath)↲ httpheader = { "Content-Type":"application/x-www-form-urlencoded" }↲ ↲ param = {}↲ param["projectId"] = projectId↲ param["name"] = name↲ param["content"] = content↲ HttpPost.post(baseurl, param, httpheader) ↲
  17. backlogのWikiをコピーしたい 〜実装〜 lLambda(Step Functions削除/S3バケット削除) 70 def main(event, context):↲ bucketname =

    'backlogwikibase'↲ pStateMachineArn = event["stateMachineArn"]↲ pProjectId = event['projectId']↲ ↲ #S3のbucketのフォルダ(projectId)配下のファイルを削除↲ s3 = S3Access()↲ s3.deleteobjects(bucketname, pProjectId)↲ ↲ #step functionsを削除↲ response = StepFunctions.deleteStateMachine(pStateMachineArn)↲
  18. backlogのWikiをコピーしたい lLambda(Step Functions削除/S3バケット削除) 71 class StepFunctions:↲ @staticmethod↲ def deleteStateMachine(pStateMachineArn):↲ sfn

    = boto3.client('stepfunctions')↲ response = sfn.delete_state_machine(stateMachineArn=pStateMachineArn)↲ return response↲
  19. backlogのWikiをコピーしたい lLambda(Step Functions削除/S3バケット削除) 72 class S3Access:↲ def deleteobjects(self, bucketname, prefix=None):↲

    keys = self.listobjects(bucketname, prefix)↲ objs = []↲ for key in keys:↲ obj = {}↲ obj['Key'] = key↲ objs.append(obj)↲ deleteobj = {}↲ deleteobj['Objects'] = objs↲ deleteobj['Quiet'] = True↲ ↲ response = self.s3.delete_objects(Bucket=bucketname, Delete=deleteobj)↲
  20. backlogのWikiをコピーしたい lLambda(Step Functions削除/S3バケット削除) 73 class S3Access:↲ def listobjects(self, bucketname, prefix=None):↲

    if prefix is None:↲ response = self.s3.list_objects(Bucket=bucketname)↲ else:↲ response = self.s3.list_objects(Bucket=bucketname, Prefix=prefix)↲ #for 'Contents' in response: # 該当する key がないと response に 'Contents' が含まれない↲ # keys = [content['Key'] for content in response['Contents']]↲ keys = []↲ for contents in response['Contents']:↲ keys.append(contents['Key'])↲ return keys↲
  21. Lambda + Step Functionsでハマったとこ lパラメータの渡し⽅ lURLパラメータで渡した値の取得⽅法 l event[“queryStringParameters”][“projectId”] lStep Functionsのパラメータの渡し⽅

    l 「InputPath」を使う lLambdaから呼ぶ場合は、Boto3のAPIを利⽤ lsfn.start_execution(stateMachineArn='string', input=ʻstringʼ) lStep FunctionsのState Machine構造でInputPath 80
  22. Lambda + Step Functionsでハマったとこ l パラメータの渡し⽅ l URLパラメータで渡した値の取得⽅法 l event[“queryStringParameters”][“projectId”]

    l Step Functionsのパラメータの渡し⽅ l 「InputPath」を使う l Lambdaから呼ぶ場合は、Boto3のAPIを利⽤ l sfn.start_execution(stateMachineArn='string', input=ʻstringʼ) lStep FunctionsのState Machine構造でInputPath l Step FunctionsのState Machine 構造の作成 82
  23. テスト対象 〜 awspec 対象リソース 〜 acm / alb / alb_listener

    / alb_target_group / ami / autoscaling_group / cloudformation_stack / cloudfront_distribution / cloudtrail / cloudwatch_alarm / cloudwatch_event / cloudwatch_logs / customer_gateway / directconnect_virtual_interface / dynamodb_table / ebs / ec2 / ecr_repository / ecs_cluster / ecs_container_instance / ecs_service / ecs_task_definition / efs / eip / elasticache / elasticache_cache_parameter_group / elasticsearch / elastictranscoder_pipeline / elb / iam_group / iam_policy / iam_role / iam_user / internet_gateway / kms / lambda / launch_configuration / nat_gateway / network_acl / network_interface / rds / rds_db_cluster_parameter_group / rds_db_parameter_group / route53_hosted_zone / route_table / s3_bucket / security_group / ses_identity / sqs / subnet / vpc / vpn_connection / vpn_gateway / waf_web_acl / account 88
  24. テスト対象 〜 awspec 例 〜 89 properties['Resources']['EC2Instances'].each do |ec2| describe

    ec2(ec2['name']) do it { should exist } its(:ebs_optimized) { should eq ec2['ebs_optimized'] } its(:source_dest_check) { should eq ec2['source_dest_check'] } its(:key_name) { should eq ec2['key_name'] } its(:instance_type) { should eq ec2['instance_type'] } its(:image_id) { should eq ec2['image_id'] } it { should have_tag('Name').value(ec2['tags'][0]['Name']) } it { should have_tag('AUTO_START').value(ec2['tags'][1]['AUTO_START']) } 〜〜略〜〜 end 参考URL: http://blog.serverworks.co.jp/tech/2017/09/01/awspec20170901/ #「name」で指定されたEC2インスタンスが存在確認 # EC2インスタンスに設定されたキー確認 # インスタンスタイプ確認 # AMI-ID 確認 # Nameタグの確認
  25. テスト対象 〜 awspec 注意点 〜 全リソースが対象でない 対象リソースでも全ての項⽬が対象ではない IAM Userのアクセスキーとシークレットアクセスキーが必要 IAM

    Userに付与するIAMポリシーはReadOnlyAccess権限 EC2インスタンスを利⽤の場合は、ReadOnlyAccess権限を付与 作業が終わったら、IAM Userを削除するのがオススメ 90
  26. テスト対象 〜 moto 対象リソース 〜 API Gateway / Autoscaling /

    Cloudformation / CloudWatch / Data Pipeline / DynamoDB / EC2 (AMI / EBS / Instances / SecurityGroups / Tags) / ECS / ELB / EMR / Glacier / IAM / Lambda / Kinesis / KMS / RDS / Redshift / Route53 / S3 / SES / SNS / SQS / STS / SWF 93
  27. テスト対象 〜 moto 例 〜 94 @mock_s3↲ def test_s3_download(self):↲ #初期化↲

    bucketname = 'buckettest20170714'↲ prefix = 'project001'↲ downloadPath = '/tmp/test.md.tmp'↲ #テスト対象↲ s3 = S3Access()↲ #テスト準備↲ s3test = boto3.client('s3')↲ s3test.create_bucket(Bucket=bucketname)↲ s3test.put_object(Bucket=bucketname, Key=prefix)↲ s3test.upload_file('./test.md', bucketname, prefix + '/test.md')↲ #テスト実施↲ s3.download(bucketname, prefix + '/test.md', downloadPath)↲ #テスト結果確認↲ ##元ファイル読み込み↲ beforeStr = self.__readTestMd('./test.md')↲ ##ダウンロードファイル読み込み↲ afterStr = self.__readTestMd(downloadPath)↲ ##assert↲ assert beforeStr == afterStr↲ 初期化 バケット名、プレフィックス、 ローカルダウンロードパスを初期化 テストの前処理 テスト実施するための仮想状態を作る バケット名、プレフィックス、 ローカルダウンロードパスを初期化 テストの結果の確認 バケット名、プレフィックス、 ローカルダウンロードパスを初期化
  28. テスト対象 〜 unittest 〜 97 Python標準ライブラリのユニットテストフレームワーク 対象はpython 参考URL Python 3.6

    https://docs.python.jp/3/library/unittest.html Python 2.7 https://docs.python.jp/2.7/library/unittest.html
  29. テスト対象 〜 unittest 例 〜 99 @staticmethod↲ def uploadWikiPage(projectId, s3,

    bucketname, wikiList):↲ wikiInfoList = []↲ for wikiPage in wikiList:↲ wikiInfo = s3.upload(wikiPage, bucketname, projectId, '/tmp/temp/')↲ wikiInfoList.append(wikiInfo)↲ return wikiInfoList↲ テスト対象のメソッド
  30. テスト対象 〜 unittest 例 〜 10 @mock_s3↲ def testUploadWikiPage001(self):↲ try:↲

    #テスト準備↲ projectId = "testfolder"↲ bucketname = "buckettest20170714"↲ s3test = boto3.client('s3')↲ s3test.create_bucket(Bucket=bucketname)↲ filelist = []↲ filelist.append("./testfolder/testFile001.txt")↲ filelist.append("./testfolder/testFile002.txt")↲ filelist.append("./testfolder/testFile005.txt")↲ filelist.append("./testfolder/testfolder01/testfile010.txt")↲ s3 = S3Access()↲ #実⾏↲ wikiInfoList = BacklogWikiCopyPreProcessing.uploadWikiPage(projectId, s3, bucketname, filelist)↲ #結果確認↲ ##⽐較結果作成↲ for file in filelist:↲ responseData = { "bucket": bucketname, "filename": file.replace("./", "") }↲ assert responseData in wikiInfoList↲ except Exception as e:↲ print e↲