#mynavi(AWSメモ) #setlinebreak(on); #html(<style>.lh15,.lh15 * { line-height: 1.5; }</style>) * 目次 [#wef7852b] #contents - 関連 -- [[AWS CloudFormationメモ]] -- [[独自ドメイン名で API Gateway または EC2にアクセスする]] -- [[Lambda&API Gateway を CloudFormation でデプロイする]] - 参考 -- https://aws.amazon.com/jp/getting-started/projects/setup-jenkins-build-server/ -- https://aws.amazon.com/jp/blogs/devops/integrating-aws-codecommit-with-jenkins/ * 概要 [#w74ec372] #html(<div style="padding-left: 10px;" class="lh15">) EC2インスタンス上にJenkinsをインストールして、API Gateway & Lambda をテスト/デプロイする環境を構築する。 前提/環境は以下の通り。 - Jenkinsサーバは EC2( t2.micro ) を利用。 - Gitリポジトリは CodeCommit を利用。 - Git の認証は IAM で行う。 - masterブランチの内容をデプロイするジョブを作成する。 - masterブランチへの push は IAMで制限。(当記事では記載しない)&br; ※参考: https://aws.amazon.com/jp/blogs/devops/refining-access-to-branches-in-aws-codecommit/ ) - jenkinsサーバへのアクセスは https で行なうように ACM 及び ELB を設定。(当記事では記載しない)&br; ※関連: [[独自ドメイン名で API Gateway または EC2にアクセスする]] - API Gateway にはカスタムドメインを設定してURLが変わらないようにする。(当記事では記載しない)&br; ※参考: https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-domainname.html #html(</div>) * IAMユーザの作成 [#dc777e43] #html(<div style="padding-left: 10px;">) [[マネジメントコンソール>https://console.aws.amazon.com/iam/home?region=ap-northeast-1#/users]] から Jenkins がデプロイ等に使用するIAMユーザを作成する。 ※Jenkins にさせる処理をポリシーとしてアタッチしておく。( AWSCodeCommit系 とか CloudFormation系 とか ) #html(</div>) * CodeCommitリポジトリの作成 [#be04414e] #html(<div style="padding-left: 10px;">) [[マネジメントコンソール>https://ap-northeast-1.console.aws.amazon.com/codesuite/codecommit/repositories?region=ap-northeast-1]] からリポジトリを作成しておく。 ※ここでは SampleRepo とした。 ※master へのpushは IAMユーザ または グループで制限を掛けておく。 #html(</div>) * EC2インスタンスの作成 [#p40f0751] #html(<div style="padding-left: 10px;">) [[マネジメントコンソール>https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-1#Instances:sort=instanceId]] からJenkinsをインストールするEC2インスタンスを作成する。 ※セキュリティグループでポート 8080 を許可しておく事。 ※今回はELB経由で httpsアクセスさせる為、デフォルトからポートの変更等は行っていない。 #html(</div>) * EC2インスタンスの設定 [#bbce0ba9] #html(<div style="padding-left: 10px;">) EC2インスタンスに接続して以下の作業を行う。 ** ec2-user用の aws-cli の設定 [#s769b608] ** ローカル用の aws-cli の設定 [#s769b608] 確認用に ec2-user 用に aws-cli の設定をしておく。 ここでは、作成しておいたIAMユーザの AWS Access Key ID、AWS Secret Access Key、リージョン等を設定する。 ※Jenkins のインストールされているサーバにログインして何か作業を行う事がある場合のみ。 ※&color(red){''ただし、これはアンチパターン。EC2にIAMロールをアタッチする方がよりセキュア。''}; #myterm2(){{ aws configure AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX AWS Secret Access Key [None]: XXXXX.......XXXXXX Default region name [None]: ap-northeast-1 Default output format [None]: }} ** ec2-user 用の git の設定 [#oc27edcb] 確認用に ec2-user 用に git クライアントの設定を行っておく。 認証にはIAMを使用する為、以下の通り設定する。 ※これも Jenkins のインストールされているサーバにログインして何か作業を行う事がある場合のみ。 #myterm2(){{ git config --global credential.helper '!aws codecommit credential-helper $@' git config --global credential.useHttpPath true git config --global user.name "ユーザ名" git config --global user.email "メールアドレス" }} ** CodeCommit へのアクセス確認 [#s1298ac9] 先ほど作成した CodeCommit リポジトリにIAMでアクセスできるか確認する。 #myterm2(){{ git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/Xxxxxxx }} ※Jenkins からのアクセスは jenkinsユーザでのアクセスになる為、別途設定が必要。(後述) ** Jenkinsのインストール [#c31e9f98] 起動したEC2インスタンスで以下の作業を行う。 ※参考: https://d1.awsstatic.com/Projects/P5505030/aws-project_Jenkins-build-server.pdf ** JDK8 のインストール [#f664e401] #myterm2(){{ sudo yum install -y java-1.8.0-openjdk-devel }} ** 使用するJavaバージョンの切替 [#r78e8b85] #myterm2(){{ sudo alternatives --config java 2 プログラムがあり 'java' を提供します。 選択 コマンド ----------------------------------------------- *+ 1 /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java 2 /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2 }} バージョンが切り替わったか確認 #myterm2(){{ java -version openjdk version "1.8.0_201" OpenJDK Runtime Environment (build 1.8.0_201-b09) OpenJDK 64-Bit Server VM (build 25.201-b09, mixed mode) }} ** Gitのインストール [#w1c5446a] #myterm2(){{ sudo yum install git -y }} ** Jenkinsのインストール [#n4605e1f] #myterm2(){{ sudo yum update -y sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo sudo rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key sudo yum install jenkins -y }} ** Jenkins ユーザ用の git 設定 [#f9310d5f] jenkins からCodeCommit にアクセスする際に IAMロールでアクセスするように設定する。 jenkins ユーザのホームディレクトリは /var/lib/jenkins になっているので、その配下に .gitconfig を以下の通り作成する。 #myterm2(){{ sudo touch /var/lib/jenkins/.gitconfig sudo chmod 664 /var/lib/jenkins/.gitconfig sudo chown jenkins:jenkins /var/lib/jenkins/.gitconfig sudo vim /var/lib/jenkins/.gitconfig [user] name = ユーザ名 email = メールアドレス [credential] helper = !aws codecommit credential-helper $@ useHttpPath = true }} ** Jenkinsユーザ用の aws-cliの設定 [#p686d994] 作成しておいたIAMユーザの AWS Access Key ID、AWS Secret Access Key、リージョン等を設定する。 ※&color(red){''これはアンチパターン。EC2にIAMロールをアタッチする方がよりセキュア。''}; #myterm2(){{ sudo -u jenkins aws configure AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX AWS Secret Access Key [None]: XXXXX.......XXXXXX Default region name [None]: ap-northeast-1 Default output format [None]: }} ** Jenkinsのサービス開始 [#gdbfc49e] #myterm2(){{ sudo service jenkins start }} ** Jenkinsインストール用の管理者パスワードを確認 [#lc81743b] #myterm2(){{ sudo cat /var/lib/jenkins/secrets/initialAdminPassword }} ※後でJenkinsでジョブを作成する時に使用する。( [[Jenkinsの設定>#c6e4a45a]]) #html(</div>) * 処理の作成 [#dffd36c5] #html(<div style="padding-left: 10px;">) ここからはローカル環境で処理をデプロイする処理(API Gateway 及び Lambda)を作成していく。 ali-cli や git の設定は [[上記>#s769b608]] と同じ感じで設定しておく事。 ** 準備 [#c8596510] #html(<div style="padding-left: 10px;">) *** git チェックアウト [#v0d4c0f8] #html(<div style="padding-left: 10px;">) #myterm2(){{ mkdir ~/workspace git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/SampleRepo # 作成しておいたCodeCommitリポジトリのURL cd SampleRepo git checkout develop }} #html(</div>) #html(</div>) ** 処理の作成 [#lb9987cf] #html(<div style="padding-left: 10px;">) *** book.py [#od3ba411] #html(<div style="padding-left: 10px;">) #mycode2(){{ # coding: utf-8 """ サンプルLambda. """ import json # 本の一覧 books = [{'isbn': f'sample{i+1:03d}', 'title': f'BOOK{i+1:03d}'} for i in range(5)] def handler(event, context): """本の情報を取得する.""" response = { 'statusCode': 200, 'headers': { 'Content-Type': 'application/json' }, 'body': json.dumps({'list': books, 'count': len(books)}) } return response }} #html(</div>) *** template.yml [#w4800203] #html(<div style="padding-left: 10px;">) #mycode2(){{ AWSTemplateFormatVersion : '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Test serverless application. Resources: SampleBookApi: Type: AWS::Serverless::Function Properties: FunctionName: SampleBookFunction Description: "Sample" Handler: book.handler Runtime: python3.6 Events: BookList: Type: Api Properties: Path: /book Method: get BookGet: Type: Api Properties: Path: /book/{id} Method: get Outputs: SampleBookApiUri: # 作成したAPIのURIをエクスポートしておく # ※AWS::Serverless::Function を使用した場合、ステージは Stage,Prod の2つが作成される Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" Export: Name: SampleBookApiUri }} ※実際の運用ではカスタムドメインを使用してベースパスマッピングを設定する事になるが、ここでは割愛する。(関連: [[CloudFormationでカスタムドメイン対応の API Gateway を作成する]]) #html(</div>) #html(</div>) #html(</div>) * ローカルで動作確認 [#b41a391f] #html(<div style="padding-left: 10px;">) 作成した処理をSAMローカルで動作確認する。 ** sam-cli インストール [#d5d1185f] #html(<div style="padding-left: 10px;">) #myterm2(){{ sudo yum -y install gcc sudo yum install python-devel pip install --upgrade pip pip install --user aws-sam-cli }} #html(</div>) ** python3.6インストール [#v5868c0b] #html(<div style="padding-left: 10px;">) #myterm2(){{ yum list available | grep "python36" sudo yum install python36 }} #html(</div>) ** Dockerインストール&起動 [#wecaef91] #html(<div style="padding-left: 10px;">) Dockerをインストールして起動しておく。 linuxの場合は自分をdockerグループに追加しておく。( /var/run/docker.sock にアクセスできる状態にしておかないと docker コマンドが使用できない ) #myterm2(){{ sudo yum install -y docker sudo service docker start sudo usermod -a -G docker ユーザ名 }} #html(</div>) ** その他のツールをインストール [#fe5299d8] #html(<div style="padding-left: 10px;">) jq など使用しそうなツールをインストール #myterm2(){{ sudo yum -y install jq # sudo pip install yq }} #html(</div>) ** SAMローカルを起動 [#i7e5d422] #html(<div style="padding-left: 10px;">) いったんターミナルからログアウト。(環境変数を読み込み直したいだけ) #myterm2(){{ exit }} 再度ログインして SAM Local起動 #myterm2(){{ cd ~/workspace/SampleRepo sam local start-api --template template.yml 2019-01-01 08:15:11 Found credentials in shared credentials file: ~/.aws/credentials 2019-01-01 08:15:11 Mounting BookFunction at http://127.0.0.1:3000/book [GET] 2019-01-01 08:15:11 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template 2019-01-01 08:15:11 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit) }} #html(</div>) 別のターミナルを起動してリクエストを投げてみる #myterm2(){{ curl http://127.0.0.1:3000/book | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 237 100 237 0 0 236 0 0:00:01 0:00:01 --:--:-- 236 { "list": [ { "isbn": "sample001", "title": "BOOK001" }, { "isbn": "sample002", "title": "BOOK002" }, { "isbn": "sample003", "title": "BOOK003" }, { "isbn": "sample004", "title": "BOOK004" }, { "isbn": "sample005", "title": "BOOK005" } ], "count": 5 } }} #html(</div>) #html(</div>) * デプロイ用のシェルを作成する [#if4dd452] #html(<div style="padding-left: 10px;">) デプロイ処理は Jenkins からそのまま使用できるようにシェル化する。 ** シェルの作成 [#h3e9df56] #html(<div style="padding-left: 10px;">) Jenkins から利用する為のデプロイ用シェルを作成する。 build.sh #mycode2(){{ #!/bin/bash echo "" MODE=$1 # スタック名 ( ここではgitリポジトリ名を取得して末尾の Repo を Stack に変えるようにした ) STACK_NAME=`cat .git/config | grep url | head -1 | awk -F"/" '{print $NF}' | sed 's/Repo$/Stack/'` # アカウントIDの取得 ACCOUNT_ID=`aws sts get-caller-identity | grep Account | awk '{print $2}' | sed -e "s/[^0-9]//g"` echo "STACK_NAME: ${STACK_NAME}" # スタック作成時のイベント確認 if [ "${MODE}" == "desc" ]; then aws cloudformation describe-stack-events --stack-name $STACK_NAME exit 0 fi # 削除 if [ "${MODE}" == "del" ]; then aws cloudformation delete-stack --stack-name $STACK_NAME aws cloudformation wait stack-delete-complete --stack-name $STACK_NAME exit 0 fi # S3バケットがない場合は作る(バケット名は世界で唯一である必要がある為、末尾にアカウントID等を付与しておく) BUCKET_NAME=my-cloudformation-templates-${ACCOUNT_ID} BUCKET_COUNT=`aws s3api list-buckets | grep -e "\"${BUCKET_NAME}\"" | wc -l` if [ "${BUCKET_COUNT}" == "0" ]; then echo aws s3api create-bucket --create-bucket-configuration '{"LocationConstraint": "ap-northeast-1"}' --bucket $BUCKET_NAME fi # 検証&パッケージング&デプロイ (成功時は作成したAPIのURIを表示する) aws cloudformation validate-template --template-body file://template.yml \ && aws cloudformation package --template-file template.yml --s3-bucket $BUCKET_NAME --output-template-file packaged-template.yml \ && aws cloudformation deploy --template-file packaged-template.yml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM \ && echo "" \ && echo "### Exported Value ###" \ && aws cloudformation describe-stacks --stack-name $STACK_NAME | awk 'BEGIN{key=""}{ if ($1 == "\"OutputKey\":") key=$2; if ($1 == "\"OutputValue\":") print key" : "$2 }' | sed 's/[",]//g' \ && echo "######################/" \ && echo "" }} #html(</div>) ** テストデプロイ [#y810683f] #html(<div style="padding-left: 10px;">) 作成したシェルでデプロイできるか確認。 #myterm2(){{ ./build.sh { "Description": "Test serverless application.", "Parameters": [] } Uploading to f799....c72 28043 / 28043.0 (100.00%) Successfully packaged artifacts and wrote output template to file packaged-template.yml. Execute the following command to deploy the packaged template aws cloudformation deploy --template-file /path_to/SampleRepo/packaged-template.yml --stack-name <YOUR STACK NAME> Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - SampleStack ### Exported Value ### SampleBookApiUri : https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod/ ######################/ }} //良さげなので、アンデプロイしておく。 //#myterm2(){{ //./build.sh del //}} デプロイされたAPIの動作確認 #myterm2(){{ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/book | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 237 100 237 0 0 561 0 --:--:-- --:--:-- --:--:-- 561 { "list": [ { "isbn": "sample001", "title": "BOOK001" }, { "isbn": "sample002", "title": "BOOK002" }, { "isbn": "sample003", "title": "BOOK003" }, { "isbn": "sample004", "title": "BOOK004" }, { "isbn": "sample005", "title": "BOOK005" } ], "count": 5 } }} #html(</div>) #html(</div>) * git リポジトリにプッシュ [#s974f195] #html(<div style="padding-left: 10px;">) ** git commit & push [#ae6e2eca] #html(<div style="padding-left: 10px;">) #myterm2(){{ git add . git commit -m 'first commit' git push }} #html(</div>) ** masterにマージ [#pab838ad] #html(<div style="padding-left: 10px;">) #myterm2(){{ git checkout master git merge develop git push }} #html(</div>) #html(</div>) * Jenkinsの設定 [#c6e4a45a] #html(<div style="padding-left: 10px;">) ** Jenkinsにログイン [#g017c319] #html(<div style="padding-left: 10px;">) 対象のEC2インスタンスの 8080ポート(※)にアクセスし、Administrator password に、先程確認した管理者パスワードを入力してログイン後、 [Install suggested plugins] で適当なプラグインをインストールしておく。 ※ http://EC2のパブリックDNS:8080 #html(</div>) ** ジョブの作成 [#gd3abfe0] #html(<div style="padding-left: 10px;">) 以下の通り作成した。 ※ポイントは Jenkins 側でシェルを書くのではなく、リポジトリに含まれるシェルを利用する事。 | 項目 | 値 | 補足 |h |ジョブ名 | SampleStack | ここでは CloudFormation のスタック名(というかCodeCommitのリポジトリ名)と合わせた。 | | モード | 「フリースタイル・プロジェクトのビルド」 | | | 説明 | サンプルジョブ | | | ソースコード管理 | Git | | | リポジトリURL | CodeCommitのリポジトリURL | | | 認証 | なし | IAMで認証する | | ビルドするブランチ | */master | | | リポジトリ・ブラウザ | (自動) | | | ビルド・トリガ | SCMをポーリング ( H/5 * * * * ) | 5分間隔 | | ビルド |「シェルの実行」| | | シェルスクリプト | ./build.sh | | #html(</div>) #html(</div>) * デプロイ確認 [#pacbfc2f] #html(<div style="padding-left: 10px;">) 環境が整ったので、処理を変更してデプロイしてみる。 上記で作成した処理は常に一覧取得を行っていたが、 https://xxxxxxxxxx/book/{id} でアクセスした場合は一意検索を行うように改修して master ブランチまで持って行く事にする。 ** 準備 [#df4db12f] #html(<div style="padding-left: 10px;">) developブランチの最新を取得 #myterm2(){{ git checkout develop git pull }} #html(</div>) ** 処理の変更 [#xb6982a7] #html(<div style="padding-left: 10px;">) book.py #mycode(){{ # coding: utf-8 """ サンプルLambda. """ import json # 本の一覧 books = [{'isbn': f'sample{i+1:03d}', 'title': f'BOOK{i+1:03d}'} for i in range(5)] def handler(event, context): """メインハンドラ""" if event.get('pathParameters') and event['pathParameters'].get('id'): result = get_book(event['pathParameters']['id']) # 一意検索 else: result = get_books() # 一覧取得 status = 200 if not result: status = 404 result = {'message': 'Data not found.'} return { 'statusCode': status, 'headers': { 'Content-Type': 'application/json' }, 'body': json.dumps(result) } def get_books(): """本の一覧を取得する.""" return books def get_book(id): """指定された本の情報を取得する.""" matches = [x for x in books if x['isbn'] == id] return matches[0] if matches else None }} #html(</div>) ** CodeCommitにプッシュ [#f4a72ce9] #html(<div style="padding-left: 10px;">) developにpush #myterm2(){{ git add. git commit -m 'fix: get book api' git push }} masterにマージ #myterm2(){{ git checkout master git pull git merge develop git push }} #html(</div>) ** デプロイされたか確認 [#zd88b5ae] しばらくするとJenkinsのジョブが実行され、以下のようなログが出力されている事が確認できた。 #myterm2(){{ 20:30:05 Started by an SCM change 20:30:05 Running as SYSTEM 20:30:05 Building in workspace /var/lib/jenkins/workspace/SampleStack 20:30:05 No credentials specified 20:30:05 Cloning the remote Git repository . . . 20:30:50 Successfully created/updated stack - SampleStack 20:30:50 20:30:50 ### Exported Value ### 20:30:50 SampleBookApiUri : https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/ 20:30:50 ######################/ 20:30:50 20:30:50 Finished: SUCCESS }} 表示されているAPIにアクセスして正しくデプロイされているか確認してみる。 #myterm2(){{ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/book/sample001 | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 41 100 41 0 0 106 0 --:--:-- --:--:-- --:--:-- 106 { "isbn": "sample001", "title": "BOOK001" } }} #html(</div>) #html(</div>)