[AWS]JenkinsでTerraformによるインフラデプロイをやってみた

# はじめに
- 今回はAmazon EC2でJenkinsサーバを構築し、Jenkins上でTerraformによるインフラのデプロイをやってみたいと思います。
- 以下のバージョンで動作確認をしています。
    - Jenkins: 2.361.1
    - Terraform: 1.3.4

# 注意事項
- 本記事で構築するアーキテクチャはdemo用のため、本番環境などで利用する際に非機能要件等を考慮した適切な設計が必要です。
- ソースのバージョン管理としてGithubを使用していますが、Githubアカウントをお持ちでない方は別途用意する必要があります。
- 本記事はJenkinsパイプラインの構築およびJenkinsパイプラインを使ったTerraformデプロイのやり方にフォーカスしていますので、それを実現するための関連AWSリソースの詳細構築手順を割愛させていただきます。


# 構成図
![tf-jenkins-demo.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/af603eed-a791-cee2-f84b-4a96c3b1faed.png)


# 構成図の説明
1. Terraformコードをリモートリポジトリにpush
1. Jenkinsコンソールにブラウザ接続し、ジョブを実行
1. TerraformコードをリモートリポジトリからJenkinsサーバにclone
1. terraform planとterraform applyを実行、tfstateファイルは同じアカウントに作成したS3バケットに保存
1. Terraformで検証用AWSリソースを作成


# 環境構築
上記のアーキテクチャを実現するためのAWSリソースをap-northeast-1リージョンに作成していきます。記載ない設定項目はデフォルト値を使用します。（詳細構築手順は割愛）
また、ネットワーク系（VPC、Subnet等）リソースは既存のリソースを使っていただいても構いません。
※以下のCloudFormationテンプレートを用意していますので、構築の手間を省きたいという方はご活用いただければと思います。

<details><summary>cfn template</summary>

```yaml
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  MyPublicIP:
    Type: "String"
    Description: "end with /32"
    Default: ""
  KeyName:
    Type: "AWS::EC2::KeyPair::KeyName"
  InstanceType:
    Type: "String"
    Default: "t2.micro"
  AMI:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
  TfstateS3BucketName:
    Type: "String"
    Default: ""

Resources:
  # Create VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: "Name"
          Value: "tf-jenkins-demo-vpc"

  # Create IGW
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: "Name"
          Value: "tf-jenkins-demo-igw"
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  # Create Route Tables
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: "tf-jenkins-demo-public-rtb"
      VpcId: !Ref VPC
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicRouteTable

  # Create Public Subnet
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: !Sub ${AWS::Region}a
      Tags:
        - Key: "Name"
          Value: "tf-jenkins-demo-public-subnet-1a"
  PublicSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetA

  # Create SG for EC2
  LinuxEC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: "tf-jenkins-demo-linux-sg"
      GroupDescription: "tf-jenkins-demo-linux-sg"
      VpcId: !Ref VPC
      Tags:
          - Key: Name
            Value: "tf-jenkins-demo-linux-sg"
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: !Ref MyPublicIP
          Description: "for jenkins console"
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyPublicIP
          Description: "for ssh"
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: "0.0.0.0/0"
          Description: "for internet access"

  # Create IAM Role for EC2
  IAMRoleForLinuxEC2:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: "ec2-s3-full-access-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - 'ec2:*'
                  - 's3:*'
                Resource: '*'
      RoleName: "ec2-s3-full-access-role"
  LinuxInstanceProfile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Path: /
      Roles:
        - !Ref IAMRoleForLinuxEC2

  # Create EC2
  LinuxEC2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref AMI
      KeyName: !Ref KeyName
      InstanceType: !Ref InstanceType
      IamInstanceProfile: !Ref LinuxInstanceProfile
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !Ref PublicSubnetA
          GroupSet:
            - !Ref LinuxEC2SG
      Tags:
          - Key: Name
            Value: "jenkins-server"

  # Create S3 Bucket for saving tfstate file
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref TfstateS3BucketName

# Public ip of jenkins server
Outputs:
  JenkinsServerPublicIp:
    Description: Public IP Of Jenkins Server
    Value: !GetAtt LinuxEC2.PublicIp
    Export:
      Name: !Sub "${AWS::StackName}-PublicIp"

```
</details>

