5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Packer, Ansible, Terraformで作るBlue-Greenデプロイメント (AWS)

Last updated at Posted at 2019-07-08

#概要
AWS環境で、デプロイメントのフローを自動化する仕事を任されたので、成果物をここにメモしておきます。

デプロイをするシステムは、以下のように構成されています。
122905.png

Blue/Greenの2環境を用意し、普段は片方のみを使用しています。使用していないほうの環境にデプロイをして、ELBの振り分け先を変更することで、ダウンタイムのないデプロイメントを目指します。また、packerやAnsible, Terraformなどのツールを使うことで、このフローを自動化していきます。

尚、こちらの記事こちらの記事などを参考にして作成しました。
#作業手順

  1. アプリケーションや必要なミドルウェアなどをインストールした、ゴールデンAMIを作成する
  2. ゴールデンAMIを参照する起動設定を新規作成し、デプロイ先のAutoScaling Groupにそれを読み込ませる
  3. Elastic Load BalancerのListenerが転送するTargetGroupを変更し、本番環境をBlueからGreenに(もしくはGreenからBlueに)切り替える

##AMIの作成
PackerとAnsibleを使って、ゴールデンAMIを作成します。AMIの基本的な設定はPackerで、ツールのインストール等の構成管理はAnsibleで、それぞれ行います。

まずは、Packerの設定です。

packer.json
{
    "variables":{
        "version":"" //デプロイのたびに変わる値は、変数としておきます
    },									
    "builders":      // AMIを作成するために一時的に立てられるEC2インスタンスについて定義								
    [										
        {										
            "type":"amazon-ebs",										
            "ami_name": "test-AMI-v{{user `version`}}",
            "region": "ap-northeast-1",										
            "source_ami": "ami-xxxxxxxxxxx",  // 基とするAMI
            "instance_type": "t2.micro",
            "ssh_username": "centos",
            "tags":{
                "version":"{{user `version`}}" // 起動設定を作成するときに、見つけやすいようにタグをつけておく
            },
            "force_deregister": true,
            "force_delete_snapshot": true,
            "security_group_ids":["sg-xxxxxxx","sg-xxxxxxx"],
            "iam_instance_profile": "xxxxxxx",
            "subnet_id":"subnet-xxxxxxxx"
        }
    ],
    "provisioners":   // AMI作成用のインスタンスの中で行いたい処理を定義
    [
        {
            "type":"shell",
            "inline": [  // Ansibleを動かすためのツールをインストール
                "sudo -E yum -y update",
                "sudo -E yum -y install epel-release",
                "sudo -E yum -y install python-pip",
                "sudo -E yum -y install ansible"
            ]
        },
        {
            "type":"ansible-local",   // Ansibleの起動
            "playbook_file":"setup.yml"
        }
    ]
}

上のファイルの中から呼ばれている、Ansibleの設定ファイル(playbook)の設定は、以下の通りです。
尚、以下ではS3上のmavenリポジトリから成果物をとってくるようにしてあります。

setup.yml
- hosts: all
  become: yes
  vars:
    proxy_env: # すべてに共通するプロキシ設定は、変数化しておく
      https_proxy: xxxxxxxxx
      http_PROXY: xxxxxxxxx
      HTTPS_PROXY: xxxxxxxxx
      HTTP_PROXY: xxxxxxxxx
      no_proxy: xxxxxxxxx
  tasks:
    - environment: "{{proxy_env}}"
      name: "install boto3 (required by aws-s3)" # boto3のインストール
      pip:
        name: boto3
    - pip:
        name: lxml
      name: "install lxml" # Ansibleのs3モジュールを動かすのに必要
      environment: "{{proxy_env}}"
    - maven_artifact: # S3上のmavenリポジトリから成果物を取得
        repository_url: 's3://xxxxxxxx'
        group_id: jp.co.xx.xx
        artifact_id: xxxxxx
        version: 1.0.0
        extension: tgz
        dest: /tmp/xxxxxx.tgz

packerコマンドで上記のpackerおよびAnsibleを実行すると、AMIが作成されるはずです
##ASGの設定変更
Terraformを用いて、まず上で作成したAMIを読み込む起動設定を作成し、そしてデプロイ先のASG(使用中でない環境のASG)にその起動設定を読み込ませるようにします。

deploy.tf
provider "aws" {
  region = "ap-northeast-1"
}

variable "target_version" {} // デプロイのたびに値が変わるものは、変数化しておく

data "aws_ami" "centos" { // packerとAnsibleで作成したAMIを取得
  most_recent = true
  owners      = ["xxx"]
  filter {  // タグで検索
    name = "version"
    values = ["${var.target_version}"]
  }
}

