こしあん
2025-02-07

TerraformとGitHubを活用したCodeDeploy導入


30{icon} {views}


TerraformでAWSインフラを構築し、EC2上のhttpdをCodeDeployで動的に更新する手順を解説。GitHubリポジトリとCodePipelineを連携し、appspec.ymlや再起動スクリプトによる自動デプロイを実現してみる。

はじめに

  • DOPの勉強でCodeシリーズを触ってみた。簡単な例として、EC2上でホストされているWebサーバーのコンテンツを、CodeDeployがGitHubからとってきてアップデートする例を実装する
  • インフラはTerraform、コンテンツはGitHubといった併用が可能

構成

  • ALBの下にあるEC2でWebサーバー(httpd)がホストされている
  • httpdのコンテンツをGitHubで管理し、CodeDeployを使って動的にアップデートしたい
  • CodePipelineはGitHubとの連携やCodeDeployの起動用
  • 基本的にAWS側のインフラは(CodeDeployやCodePipelineも含む)はTerraformで管理する

ポイント

ポイントはGitHubとTerraformの住み分け

GitHub側

github-repository/
├── appspec.yml
├── index.html
└── scripts
    └── restart_httpd.sh

CodePipelineがGitHubを読んでくるので、CodeDeployに渡すものやその処理をGitHub側で定義する

  • appspec.yml : CodeDeployの処理フローの定義。ファイル名はこれにする必要がある。GitHub Actionsの定義みたいなもの
  • index.html : httpdで読み込まれるindex.html。このようにアプリケーションで必要なアセットもGitHub側で管理し、CodeDeployを通じて実行環境のEC2にわたすことができる
  • scripts/restart_httpd.sh:デプロイ時に呼ばれるシェルスクリプト。ここはhttpdの再起動をしている

AWS側

全般的にIAMの権限が最初慣れるのに時間かかるかもしれない。

  • CodePipeline
    • GitHubとの連携はoAuthでやるのが定石だが、面倒なのでPersonal Access Tokenでやる(ただこの方法は非推奨になっているので注意)
    • ポリシーが注意が必要で、アーティファクトのS3バケットへの読み書き・AWSCodeDeployDeployerAccessマネージドポリシーをつける
  • CodeDeploy
    • AWSCodeDeployRole(実質ポリシー)をCodeDeployのロールにアタッチ。arnはarn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
    • CodePipelineにつけたマネージドポリシーとは別物
  • EC2
    • 初期起動時にCodeDeployエージェントのインストールが必要。ユーザーデータで行うのが手軽(Terraform側で行う)
    • CodeDeployエージェントは、CodeDeployとの橋渡しを行い、実際のデプロイ処理を行う
    • S3からアーティファクトのDLを行うので、S3への読み取り権限が必要

結果

Terraform→CodeDeploy(CodePipeline)の順番で実行する。

Terraformだけ実行した場合

httpdのデフォルトの表示になる

CodePipelineを実行した後

CodePipeline側

全体のPipeline

デプロイグループが作成されている。このデプロイではOneAtTimeというオプションだが他のオプションも可能。

インスタンス単位のデプロイ詳細

コード(GitHub側)

github-repository/
├── appspec.yml
├── index.html
└── scripts
    └── restart_httpd.sh

appspec.yml

正確にこのファイル名にする必要がある。GitHub Actionsのワークフロー定義のようなもの

version: 0.0
os: linux
files:
  # リポジトリルートから全ファイルを /var/www/html にコピーする
  - source: /
    destination: /var/www/html
hooks:
  # ファイルの配置が完了した後、httpdの再起動を実行する
  AfterInstall:
    - location: scripts/restart_httpd.sh
      timeout: 300
      runas: root

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>My Static Site</title>
</head>
<body>
  <h1>Welcome to My Static Website!</h1>
  <p>This page is deployed via CodeDeploy and Terraform.</p>
</body>
</html>

scripts/restart_httpd.sh

#!/bin/bash
# httpd (Apache) の再起動
systemctl restart httpd

コード(Terraform側)

alb_ec2.tf