## ネットワーク
- VPC
    
| 設定項目          | 設定値              | 
| ----------------- | ------------------- | 
| 名前タグ          | tf-jenkins-demo-vpc | 
| IPv4 CIDRブロック | 10.0.0.0/16         | 
| DNSホスト名       | 有効化              | 
| DNS解決           | 有効化              | 

- インターネットゲートウェイ

| 設定項目        | 設定値              | 
| --------------- | ------------------- | 
| 名前タグ        | tf-jenkins-demo-igw | 
| アタッチするVPC | tf-jenkins-demo-vpc | 

- ルートテーブル

| 設定項目 | 設定値                                             | 
| -------- | -------------------------------------------------- | 
| 名前タグ | tf-jenkins-demo-public-rtb                         | 
| VPC      | tf-jenkins-demo-vpc                                | 
| ルート   | 送信先: 0.0.0.0/0<br>ターゲット: tf-jenkins-demo-igw |
 
- サブネット

| 設定項目                 | 設定値                           | 
| ------------------------ | -------------------------------- | 
| VPC                      | tf-jenkins-demo-vpc              | 
| サブネット名             | tf-jenkins-demo-public-subnet-1a | 
| アベイラビリティーゾーン | ap-northeast-1a                  | 
| IPv4 CIDRブロック        | 10.0.1.0/24                      | 
| 関連付けるルートテーブル | tf-jenkins-demo-public-rtb       | 

## セキュリティグループ

| 設定項目                                   | 設定値                                                                                                                   | 
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | 
| セキュリティグループ名                     | tf-jenkins-demo-linux-sg                                                                                                 | 
| 説明                                       | tf-jenkins-demo-linux-sg                                                                                                 | 
| VPC                                        | tf-jenkins-demo-vpc                                                                                                      | 
| インバウンドルール（カスタムTCP）          | タイプ: タスタムTCP<br>プロトコル: TCP<br>ポート範囲: 8080<br>ソース: 自分自身のパブリックIPアドレス（末尾に/32が必要）  | 
| インバウンドルール（SSH）                  | タイプ: SSH<br>プロトコル: TCP<br>ポート範囲: 22<br>ソース: 自分自身のパブリックIPアドレス（末尾に/32が必要）                                                                         | 
| アウトバウンドルール（インターネット接続） | タイプ: すべてのトラフィック<br>プロトコル: すべて<br>ポート範囲: すべて<br>送信先: 0.0.0.0/0                            | 

## IAMポリシー＆ロール
- IAMポリシー
```json: ec2-s3-full-access-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ec2:*",
                "s3:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```

- IAMロール

| 設定項目                     | 設定値                    | 
| ---------------------------- | ------------------------- | 
| ロール名                     | ec2-s3-full-access-role   | 
| 信頼されたエンティティタイプ | EC2                       | 
| 許可ポリシー                 | ec2-s3-full-access-policy | 

## EC2インスタンス

| 設定項目                     | 設定値                           | 
| ---------------------------- | -------------------------------- | 
| 名前タグ                     | jenkins-server                   | 
| AMI                          | Amazon Linux 2(amzn2-ami-hvm-x86_64-gp2)                   | 
| インスタンスタイプ           | t2.micro                         | 
| キーペア                     | 既存のものを選択、または新規作成 | 
| VPC                          | tf-jenkins-demo-vpc              | 
| サブネット                   | tf-jenkins-demo-public-subnet-1a | 
| パブリック IP の自動割り当て | 有効化                           | 
| セキュリティグループ         | tf-jenkins-demo-linux-sg         | 
| IAMインスタンスプロフィール  | ec2-s3-full-access-role          | 

## S3バケット（Terraformのbackend用）

| 設定項目   | 設定値                                 | 
| ---------- | -------------------------------------- | 
| バケット名 | 任意（グローバルで一意でなければならない | 
| リージョン | ap-northeast-1                         | 



