CircleCIでIP制限のあるEC2インスタンスに自動デプロイできるようにする

はじめに

PythonのWebアプリケーションフレームワークFlaskを使って、「消耗品買い物リスト」を作ってみるシリーズ第7回目です。
今回はEC2へのデプロイ自動化の設定を行います。

前回まで

今回やること

前回までで、CircleCIを使ってのテスト自動化ができるようになったところで、デプロイ自動化の設定をしていきます。

実現したいこと

  • テスト自動化 ← 前回の記事はここまで
    • 変更がPushされたらテストが自動で走る
  • デプロイ自動化 ← 今回の記事はここまで
    • masterに変更がPush(基本的にはブランチのマージ)されたらテストが自動で走り、テストが通ると自動で本番環境にデプロイする

EC2へのデプロイですが、やっていることはEC2インスタンスにSSH接続して、masterブランチをPullする、というシンプルな動きになります。
公式ドキュメントのSSHの項を参考にします。

流れ

  • SSHキーを追加
  • EC2インスタンスの接続ユーザー情報を環境変数に設定
  • IPを開ける設定
    • (AWS)デプロイ用IAMユーザーを作成
    • (CircleCI)IAMユーザーの接続に情報をCircleCIの環境変数に設定
  • config.ymlを編集
  • 動作確認

SSHキーを追加

  • CircleCIダッシュボードを開く
  • Projectsから対象リポジトリを選択
  • Pipelinesの画面右上Project Settings
  • 左側のメニューSSH Keys
  • Additional SSH Keys > Add SSH Key
  • Hostname: (EC2のIP)
    • SSHキーを使用するHost先を指定します。未指定だとすべての接続先に対して同じSSHキーを使用します。
    • まだドメインの紐付けをしていないので、ここでは使用しているEC2のIPを入力します。(ElasticIPで固定IPにしたものが必要です)
  • Private Key: 秘密鍵の中身
    • $ cat ~/.ssh/[秘密鍵] として出た結果を貼り付けます
    • -----BEGIN RSA PRIVATE KEY-----から始まるすべてを貼り付けないとエラーになるので注意
  • Add SSH Keyを押下

EC2インスタンスの接続ユーザー情報を環境変数に設定

引き続きCircleCIダッシュボードのProject Settingsでの操作です。

  • 左側のメニューEnvironment Variables
  • Add Environment Variable
  • Name: DEPLOY_USER
  • Value: (EC2に接続するuser名)
  • Add Environment variables
  • 同じ要領で以下も追加
    • Name: HOST
    • Value: (EC2のIP)

IPを開ける設定

EC2インスタンスに接続できるIPを制限している場合、CircleCIコンテナからの接続を受け付けることができません。
なので、ビルド時にCircleCIコンテナのIPからのSSH接続を許可し、処理が完了したら許可を取り消す、という方法を取ります。

具体的な手順は以下です。

  • AWSでCircleCI用のIAMユーザー/セキュリティグループを作成
  • CircleCIの環境変数にAWSアクセスキーを登録
  • シェルを作成

AWSでCircleCI用のIAMユーザー/セキュリティグループを作成

ここはAWSコンソールでの作業です。

IAMユーザー作成

  • AWSコンソール > IAM > ポリシー
  • ポリシーの作成 > JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupIngress"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:ec2:*:*:security-group/*"
        }
    ]
}
  • ポリシーの作成が完了
  • ユーザー > ユーザーを追加
    • ユーザー名: circleci
    • アクセスの種類: プログラムによるアクセス
  • アクセス許可の設定 > 既存のポリシーを追加 > さきほど作成したポリシーを追加
  • CSVをダウンロード

セキュリティグループ作成

CircleCI用のセキュリティグループを作成します。
もともと使っていたセキュリティグループをそのまま使ってもいいのですが、管理しやすさを考えてCI/CD用のグループを作っておきます。

  • AWSコンソール > EC2 > セキュリティグループ
  • セキュリティグループを作成
  • セキュリティグループ名や説明などは任意
  • インバウンド、アウトバウンドともにデフォルトでそのまま作成
  • 作成したセキュリティグループをEC2インスタンスにアタッチ
    • インスタンス一覧からインスタンスを右クリック > ネットワーキング > セキュリティグループを変更
    • 前項で作成したセキュリティグループを追加(既存のセキュリティグループを外す必要は無いです)

CircleCIの環境変数にAWSアクセスキーを登録

再びCircleCIダッシュボードに戻り、前項でダウンロードしたCSVの認証情報を登録します。
それぞれ以下の変数名で設定します。

  • アクセスキー : AWS_ACCESS_KEY_ID
  • シークレットアクセスキー : AWS_SECRET_ACCESS_KEY

シェルを作成