##########################
# ALB用セキュリティグループ
##########################
resource "aws_security_group" "alb_sg" {
  name        = "alb-sg"
  description = "Allow HTTP access from ALB"
  vpc_id      = var.vpc_id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description      = "Allow all outband traffic"
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

##########################
# ALBの作成
##########################
resource "aws_lb" "my_alb" {
  name               = "my-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = var.public_subnet_ids
}

##########################
# ターゲットグループの作成
##########################
resource "aws_lb_target_group" "my_tg" {
  name     = "my-target-group"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  # ヘルスチェック設定(必要に応じて調整)
  health_check {
    path                = "/"
    protocol            = "HTTP"
    matcher             = "200"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    interval            = 30
    timeout             = 5
  }
}

##########################
# ALBリスナーの作成
##########################
resource "aws_lb_listener" "my_listener" {
  load_balancer_arn = aws_lb.my_alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.my_tg.arn
  }
}

##########################
# EC2用セキュリティグループ
##########################
resource "aws_security_group" "ec2_sg" {
  name        = "ec2-sg"
  description = "Allow HTTP access from ALB to EC2 instance (httpd)"
  vpc_id      = var.vpc_id

  ingress {
    description = "Allow HTTP from ALB"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    # ALBのセキュリティグループからのアクセスを許可
    security_groups = [aws_security_group.alb_sg.id]
  }

  egress {
    description = "Allow all outband traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

##########################
# IAMインスタンスロールの作成
##########################
resource "aws_iam_role" "ec2_ssm_role" {
  name               = "ec2_ssm_role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = {
        Service = "ec2.amazonaws.com"
      },
      Action = "sts:AssumeRole"
    }]
  })
}

# 必要なIAMポリシー(S3へのアクセス権限)をアタッチする
resource "aws_iam_role_policy" "ec2_s3_policy" {
  name = "s3_read_policy"
  role = aws_iam_role.ec2_ssm_role.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "s3:GetObject",
          "s3:GetObjectVersion"
        ],
        Resource = [
          "${aws_s3_bucket.codepipeline_artifacts.arn}/*"
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ec2_ssm_attach" {
  role       = aws_iam_role.ec2_ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

##########################
# IAM インスタンスプロファイル (EC2 → SSM Role 紐づけ)
##########################
resource "aws_iam_instance_profile" "ec2_ssm_profile" {
  name = "ec2-ssm-profile"
  role = aws_iam_role.ec2_ssm_role.name
}

##########################
# EC2インスタンスの作成(httpd起動)
##########################
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["137112412989"] # AmazonのAMI所有者ID

  filter {
    name = "name"
    # Amazon Linux 2023 AMIの名前パターン。minimumを除外する
    values = ["al2023-ami-2023*-kernel-*-x86_64"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_instance" "web" {
  count                  = 1
  ami                    = data.aws_ami.amazon_linux.id # ご利用のリージョンに合わせたAMI ID(例: Amazon Linux 2)
  instance_type          = "t3.micro"
  subnet_id              = element(var.private_subnet_ids, 0)
  iam_instance_profile   = aws_iam_instance_profile.ec2_ssm_profile.name
  vpc_security_group_ids = [aws_security_group.ec2_sg.id]

  # EC2起動時にhttpdインストールと起動を実施(必要に応じてCodeDeployエージェントのインストールも追加)
  user_data = <<EOF
#!/bin/bash
# OS のアップデートと必須パッケージのインストール
dnf update -y
dnf install -y httpd wget ruby

# CodeDeploy エージェントのインストール
cd /home/ec2-user
wget https://aws-codedeploy-ap-northeast-1.s3.amazonaws.com/latest/install -O install
chmod +x install
./install auto

# systemd 設定のリロードと CodeDeploy エージェントの有効化/起動
systemctl daemon-reload
systemctl enable codedeploy-agent
systemctl start codedeploy-agent

# httpd の有効化と起動
systemctl enable httpd
systemctl start httpd
EOF

  # CodeDeployで対象となるようにタグを設定(後述のデプロイグループのEC2フィルタと一致)
  tags = {
    Name = "MyAppServer"
  }
}

##########################
# ALBとEC2の紐づけ(ターゲットグループへの登録)
##########################
resource "aws_lb_target_group_attachment" "tg_attachment" {
  count            = length(aws_instance.web.*.id)
  target_group_arn = aws_lb_target_group.my_tg.arn
  target_id        = aws_instance.web[count.index].id
  port             = 80
}

output "alb_domain_name" {
  value = aws_lb.my_alb.dns_name
}

codepipeline.tf

  • GitHubのV1は非推奨になっているので、ちゃんと使うときはV2のoAuthを使うバージョンにするように
# S3バケット(アーティファクト用)の作成
resource "aws_s3_bucket" "codepipeline_artifacts" {
  bucket        = var.artifact_bucket_name
  force_destroy = true
}

resource "aws_s3_bucket_versioning" "codepipeline_versioning" {
  bucket = aws_s3_bucket.codepipeline_artifacts.id
  versioning_configuration {
    status = "Enabled"
  }
}

# CodePipeline用IAMロールの作成
resource "aws_iam_role" "codepipeline_role" {
  name = "CodePipelineRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = {
        Service = "codepipeline.amazonaws.com"
      },
      Action = "sts:AssumeRole"
    }]
  })
}