# Jenkinsの構築
## Jenkinsのインストール
上記作成したEC2にJenkinsをインストールしていきます。Dockerコンテナなどで構築する方法もあるかと思いますが、今回はEC2に直接インストールすることにします。
EC2にssh接続して、[Jenkins公式サイト](https://www.jenkins.io/doc/book/installing/)のインストール手順を参考に、下記のコマンドを実行していきます。
```bash
sudo wget -O /etc/yum.repos.d/jenkins.repo \
    https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
sudo yum upgrade -y
sudo amazon-linux-extras install java-openjdk11 -y
sudo yum install git -y
# Jenkinsのバージョンを指定してインストールする
sudo yum install jenkins-2.361.1 -y
sudo systemctl daemon-reload
sudo systemctl enable jenkins
sudo systemctl start jenkins
```

## Jenkinsの初期設定
ブラウザからJenkinsにアクセスして初期設定を行います。
URL: `http://<EC2のパブリックIPアドレス>:8080/`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/301c5a87-f4b1-43ff-0276-7e0782732c81.png)
初回アクセス時は、Unlock Jenkinsの画面が表示され、Administrator passwordが聞かれます。EC2で以下のコマンドを実行してAdministrator passwordを確認します。
```bash
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
```
「Administrator password」を入力し、「Continue」をクリックします。次のプラグインのインストール確認画面で「Install suggested plugins」を選択します。
![Screenshot 2022-11-10 at 11.21.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/3257deba-b81e-fbb1-66fa-cf443fc128a1.png)

初期Adminユーザ作成画面が表示されたら必要事項を入力して「Save and Continue」をクリックします。

![Screenshot 2022-11-10 at 11.30.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/d797c069-2ad8-b63e-1a20-38c36c7c07ad.png)

そのまま「Save and Continue」をクリックして、「Start using Jenkins」をクリックします。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f323935343733342f32353661323764632d353765332d393262302d376532302d3764373130323437393062622e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/64c81860-b1ba-b2b6-542e-1df3c470c2c1.png)
![Screenshot 2022-11-10 at 11.32.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/7a7a774e-07db-9dd2-8a8a-a23f5f90fb07.png)




## Terraformプラグインのインストール
ダッシュボードが表示されたら、「Jenkinsの管理」→「プラグインの管理」を開きます。「Plugin Manager」画面で、「利用可能」タブをクリックし、「Terraform」と検索し、表示されたプラグインをインストールします。
![Screenshot 2022-11-10 at 11.53.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/edcf8425-4e0b-9390-90ff-291b3b0a448f.png)
インストール完了後、「Jenkinsの管理」→「Global Tool Configuration」を開き、Terraform欄の「Terraform追加」をクリックします。「Name」を入力し該当バージョンのTerraformを選択します。今回は1.3.4をインストールします。最後に「Save」をクリックして設定を保存します。
※「Name」欄に入力する名前は任意ですが、Jenkinsfileのtoolsブロックに記述する名前と一致する必要があります。
![Screenshot 2022-11-10 at 12.12.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/be7305ff-1072-d350-fc6e-67a15ed04d06.png)

# Githubリポジトリの作成
今回はdemoのため、パブリックリポジトリを作成することにします。プライベートリポジトリを作成する場合は、リポジトリcloneのためのaccess tokenやssh keyをJenkinsに登録する必要がありますので、ご注意ください。

