S3バケットにJSONファイルをアップロードするCI・CDを作ってみた

スマホアプリで全ユーザに「お知らせ」を表示しようと思い、JSONデータを公開する仕組みを作ってみました。

API(API Gateway + Lambda)を作っても良かったのですが、簡単に済ませるため、S3バケットにJSONファイルを格納して公開することで簡略化しました。 (「クラウド側の仕組みとCI/CDの仕組み」を作ってみたかった)

目次

概要

GitHubリポジトリのJSONファイルを更新すると、CircleCIによってS3バケットのJSONファイルが更新されます。簡略化のためCloudFrontは未使用です。

CI/CDの概要図

JSONファイルの仕様

次のJSONファイルをS3バケットに格納します。なお、「お知らせ」の更新頻度は月数回を想定しているため、頻繁なJSONデータ取得はさせない予定です。(この仕組はクライアント側ですが、Periodパラメータを利用する想定)

information.json
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
view raw Makefile hosted with ❤ by GitHub

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
view raw prepare.yaml hosted with ❤ by GitHub

続いてデプロイします。

$ 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
view raw test_checker.py hosted with ❤ by GitHub

確認する内容は下記です。

  • 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
view raw assume_role.sh hosted with ❤ by GitHub

次に実行権限を付与しておきます。

$ chmod 755 assume_role.sh

JSONファイルを作成

次のJSONファイルをinformation.jsonとして作成します。

{
"Message": "テストメッセージ",
"DeadlineTimestamp": 1578636000,
"Period": 86400
}
view raw information.json hosted with ❤ by GitHub

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
view raw config.yml hosted with ❤ by GitHub

CircleCIの設定

リポジトリのPush

まずはGitHubにリポジトリをPushしておきます。

$ git push origin master

CircleCIにログイン

CircleCIにログインします。

プロジェクト作成

「ADD PROJECT」を選択し、さきほどGitHubにPushしたリポジトリを選択します。

Add Projectをする

続いて、「Start building」を選択します。

Start buildingを選択する

初めてのジョブが走りますが、環境変数が未設定なので失敗します。

環境変数が未設定なので失敗する

環境変数を設定

プロジェクト一覧の設定マークを押し、設定画面に移ります。

設定画面に移動する

Environment Variablesを選択します。

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」を選択します。

Rerun from failedを選択する

しばらくするとデプロイが成功しました!

デプロイが成功する

次のコマンドでJSON取得できます。

$ curl app-information-dev.s3.amazonaws.com/information.json
{
    "Message": "テストメッセージ",
    "DeadlineTimestamp": 1578636000,
    "Period": 86400
}

本番環境

この状態で「Approve Job」のApproveを選択し、続いて本番環境にデプロイを進めます。

Approveを選択する

Approveする

しばらく待つと、デプロイが成功しました!

本番環境へのデプロイが成功する

次のコマンドでJSON取得できます。

$ curl app-information-prod.s3.amazonaws.com/information.json
{
    "Message": "テストメッセージ",
    "DeadlineTimestamp": 1578636000,
    "Period": 86400
}

さいごに

思っていたよりも簡単にできました。API化は必要になったら考えたいです。

参考