セキュリティグループに自身のIPを追加→SSH接続してデプロイ→セキュリティグループから自身のIPを削除するシェルを作成します。

shells/deploy.sh

#!/bin/sh
set -ex

export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
export AWS_DEFAULT_REGION="ap-northeast-1"

export DEPLOY_USER=${DEPLOY_USER}
export HOST=${HOST}

MY_SECURITY_GROUP="[セキュリティグループID]"
MY_IP=`curl -f -s ifconfig.me`

trap "aws ec2 revoke-security-group-ingress --group-id $MY_SECURITY_GROUP --protocol tcp --port 22 --cidr $MY_IP/32" 0 1 2 3 15
aws ec2 authorize-security-group-ingress --group-id $MY_SECURITY_GROUP --protocol tcp --port 22 --cidr $MY_IP/32
ssh $DEPLOY_USER@$HOST "cd [プロジェクトディレクトリ] && git pull origin master"

シェルはこちらの記事を参考にしています。記事に詳しい解説があるのでご参考ください。

config.ymlを編集する

version: 2

jobs:
  build-and-test:
    (省略)

  deploy:
    machine:
      enabled: true
    steps:
      - checkout
      - run: sudo pip install awscli
      - run:
          name: SSH経由のデプロイ
          command: |
            ./shells/deploy.sh

workflows:
  version: 2
  workflows:
    jobs:
      - build-and-test
      - deploy:
          requires:
            - build-and-test
          filters:
            branches:
              only: master

動作確認

ここまでの変更内容を、ci-testブランチにプッシュした後、ブランチをmasterにマージします。

CircleCIダッシュボードで確認してみると…

ci-testブランチではbuild-and-testジョブのみが実行され、
masterブランチにマージされると、build-and-testジョブに加えdeployジョブが動いているのがわかります。

無事通っているので、ローカルマシンからインスタンスに接続し、git logで変更内容がpullされていることを確認できたら自動ビルド設定は完了です。

これでmasterブランチに変更があったら自動でデプロイが動くようになりました。

ハマったポイント

設定でハマったポイントとその対応策を紹介します。

構文エラー

Error: Config does not conform to schema: {:workflows {:workflows {:jobs [nil {:deploy (not (map? nil)), :requires (not (map? a-clojure.lang.LazySeq)), :filters {:branches disallowed-key}}]}}}

workflows:のインデントが一段足りなかったので調整

CIでshellが動かない

$ chmod 755 [filepath]

して再度commit

shellが失敗してる

#!/bin/bash -eo pipefail
./shells/deploy.sh
/usr/local/lib/python2.7/dist-packages/urllib3/util/ssl_.py:394: SNIMissingWarning: An HTTPS request has been made, but the SNI (Server Name Indication) extension to TLS is not available on this platform. This may cause the server to present an incorrect TLS certificate, which can cause validation failures. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  SNIMissingWarning,
ssh: Could not resolve hostname ***********}: Name or service not known
/usr/local/lib/python2.7/dist-packages/urllib3/util/ssl_.py:394: SNIMissingWarning: An HTTPS request has been made, but the SNI (Server Name Indication) extension to TLS is not available on this platform. This may cause the server to present an incorrect TLS certificate, which can cause validation failures. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  SNIMissingWarning,
CircleCI received exit code 0

シェルスクリプトのタイポを修正。
set -exを付けて実行すると以下のように出力されてめちゃくちゃ便利です。

+ export AWS_ACCESS_KEY_ID=XXX
+ AWS_ACCESS_KEY_ID=XXX
+ export AWS_SECRET_ACCESS_KEY=XXX
+ AWS_SECRET_ACCESS_KEY=XXX
+ export AWS_DEFAULT_REGION=ap-northeast-1
+ AWS_DEFAULT_REGION=ap-northeast-1
+ export DEPLOY_USER=XXX
+ DEPLOY_USER=XXX
+ export 'HOST=X.XXX.XX.XX}'
+ HOST='X.XXX.XX.XX}'
+ MY_SECURITY_GROUP=sg-xxx
++ curl -f -s ifconfig.me
+ MY_IP=XXX.XXX.XX.XX
+ aws ec2 authorize-security-group-ingress --group-id sg-xxx --protocol tcp --port 22 --cidr xxx.xxx.xx.Xx/32
+ ssh 'xxx@x.xxx.xx.xx}' 'cd to-buy-list && git pull origin master'
ssh: Could not resolve hostname x.xxx.xx.xx}: nodename nor servname provided, or not known

(HOSTを指定する部分で閉じカッコ}が一つ多いという凡ミスでした)

終わりに

今回でテスト・デプロイ自動化の設定が完了し、開発基盤がようやく整いました。長かった…
次回からようやく機能開発に着手します。

参考