TerraformとGitHubを活用したCodeDeploy導入
Posted On 2025-02-07
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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー