AWSのEC2を使ってCodeCommitのソースを定期的にCICDしてみた①〜EC2構築編〜

過去記事でHUGOをAWS環境で自動デプロイするための手順を紹介しましたがEC2でデプロイする方法に切り替えたのでその内容を紹介します。

Ishiguro Suguru

本WebサイトはHUGOで作成しておりビルドしてできた静的ファイルをS3に配置して公開する仕組みになっています。 この構成を見直したため紹介します。

過去記事で紹介していますがCICD(ビルドとデプロイ)はAWSのCodeCommit, CodePipeline, CodeBuildを使っています。

AWS環境におけるHUGOの自動デプロイ手順

HUGOには予約投稿機能がないため未来の日付で記事を作成しておき定期的にCloudWatchEventでCodePipelineをキックしていました。 しかしこの方法だとCodeBuildの利用料金がそこそこかかってしまうため(とはいっても10USD前後ですが)、なるべく利用料金を下げる方法を模索してEC2でCICDする方法に落ち着きました。

今回は構成変更した際の手順など紹介したいと思います。

やりたかったこと

当初の目的およびその解決策は以下のとおりです。

  • 目的:HUGOの記事をあらかじめ作成しておき予約投稿したかった
  • 解決策:公開日時に合わせてビルドを行い配信先のサーバやS3に配置(デプロイ)する

静的サイトジェネレーターのHUGOはビルド時点で記事を作成するため予約投稿ができません。 そのためあらかじめ記事を作成しておいて決まった時間に記事を公開するためには目的の時間にビルドを行いファイルを作成する必要があります。

本サイトはAWS環境に構築しているためAWSのリソースを使って公開日時に合わせたビルドとデプロイを実現しています。 その際に使用しているリソースは以下のとおりです。

  • 変更前:CloudWatchEvent + CodeCommit + CodePipeline + CodeBuild
  • 変更後:CloudWatchEvent + CodeCommit + EC2(+VPC)

実現方法はいくつかあると思いますが本サイトでやっていたのはこの2つです。 CloudWatchEventで公開日時に合わせてCICDをキックするところは同じですがCICDを行うリソースが違います。

どちらも結果は同じですが利用料金が若干ことなるためEC2でのCICDに変更しました。

EC2とCodeBuildの利用料金比較

CodeBuildの利用料金は公式サイトに記載があります。

AWS CodeBuild の料金

記載のとおりCodeBuildはインスタンスタイプに応じたビルド1分あたりの料金で計算されます。

たとえば以下の場合はざっくり月額7USDかかります。 ビルド回数を減らせば当然値段もさがりますがその辺を気にしたくないなと思いEC2に切り替えています。

  • インスタンスタイプ:general1.small
  • Linuxのビルド1分あたりの料金:0.005USD
  • 1日のビルド回数:10回
  • ビルドにかかる時間:5分
  • 30日分のトータルビルド時間:10×5×30=1500分
  • 無料利用枠:100分(general1.smallの場合)
  • CodeBuild利用コスト:(1500-100)×0.005=7USD

EC2の場合はCICD時のみ起動してそれ以外は停止していたとするとざっくり月額4.2USDです。 CICDだけなら起動時間はもっと短くできますが料金比較のための仮置きしています。

  • インスタンスタイプ:t2.medium
  • Linuxインスタンス1時間あたりの料金:0.06USD
  • 1日のインスタンス起動回数:10回
  • インスタンス起動時間:10分
  • 30日分のEC2トータル起動時間:10×10×30=3000分(50時間)
  • 汎用SSD(GP2):10GB
  • EBS1GBあたりの料金:0.12USD
  • EC2利用コスト:50×0.06=3USD
  • EBS利用コスト:10×0.12=1.2USD

比較するとおおよそ半額近くまで減らせるのではないかと思います。

EC2の利点としては金額の他にビルド時間を短縮できることです。 事前にEC2にビルド環境を準備しておけば毎回必要なモジュールをインストールする必要がないからです。

またCodeBuildの数が多い場合EC2に切り替えて処理すれば大幅に費用を削減できるかもしれません。

EC2のCICD処理の流れ

EC2でビルド・デプロイをする流れは以下のとおりです。

  1. CloudWatchEventでインスタンス起動

  2. インスタンス起動でスクリプト実行

    1. CodeCommitからビルドするソースを取得
    2. 取得したソースのビルド
    3. S3にビルドしたファイルを転送
    4. CloudFrontのキャッシュをクリア
    5. ビルドしたファイルの削除
  3. CloudWatchEventでインスタンス停止

起動時に実行する処理の流れはCodeBuildと同じになります。

EC2のCICD環境構築手順

上記の処理を実現するための手順を紹介します。

概要は以下のとおりです。

  1. EC2インスタンス構築
  2. CICD設定

HUGOのビルドはDockerを使用しています。 Dockerを使用すればHUGO以外のビルドを行いたい場合も同じEC2インスタンス上で簡単にビルド環境を構築できるためオススメです。

まずはCICDのためのEC2インスタンスを用意しましょう。

CloudFormationを使ったEC2インスタンス構築

今回はCloudFromationを使ってEC2インスタンスを作ってみました。コードは以下になります。

  • cmn-network.yaml: VPCなどネットワーク周りの設定
AWSTemplateFormatVersion: "2010-09-09"
Description: Create Common Network

