S3バケットにJSONファイルをアップロードするCI・CDを作ってみた
スマホアプリで全ユーザに「お知らせ」を表示しようと思い、JSONデータを公開する仕組みを作ってみました。
API(API Gateway + Lambda)を作っても良かったのですが、簡単に済ませるため、S3バケットにJSONファイルを格納して公開することで簡略化しました。 (「クラウド側の仕組みとCI/CDの仕組み」を作ってみたかった)
目次
概要
GitHubリポジトリのJSONファイルを更新すると、CircleCIによってS3バケットのJSONファイルが更新されます。簡略化のためCloudFrontは未使用です。
JSONファイルの仕様
次のJSONファイルをS3バケットに格納します。なお、「お知らせ」の更新頻度は月数回を想定しているため、頻繁なJSONデータ取得はさせない予定です。(この仕組はクライアント側ですが、Periodパラメータを利用する想定)
1 2 3 4 5 | { "Message": "お知らせ:xxxxx", "DeadlineTimestamp": 1578636000, "Period": 86400} |
- Message
- 表示するお知らせの本文
- DeadlineTimestamp
- お知らせの有効期限(Unixtime)
- これ以降はお知らせを表示しない
- Period
- データ再取得の期間(Unixtime)
- 前回取得してからこの期間が経過すると、再取得する
リポジトリとブランチ運用
masterブランチにPushされたら、CircleCIによって開発環境にデプロイします。そのあと、CircleCIのApprove機能で本番環境にデプロイします。
| 種類 | 名前の例 | 環境 |
|---|---|---|
| ブランチ | master | 開発環境 → 本番環境 |
CI/CDを構築する
下記を作成していきます。
- Python仮想環境を作成
- Makefileを作成
- S3バケットを作成
- IAMユーザとIAMロールを作成(CircleCI用)
- IAMロールのARNを取得
- IAMユーザのアクセスキーを取得
- 単体テスト用のファイルを作成
- AssumeRole用のスクリプトを作成
- CircleCIの設定ファイルを作成
- CircleCIの設定
なお、IAMユーザやIAMロールの詳細は下記をご覧ください。
Python仮想環境を作成
次のコマンドでPythonの仮想環境を作成します。
$ pipenv install --python 3.7.2 |
続いて、次のコマンドで必要なライブラリをPython仮想環境に導入します。
$ pipenv install awscli$ pipenv install pytest |
Makefileを作成
次のMakefileを作成します。
| BASE_STACK_NAME := App-Information | |
| prepare: | |
| aws cloudformation deploy \ | |
| --template-file prepare.yaml \ | |
| --stack-name $(BASE_STACK_NAME)-Prepare-${ENV} \ | |
| --capabilities CAPABILITY_NAMED_IAM \ | |
| --parameter-overrides Env=${ENV} | |
| describe-prepare: | |
| aws cloudformation describe-stacks \ | |
| --stack-name $(BASE_STACK_NAME)-Prepare-${ENV} \ | |
| --query 'Stacks[].Outputs' | |
| test-json: | |
| python -m pytest test/ | |
| create-access-key: | |
| aws iam create-access-key \ | |
| --user-name app-information-deploy-user-${ENV} | |
| deploy: | |
| aws s3api put-object \ | |
| --bucket app-information-${ENV} \ | |
| --key information.json \ | |
| --body information.json \ | |
| --content-type application/json \ | |
| --acl public-read |
S3バケット & IAMユーザ & IAMロールを作成
次のCloudFormationテンプレートをprepare.yamlとして作成します。sts:ExternalIdには任意の値を設定します。これはCircelCIの環境変数に設定します。
| AWSTemplateFormatVersion: "2010-09-09" | |
| Description: App Information Prepare | |
| Parameters: | |
| Env: | |
| Type: String | |
| AllowedValues: | |
| - prod | |
| - dev | |
| Resources: | |
| # S3バケット | |
| InformationBucket: | |
| Type: AWS::S3::Bucket | |
| DeletionPolicy: Retain | |
| Properties: | |
| AccessControl: PublicRead | |
| BucketName: !Sub app-information-${Env} | |
| # デプロイ用のIAMユーザ | |
| DeployUser: | |
| Type: AWS::IAM::User | |
| Properties: | |
| UserName: !Sub app-information-deploy-user-${Env} | |
| # デプロイ用のIAMユーザに付与するIAMポリシー(AssumeRoleできる) | |
| DeployUserPolicy: | |
| Type: AWS::IAM::Policy | |
| Properties: | |
| PolicyName: !Sub app-information-deploy-policy-${Env} | |
| PolicyDocument: | |
| Version: "2012-10-17" | |
| Statement: | |
| - Effect: Allow | |
| Action: sts:AssumeRole | |
| Resource: !GetAtt DeployRoleForUser.Arn | |
| Users: | |
| - !Ref DeployUser | |
| # デプロイ用のIAMユーザがAssumeRoleするIAMロール(S3に対するWrite権限) | |
| DeployRoleForUser: | |
| Type: AWS::IAM::Role | |
| Properties: | |
| RoleName: !Sub app-information-deploy-role-for-user-${Env} | |
| AssumeRolePolicyDocument: | |
| Version: "2012-10-17" | |
| Statement: | |
| - Effect: Allow | |
| Action: sts:AssumeRole | |
| Principal: | |
| AWS: | |
| - !GetAtt DeployUser.Arn | |
| Condition: | |
| StringEquals: | |
| sts:ExternalId: any-id-hoge-fuga | |
| Policies: | |
| - PolicyName: !Sub app-information-deploy-policy-for-user-${Env} | |
| PolicyDocument: | |
| Version: "2012-10-17" | |
| Statement: | |
| - Effect: Allow | |
| Action: | |
| - s3:PutObject | |
| - s3:PutObjectAcl | |
| Resource: | |
| - !Sub ${InformationBucket.Arn}/information.json | |
| MaxSessionDuration: 3600 |
続いてデプロイします。
$ ENV=dev make prepare$ ENV=prod make prepare |
IAMロールのARNを取得
次のコマンドを実行し、作成したIAMロールのARNを取得します。これはAssumeRoleするために必要なのでメモしておき、CircelCIの環境変数に設定します。
$ ENV=dev make describe-prepare$ ENV=prod make describe-prepare |
IAMユーザのアクセスキーを取得
IAMユーザのアクセスキーを取得します。これはCircleCIの環境変数に設定するためメモしておきます。
$ ENV=dev make create-access-key$ ENV=prod make create-access-key |
単体テスト用のファイルを作成
次のPythonコードをtest_checker.pyとして作成します。
| import pytest | |
| import json | |
| class TestInformationJson(object): | |
| def get_json_data(self): | |
| with open('information.json') as f: | |
| return json.load(f) | |
| def test_exist_key(self): | |
| data = self.get_json_data() | |
| assert 'Message' in data | |
| assert 'DeadlineTimestamp' in data | |
| assert 'Period' in data | |
| def test_value_type(self): | |
| data = self.get_json_data() | |
| assert type(data['Message']) is str | |
| assert type(data['DeadlineTimestamp']) is int | |
| assert type(data['Period']) is int |
確認する内容は下記です。
- JSONファイルとして正しいか?
- 必要なKeyがあるか?
- Valueの型が期待通りか?
AssumeRole用のスクリプトを作成
AssumeRole用に次のスクリプトをassume_role.shとして作成します。
| #!/usr/bin/env bash | |
| set -xeuo pipefail | |
| aws_sts_credentials="$(aws sts assume-role \ | |
| --role-arn "$AWS_DEPLOY_IAM_ROLE_ARN" \ | |
| --role-session-name "$ROLE_SESSION_NAME" \ | |
| --external-id "$AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID" \ | |
| --duration-seconds 900 \ | |
| --query "Credentials" \ | |
| --output "json")" | |
| cat <<EOT > "aws-env.sh" | |
| export AWS_ACCESS_KEY_ID="$(echo $aws_sts_credentials | jq -r '.AccessKeyId')" | |
| export AWS_SECRET_ACCESS_KEY="$(echo $aws_sts_credentials | jq -r '.SecretAccessKey')" | |
| export AWS_SESSION_TOKEN="$(echo $aws_sts_credentials | jq -r '.SessionToken')" | |
| EOT |
次に実行権限を付与しておきます。
$ chmod 755 assume_role.sh |
JSONファイルを作成
次のJSONファイルをinformation.jsonとして作成します。
| { | |
| "Message": "テストメッセージ", | |
| "DeadlineTimestamp": 1578636000, | |
| "Period": 86400 | |
| } |
CircleCIの設定ファイルを作成
.circleciディレクトリを作成し、その中にconfig.ymlを作成します。
$ mkdir .circleci$ touch .circleci/config.yml |
続いて、config.ymlファイルの中身を記述します。
| version: 2.1 | |
| executors: | |
| my-executor: | |
| docker: | |
| - image: circleci/python:3.7.2 | |
| environment: | |
| PIPENV_VENV_IN_PROJECT: true | |
| working_directory: ~/work | |
| commands: | |
| restore: | |
| steps: | |
| - restore_cache: | |
| key: work-v1-{{ .Branch }}-{{ checksum "Pipfile.lock" }} | |
| save: | |
| steps: | |
| - save_cache: | |
| paths: | |
| - ".venv" | |
| key: work-v1-{{ .Branch }}-{{ checksum "Pipfile.lock" }} | |
| deploy: | |
| parameters: | |
| env: | |
| type: enum | |
| enum: ["prod", "dev"] | |
| steps: | |
| - checkout | |
| - restore | |
| - run: | |
| name: deploy | |
| command: | | |
| source .venv/bin/activate | |
| aws --version | |
| echo << parameters.env >> | |
| if [ << parameters.env >> = "dev" ]; then | |
| export ENV=<< parameters.env >> | |
| export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID_DEV | |
| export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_DEV | |
| export AWS_DEPLOY_IAM_ROLE_ARN=$AWS_DEPLOY_IAM_ROLE_ARN_DEV | |
| export AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID=$AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID_DEV | |
| else | |
| export ENV=<< parameters.env >> | |
| export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID_PROD | |
| export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_PROD | |
| export AWS_DEPLOY_IAM_ROLE_ARN=$AWS_DEPLOY_IAM_ROLE_ARN_PROD | |
| export AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID=$AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID_PROD | |
| fi | |
| export ROLE_SESSION_NAME=deploy-$ENV | |
| ./assume_role.sh | |
| source aws-env.sh | |
| make deploy | |
| jobs: | |
| setup: | |
| executor: my-executor | |
| steps: | |
| - checkout | |
| - restore | |
| - run: | |
| name: install | |
| command: | | |
| sudo pip install pipenv | |
| pipenv install | |
| - save | |
| test: | |
| executor: my-executor | |
| steps: | |
| - checkout | |
| - restore | |
| - run: | |
| name: test | |
| command: | | |
| source .venv/bin/activate | |
| make test-json | |
| deploy_dev: | |
| executor: my-executor | |
| steps: | |
| - checkout | |
| - restore | |
| - deploy: | |
| env: dev | |
| deploy_prod: | |
| executor: my-executor | |
| steps: | |
| - checkout | |
| - restore | |
| - deploy: | |
| env: prod | |
| workflows: | |
| version: 2.1 | |
| release-workflow: | |
| jobs: | |
| - setup: | |
| filters: | |
| branches: | |
| only: | |
| - master | |
| - test: | |
| requires: | |
| - setup | |
| filters: | |
| branches: | |
| only: | |
| - master | |
| - deploy_dev: | |
| requires: | |
| - test | |
| filters: | |
| branches: | |
| only: | |
| - master | |
| - approve_for_prod: | |
| type: approval | |
| requires: | |
| - deploy_dev | |
| filters: | |
| branches: | |
| only: | |
| - master | |
| - deploy_prod: | |
| requires: | |
| - approve_for_prod | |
| filters: | |
| branches: | |
| only: | |
| - master |
CircleCIの設定
リポジトリのPush
まずはGitHubにリポジトリをPushしておきます。
$ git push origin master |
CircleCIにログイン
CircleCIにログインします。
プロジェクト作成
「ADD PROJECT」を選択し、さきほどGitHubにPushしたリポジトリを選択します。
続いて、「Start building」を選択します。
初めてのジョブが走りますが、環境変数が未設定なので失敗します。
環境変数を設定
プロジェクト一覧の設定マークを押し、設定画面に移ります。
Environment Variablesを選択します。
次の環境変数を追加します。
| Name | Value |
|---|---|
| AWS_ACCESS_KEY_ID_DEV | 取得したAccessKeyId(開発用) |
| AWS_ACCESS_KEY_ID_PROD | 取得したAccessKeyId(本番用) |
| AWS_SECRET_ACCESS_KEY_DEV | 取得したSecretAccessKey(開発用) |
| AWS_SECRET_ACCESS_KEY_PROD | 取得したSecretAccessKey(本番用) |
| AWS_DEPLOY_IAM_ROLE_ARN_DEV | 取得したIAMロールのARN(開発用) |
| AWS_DEPLOY_IAM_ROLE_ARN_PROD | 取得したIAMロールのARN(本番用) |
| AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID_DEV | sts:ExternalIdで設定した値(開発用) |
| AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID_PROD | sts:ExternalIdで設定した値(本番用) |
| AWS_DEFAULT_REGION | ap-northeast-1 |
| AWS_DEFAULT_OUTPUT | json |
動作確認
開発環境
さきほど失敗したWorkflowsの「Rerun」を選択し、そこの「Rerun from failed」を選択します。
しばらくするとデプロイが成功しました!
次のコマンドでJSON取得できます。
$ curl app-information-dev.s3.amazonaws.com/information.json{ "Message": "テストメッセージ", "DeadlineTimestamp": 1578636000, "Period": 86400} |
本番環境
この状態で「Approve Job」のApproveを選択し、続いて本番環境にデプロイを進めます。
しばらく待つと、デプロイが成功しました!
次のコマンドでJSON取得できます。
$ curl app-information-prod.s3.amazonaws.com/information.json{ "Message": "テストメッセージ", "DeadlineTimestamp": 1578636000, "Period": 86400} |
さいごに
思っていたよりも簡単にできました。API化は必要になったら考えたいです。