# 必要なIAMポリシー(S3やCodeDeployへのアクセス権限)をアタッチする
resource "aws_iam_role_policy" "codepipeline_policy" {
  name = "CodePipelinePolicy"
  role = aws_iam_role.codepipeline_role.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "s3:GetObject",
          "s3:GetObjectVersion",
          "s3:PutObject"
        ],
        Resource = [
          "${aws_s3_bucket.codepipeline_artifacts.arn}/*"
        ]
      }
    ]
  })
}

# AWSが提供しているマネージドポリシーAWSCodeDeployDeployerAccessをアタッチ
resource "aws_iam_role_policy_attachment" "code_deployer_managed_policy" {
  role       = aws_iam_role.codepipeline_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployDeployerAccess"
}

# CodePipelineの定義
resource "aws_codepipeline" "my_pipeline" {
  name     = "MyAppPipeline"
  role_arn = aws_iam_role.codepipeline_role.arn

  artifact_store {
    location = aws_s3_bucket.codepipeline_artifacts.bucket
    type     = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "ThirdParty"
      provider         = "GitHub"
      version          = "1"
      output_artifacts = ["SourceArtifact"]

      configuration = {
        Owner      = var.github_owner      # GitHubのオーナー名(ユーザー名または組織名)
        Repo       = var.github_repository # リポジトリ名
        Branch     = var.github_branch     # 利用するブランチ名
        OAuthToken = var.github_pat        # PATで認証
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeploy"
      input_artifacts = ["SourceArtifact"]
      version         = "1"

      configuration = {
        ApplicationName     = aws_codedeploy_app.my_app.name
        DeploymentGroupName = aws_codedeploy_deployment_group.my_deployment_group.deployment_group_name
      }
    }
  }
}

##########################
# CodeDeploy用IAMロール(最小限のAssumeRoleポリシー例)
##########################
resource "aws_iam_role" "codedeploy_role" {
  name = "CodeDeployDemoRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = {
        Service = "codedeploy.amazonaws.com"
      },
      Action = "sts:AssumeRole"
    }]
  })
}

# AWSが提供しているマネージドポリシーAWSCodeDeployRoleをアタッチ
resource "aws_iam_role_policy_attachment" "code_deploy_managed_policy" {
  role       = aws_iam_role.codedeploy_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"
}

##########################
# CodeDeployアプリケーション
##########################
resource "aws_codedeploy_app" "my_app" {
  name             = "MyApp"
  compute_platform = "Server" # EC2/オンプレミスの場合は "Server"
}

##########################
# CodeDeployデプロイグループ
##########################
resource "aws_codedeploy_deployment_group" "my_deployment_group" {
  app_name               = aws_codedeploy_app.my_app.name
  deployment_group_name  = "MyDeploymentGroup"
  service_role_arn       = aws_iam_role.codedeploy_role.arn
  deployment_config_name = "CodeDeployDefault.OneAtATime"

  # EC2インスタンスの選定はタグ(Name=MyAppServer)で行う
  ec2_tag_set {
    ec2_tag_filter {
      key   = "Name"
      type  = "KEY_AND_VALUE"
      value = "MyAppServer"
    }
  }

  # (オプション)ALBとの統合設定も可能(ここではターゲットグループARNを指定)
  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.my_listener.arn]
      }

      target_group {
        name = aws_lb_target_group.my_tg.name
      }
    }
  }

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }
}

所感

  • 最初慣れるの大変だけどまあ便利
  • 素のEC2を使うことそんなにないから、ECSとかLambdaとかで使えたら結構有用そう


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です