Parameters:
  EnvironmentName:
    Type: String
    Default: cmn

  VPCCIDR:
    Type: String
    Default: 10.0.0.0/16

  PublicSubnetCIDR:
    Type: String
    Default: 10.0.1.0/24

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-vpc

  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-igw

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: 'AWS::Region'
      CidrBlock: !Ref PublicSubnetCIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-Public-subnet

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-Public-rtb

  PublicSubnetToInternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  EndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EndpointSecurityGroup
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-endpoint-sg
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref VPCCIDR

  EndpointS3:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - !Ref PublicRouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC

Outputs:
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub ${EnvironmentName}-vpc

  VPCCIDR:
    Value: !Ref VPCCIDR
    Export:
      Name: !Sub ${EnvironmentName}-vpccidr

  IGW:
    Value: !Ref IGW
    Export:
      Name: !Sub ${EnvironmentName}-igw

  PublicSubnet:
    Value: !Ref PublicSubnet
    Export:
      Name: !Sub ${EnvironmentName}-Public-subnet

  PublicRouteTable:
    Value: !Ref PublicRouteTable
    Export:
      Name: !Sub ${EnvironmentName}-Public-rtb

  SecurityGroup:
    Value: !Ref EndpointSecurityGroup
    Export:
      Name: !Sub ${EnvironmentName}-endpoint-sg

  EndpointS3:
    Value: !Ref EndpointS3
    Export:
      Name: !Sub ${EnvironmentName}-endpoint-S3
  • cmn-ec2.yaml: EC2インスタンス設定
AWSTemplateFormatVersion: "2010-09-09"
Description: Create EC2 for Build

Parameters:
  EnvironmentName:
    Type: String
    Default: cmn

  Ec2ImageId:
    Type: String
    Default: ami-0ce107ae7af2e92b5

  Ec2InstanceType:
    Type: String
    Default: t2.medium

  KeyPair:
    Type: String
    Default: xxxxx

Resources:
  EC2IAMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${EnvironmentName}-ssm-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - Ref: EC2IAMRole
      InstanceProfileName: !Sub ${EnvironmentName}-EC2InstanceProfile

  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2SecurityGroup
      VpcId: !ImportValue cmn-vpc
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-ec2-sg

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref Ec2InstanceType
      ImageId: !Ref Ec2ImageId
      IamInstanceProfile: !Ref EC2InstanceProfile
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !ImportValue cmn-Public-subnet
          GroupSet:
            - !Ref EC2SecurityGroup
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeSize: 10
            VolumeType: gp2
      SourceDestCheck: true
      KeyName: !Ref KeyPair
      UserData: !Base64 |
        #!/bin/bash
        yum update -y
        yum install git -y
        sudo ln -sf /usr/share/zoneinfo/Japan /etc/localtime
        sudo sed -i "s/\"UTC\"/\"Japan\"/g" /etc/sysconfig/clock
        sudo sed -i "s/en_US\.UTF-8/ja_JP\.UTF-8/g" /etc/sysconfig/i18n
        yum install -y docker
        systemctl enable docker
        service docker start
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-build-ec2
        - Key: AutoShutDown
          Value: true
        - Key: AutoStartUp
          Value: true

Outputs:
  EC2SecurityGroup:
    Value: !Ref EC2SecurityGroup
    Export:
      Name: !Sub ${EnvironmentName}-ec2-sg

  EC2Instance:
    Value: !Ref EC2Instance
    Export:
      Name: !Sub ${EnvironmentName}-build-ec2

長いのでポイントだけ解説します。

  • ファイルはcmn-network.yamlcmn-ec2.yamlにわけているが1つにまとめてしまってもOK。
  • キーペアはあらかじめ作成しておきパラメータKeyPairで指定する。
  • EC2へはSSM(AWS Session Manager)を使って接続できるようにするためEC2IAMRole, EC2InstanceProfileを作成。
  • UserDataを指定してEC2構築時に必要なソフトウェアなどインストール。

今回はDockerを使用するためUserDataにあらかじめインストールコマンドを記述し有効化してあります。 その他gitのインストールおよびAmazon Linuxで行う初期設定をとりあえず入れています。

#!/bin/bash
yum update -y
yum install git -y
sudo ln -sf /usr/share/zoneinfo/Japan /etc/localtime
sudo sed -i "s/\"UTC\"/\"Japan\"/g" /etc/sysconfig/clock
sudo sed -i "s/en_US\.UTF-8/ja_JP\.UTF-8/g" /etc/sysconfig/i18n
yum install -y docker
systemctl enable docker
service docker start

UserDataについては以下に詳しく載っていますので興味ある方は呼んでみてください。

参考情報:cloud-initを使ったLinux OSの初期設定

またSSMについてはこちらにまとまっています。

参考情報:さらば踏み台サーバ。Session Managerを使ってEC2に直接SSHする

【補足】AWSコンソールを使ったCoudFormation実行方法

補足としてCoudFormation実行方法を紹介しておきます。

①AWSコンソールにログインしてCloudFormation管理画面を開く。

②「スタックの作成>新しいリソースを使用(標準)」をクリック。

③テンプレートの指定画面が開く。テンプレートのyamlファイルをS3に配置してURLを入力。

④スタックの詳細指定画面が開く。スタックの名前と必要なパラメータを入力して「次へ」をクリック。

⑤スタックオプションの設定画面が開く。デフォルトのままで「次へ」をクリック。

⑥設定内容確認のためのレビュー画面が開く。IAMロールを作成する場合はIAMリソース作成の承認チェックボックスにチェックを入れ「スタックの作成」をクリック。

以上でCloudFormationが実行されます。 正常に実行されるとステータスが"CREATE_COMPLETE"or"UPDATE_COMPLETE"と表示されます。

最後に

長くなってしまったためEC2構築後の設定やスクリプトについては次回記事で紹介します。

comments powered by Disqus