# JenkinsfileとTerraformコードの作成
## Jenkinsfile
pipelineのtoolsブロックにJenkinsにインストールしたTerraformプラグインの「Name」を記述します。また、今回はinputを使って、terraform plan実行後、手動で承認することでterraform applyを実行するようにします。
※本スクリプトはdemo用のため、基本的な処理しか記述されていません。
```groovy: Jenkinsfile
pipeline {
  agent any
  options {
    skipDefaultCheckout(true)
  }
  tools {
    terraform "terraform-1.3.4"
  }
  stages{
    stage("clean workspace") {
      steps {
        cleanWs()
      }
    }
    stage("checkout") {
      steps {
        checkout scm
      }
    }
    stage("terraform init") {
      steps {
        sh "terraform init -no-color"   
      }
    }
    stage("terraform plan") {
      steps {
        sh "terraform plan -no-color -out=plan.out"
        input message: "Apply Plan?", ok: "Apply"
      }
    }
    stage("terraform apply") {
      steps {
        sh "terraform apply plan.out -no-color"    
      }
    }
  }
}

```
## Terraformコード
今回はdemoのため、tfファイルを分けずに、provider、backend、対象リソース等を全部main.tfに記述します。今回はTerraformでVPCを作成してみます。「created-by-terraform」という名前タグをつけます。
backendに使用するS3バケットは[環境構築](#環境構築)で作成したS3バケットの名前を指定してください。
```terraform: main.tf
provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  required_version = "~> 1.3.4"
  backend "s3" {
    # your own s3 bucket
    bucket = ""
    region = "ap-northeast-1"
    key    = "terraform.tfstate"
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "created-by-terraform"
  }
}

```
## ディレクトリ構成
作成済みのJenkinsfileとTerraformコードを事前に用意したGithubリポジトリにpushします。ディレクトリ構成は以下とします。
```
.
├── .gitignore
├── Jenkinsfile
└── main.tf
```

# Jenkinsジョブの作成
ダッシュボード→「新規ジョブ作成」をクリックします。ジョブ名を入力し、パイプラインを選択し、「OK」をクリックします。
![Screenshot 2022-11-10 at 18.46.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/c96ac7ca-76ef-e992-11de-02504b06ecea.png)

パイプラインを以下のように設定します。
- 定義: Pipeline script from SCM
- SCM: Git
- リポジトリURL: 該当リポジトリのURL
    - ![Screenshot 2022-11-10 at 15.02.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/0e6a0acc-4c0b-0f3f-9611-2d0ca86e7fe8.png)
- ブランチ指定子: 対象ブランチを指定
- Script Path: Jenkinsfile

![Screenshot 2022-11-10 at 18.47.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/6a0e4c24-22a8-76fa-d45b-ea279c80d1f7.png)
![Screenshot 2022-11-10 at 18.47.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/b628cdf1-a6ee-e603-9e77-e8925e0891b9.png)
最後に「保存」をクリックします。


# Jenkinsジョブの実行
では、ジョブを実行してみます。「ビルド実行」をクリックします。
![Screenshot 2022-11-10 at 18.48.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/9c16e210-87e6-fba0-2df9-11bc0df9e678.png)
ビルド履歴に実行中のジョブが表示されますので、該当ジョブの「Console Output」画面を開きます。
![Screenshot 2022-11-10 at 18.48.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/6003d2b0-1293-c10b-2c32-1e2994cbbb17.png)
ジョブの中でterraform initとterraform planが実行され、その後「Apply Plan?」というinput messageが表示されます。「Apply」をクリックするとterraform applyが実行され、AWSリソースが作成されます。「Abort」をクリックするとジョブが終了しますので、terraform applyは実行されません。
![Screenshot 2022-11-10 at 18.48.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/f5c73c5c-47a4-75c3-d054-4a4ffad9654a.png)

ジョブが正常に終了したら、AWSマネージメントコンソールで該当リソースを確認してみましょう。「created-by-terraform」というVPCが無事作成されましたね。
![Screenshot 2022-11-10 at 18.49.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/a5169493-10a8-96cb-cf52-78c49fb0c985.png)

今回はJenkinsfileにinputを記述し、手動承認のフェーズを入れていますが、手動承認が必要ない場合は、該当箇所のinputを削除していただいても構いません。

また、Jenkinsジョブのビルドトリガを設定することで、SCMポーリングして変更があったらジョブを起動したり、あるいは定期的にジョブを実行することもできます。
![Screenshot 2022-11-10 at 18.53.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/2f378dcd-3c95-c8a6-2386-d34b8ad88750.png)


# おわりに
今回はAmazon EC2でJenkinsサーバを構築し、Jenkins上でTerraformによるインフラのデプロイをやってみました。簡単なdemoではありますが、JenkinsでTerraformを動かす際にぜひ参考にしていただければと思います。
最後に、今回構築したAWSリソースの削除をお忘れずに！

# 参考
- [Installing Jenkins | jenkins.io](https://www.jenkins.io/doc/book/installing/)
- [Pipeline | jenkins.io](https://www.jenkins.io/doc/book/pipeline/)
- [Providers - aws | registry.terraform.io](https://registry.terraform.io/providers/hashicorp/aws/4.38.0)