resource "aws_launch_configuration" "test" { // 起動設定を新規作成
  name                 = "config-test-v${var.target_version}"
  image_id             = "${data.aws_ami.centos.id}" // 作成したAMIを指定
  instance_type        = "t2.micro"
  enable_monitoring    = false
  security_groups      = ["sg-xxxxxxx", "sg-xxxxxxxxx","sg-xxxxxxx"]
  key_name             = "xxxxxxxxx"
  iam_instance_profile = "xxxxxxxxx"
  root_block_device {
    delete_on_termination = true
    iops = 100
    volume_size = 30
    volume_type = "gp2"
  }
}

resource "aws_autoscaling_group" "test_resource" {  // デプロイ先のASGが上の起動設定を読みこむよう、設定変更
  name = "asg-test"
  launch_configuration = "${aws_launch_configuration.test.name}"  // 上で作成した起動設定を指定
  min_size = 2
  max_size = 2
  desired_capacity = 2
  target_group_arns = ["xxxxx","xxxxxx"]
  vpc_zone_identifier = ["xxxxxxx","xxxxxxxx"]
  lifecycle {
    create_before_destroy = true
  }
}

terraformコマンドで上の設定を反映させると、使用中でない環境のASGに設定が反映されます。
##Blue/Greenの切り替え
最後に、ELBの振り分けの設定を変えます。適当なスクリプト言語でシェルを書くのですが、今回はCentOSにデフォルトで入っているPythonの2系で書きます(今時Python2かよ、って感じですが)。

処理の手順は、以下の通りです

  1. ASGに紐づくEC2のインスタンスのリストを取得
  2. EC2がすべて立ち上がって、テストをパスしたか確認する
  3. EC2がすべて立ち上がっていれば、ELBのListenerのリクエストの転送先のTargetGroupを変更し、Blue/Greenを切り替える

以下がコードです。

switch_elb.sh
#! /bin/python
# -*- coding: utf-8 -*-

from time import sleep
import json
import subprocess

# ASGに紐づくEC2インスタンスのリストの取得
GET_INSTANCE_LIST = """aws ec2 describe-instances --filters "Name=tag:version,Values=%s"""
# EC2インスタンスがヘルシーな状態であるか確認
GET_INSTANCE_STATUS = "aws ec2 describe-instance-status --instance-id %s"
# ELBの切り替え
SWITCH_ELB_LISTENER = "aws elbv2 modify-rule --rule-arn %s --actions Type=forward,TargetGroupArn=%s"

# EC2のリストを取得し、ヘルスチェック
def get_ec2_health(version):
    instance_ids = []
    # インスタンスのリスト取得
    instance_list = subprocess.check_output(GET_INSTANCE_LIST % version,shell=True)
    instance_dict = json.loads(instance_list)
    for instance in instance_dict["Reservations"]:
        # 停止中のインスタンスは除く
        if instance["Instances"][0]["State"]["Code"] != 48:
            instance_ids.append(instance["Instances"][0]["InstanceId"]) 
    for i in range(30):
        # 一つずつインスタンスをヘルスチェックし、一つでもダメであれば、15秒後に再チェック
        result = check_ec2_status(instance_ids)
        if result == True:
            return True
        else:
            sleep(15)
    return False

# EC2のヘルスチェック
def check_ec2_status(instance_ids):
    for instance_id in instance_ids:
        result = commands.getoutput(GET_INSTANCE_STATUS % instance_id)
        status_dict = json.loads(result) 
        # 一つでもヘルスチェックに失敗すれば、Falseを返す
        if status_dict["InstanceStatuses"][0]["SystemStatus"]["Details"][0]["Status"] != "passed" or status_dict["InstanceStatuses"][0]["InstanceStatus"]["Details"][0]["Status"] != "passed":
            return False
    return True

# ELBの切り替え
def switch_elb():
    ELB_rules = {}
    try:
        # ELBのListenerのRuleのarnをjson形式で羅列しておくファイル
        file = open("../../ELB_rules_arns.json","r")
        ELB_rules = json.load(file)
    except:
        print("ELBのルールの情報が読み込めませんでした")
        return
    target_group_arns = {}
    try:
        # デプロイ先の環境の、TargetGroupのarnをjson形式で羅列しておくファイル
        file = open("./target_group_arns.json","r")
        target_group_arns = json.load(file)
    except:
        print("ターゲットグループの情報が読み込めませんでした")
        return
    for key in ELB_rules:
        result = subprocess.call(SWITCH_ELB_LISTENER % (ELB_rules[key],target_group_arns[key]),shell=True)
        if result == 0:
            print("切り替え成功")
    return 0

# メインの関数
def main():
    version = input("デプロイのバージョンを指定してください")
    ec2_health = get_ec2_health(version)
    if ec2_health == True:
        switch_elb_status = switch_elb()
        if switch_elb_status != 0:
            print("ELBの切り替えがうまくいきませんでした")
        else:
            print("ELBの切り替えに成功しました")
    else:
        print("EC2のヘルスチェックが失敗したため、切り替えを行いません")

if __name__ == "__main__":
    main()

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?