Home

Published

- 10 min read

AWS CodePipelineを使ってAstroの静的サイトをS3にデプロイする

img of AWS CodePipelineを使ってAstroの静的サイトをS3にデプロイする

はじめに

今回はAWS CodePipelineを使って、前回Astroで作成した静的サイト(ブログ)をデプロイしていきます。
パイプラインの構成イメージは以下の通りです。
また、前回すでにS3は作成済みのため、本記事ではS3の作成部分は割愛します。

CodepiPeline構成図

CodeCommmitの準備

今回はブログのコード、コンテンツをすべてCodeCommitに格納しました。
GitHub等CodePipelineのソースにできるものであれば何でもいいです。
CodeCommitはマネジメントコンソールで作成しました。

CodePipelineの設定

CodePipelineは以下のステージを作成します。

  1. Source: CodeCommitをソースプロバイダーとしてソースコードを取得する
  2. Build: CodeBuildをアクションプロバイダーとしてAstroブログをデプロイする
  3. Deploy: S3をアクションプロバイダーとしてビルドアーティファクトをプットする

CodeCommitからコードを取得、CodeBuildでAstroをビルド、最後にS3にコンテンツをアップロードする流れです。

CodeBuildの設定

CodeBuildにおけるLambdaの使用について

CodeBuildのコンピューティングにLambdaが使用できるようになったため、今回はそちらを使用していきます。
(参考)AWS CodeBuildでのAWS Lambdaコンピューティングの使用

今回のユースケースではnpmを使用して必要なパッケージのインストールとビルド(astro build)を行います。
そのため、ランタイムはNode.jsコンテナを使用します。
もちろん従来のEC2タイプでも実装可能ですがLambdaの方が圧倒的に早いので、ユースケースにマッチする場合はLambdaの使用がおすすめです。

今回に似たようなユースケースとして、公式ドキュメントではReactアプリのビルドサンプルが紹介されています。
(参考)CodeBuild Lambda Node.js で単一ページの React アプリを作成する

buildspec

buildspecファイル(CodeBuildで実行するコマンドを記述したファイル)は以下の通りです。

   version: 0.2
phases:
  pre_build:
    commands:
      - node -v
      - npm -v
  build:
    commands:
      - npm install -g npm@latest
      - npm install
      - npm run build
artifacts:
  name: "blog_content"
  base-directory: "dist"
  files:
    - "**/*"
  exclude-paths:
    - "**/admin/*"
    - "**/sitemap*.xml"
    - "**/robots.txt"
    - "**/openblog-lighthouse-score.svg"
  • pre_buildフェーズではnodeやnpmのバージョン確認をしていますが、必須ではありません。
  • buildフェーズでは、npm自身のアップデート後、npm installnpm run build(astro build)をしています。
  • ビルドアーティファクトには、ビルドしたコンテンツが出力されているdistディレクトリを指定します。
    しかし、中身には不要なファイルが含まれているため、exclude-pathsにパスの指定を行います。

buildspecのリファレンスについては以下の公式ドキュメントを参照ください。
(参考)CodeBuildのビルド仕様リファレンス

作成したファイルをCodeCommmitに格納します。
今回のケースでは、cicdというフォルダを作成し、そこに配置しています。
※CodeBuildの設定でファイルパスを指定します。

Terraformコード

上記の設定を踏まえたTerraformコードは以下の通りです。
使用する場合は各自の環境に応じて書き換えてください。
また、今回CodeCommitはマネジメントコンソールで作成し、Terraformでは管理しません。

   # CodePipeline Settings

resource "aws_codepipeline" "blog" {
  name     = "astro-blog-pipeline"
  role_arn = aws_iam_role.blog["codepipeline"].arn
  artifact_store {
    location = aws_s3_bucket.codepipeline_artifact.bucket
    type     = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeCommit"
      version          = "1"
      output_artifacts = ["SourceArtifact"]

      configuration = {
        RepositoryName   = var.application_repository_name # Codecommitのリポジトリ名
        BranchName       = "release" # リリース対象のブランチ名
        PollForSourceChanges = false
        OutputArtifactFormat = "CODE_ZIP"
      }
    }
  }

  stage {
    name = "Build"

    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["SourceArtifact"]
      output_artifacts = ["BuildArtifact"]
      version          = "1"

      configuration = {
        ProjectName = aws_codebuild_project.blog.name
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "S3"
      input_artifacts = ["BuildArtifact"]
      version         = "1"

      configuration = {
        BucketName  = "YOUR_BLOG_S3_BUCKET_NAME" # デプロイするS3バケット名
        Extract     = true # ビルドアーティファクトを展開してputする
      }
    }
  }
}

# CodeBuild Settings

resource "aws_codebuild_project" "blog" {
  name           = "astro-blog-codebuild"
  description    = "run astro build with lambda"
  build_timeout  = 15 # Lambdaなのでタイムアウトは15分まで
  service_role = aws_iam_role.blog["codebuild"].arn

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    # メモリの設定(1GBでも動くが遅い)
    compute_type                = "BUILD_LAMBDA_2GB"
    # Node.jsのLambdaコンテナイメージを使用する
    image                       = "aws/codebuild/amazonlinux-x86_64-lambda-standard:nodejs20"
    type                        = "LINUX_LAMBDA_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = false
  }

  source {
    type            = "CODEPIPELINE"
    buildspec       = "cicd/buildspec.yml" # CodeCOmmitのルートから見たbuildspecファイルのパス
  }
}


# S3 Settings (Artifact Store)

resource "aws_s3_bucket" "codepipeline_artifact" {
  # CodePipelineのアーティファクトストアのS3バケット
  bucket = "YOUR_ARTIFACT_S3_BUCKET_NAME"
}

resource "aws_s3_bucket_public_access_block" "codepipeline_artifact" {
  bucket                  = aws_s3_bucket.codepipeline_artifact.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# IAM Roles

# for_eachで使用するmap(名前は良くなかったかも)
locals {
  service_name = {
    codepipeline = "codepipeline.amazonaws.com"
    codebuild   = "codebuild.amazonaws.com"
  }
}

## IAM Role

resource "aws_iam_role" "blog" {
  # for_eachを使用して、CodePipelineロールとCodeBuildロールをそれぞれ定義する
  for_each = tomap(local.service_name)
  name               = "astro-blog-role-${each.key}"
  # 信頼関係ポリシーに設定するAWSサービス名を注入する
  assume_role_policy = templatefile("${path.module}/template/assume_role_policy.json",
  {
    service_name = each.value
  })
}

## IAM Policy

resource "aws_iam_policy" "blog" {
  # for_eachを使用して、CodePipelineロールとCodeBuildロールをそれぞれ定義する
  for_each = tomap(local.service_name)
  name = "astro-blog-policy-${each.key}"
  # 各ポリシーに記載された変数に値を注入する
  policy = templatefile("${path.module}/template/${each.key}_role_policy.json",
    {
      application_repository_name = var.application_repository_name
      blog_content_s3_bucket_arn = aws_s3_bucket.blog.arn
      artifact_s3_bucket_arn = aws_s3_bucket.codepipeline_artifact.arn
      codebuild_arn = aws_codebuild_project.blog.arn
    }
  )
}


## IAM Role Policy Attachment

resource "aws_iam_role_policy_attachment" "blog" {
  # for_eachを使用して、CodePipelineロールとCodeBuildロールをそれぞれ定義する
  for_each = tomap(local.service_name)
  role = aws_iam_role.blog[each.key].name
  policy_arn = aws_iam_policy.blog[each.key].arn
}

信頼関係ポリシー

IAMロールの信頼関係ポリシーです。
このファイルはtemplateフォルダ内に配置します。

   {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "${service_name}"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

CodePipelineロールポリシー

CodePipelineのサービスロールに設定するポリシーです。
CodeCommmit、CodeBuild、S3に対する各操作の許可を記載します。

このファイルはtemplateフォルダ内に配置します。
YOUR_AWS_ACCOUNT_ID は書き換えてください。

   {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CodeCommit",
            "Effect": "Allow",
            "Action": [
                "codecommit:CancelUploadArchive",
                "codecommit:GetBranch",
                "codecommit:GetCommit",
                "codecommit:GetUploadArchiveStatus",
                "codecommit:UploadArchive"
            ],
            "Resource": "arn:aws:codecommit:ap-northeast-1:YOUR_AWS_ACCOUNT_ID:${application_repository_name}"
        },
        {
            "Sid": "S3",
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "${artifact_s3_bucket_arn}/*",
                "${blog_content_s3_bucket_arn}/*"
            ]
        },
        {
            "Sid": "CodeBuild",
            "Effect": "Allow",
            "Action": [
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild"
            ],
            "Resource": [
                "${codebuild_arn}"
            ]
        }
    ]
}

CodeBuildロールポリシー

CodeBuildのサービスロールに設定するポリシーです。
アーティファクトストアのS3に対する操作の許可を記載します。
その他ログやECRへの許可も記載します。
サービスロールの権限サンプルは公式ドキュメントを参照ください。
(参考)CodeBuild サービスロールの作成

このファイルはtemplateフォルダ内に配置します。
YOUR_AWS_ACCOUNT_ID は書き換えてください。

   {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudWatchLogsPolicy",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Sid": "S3Policy",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:PutObject",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation"
            ],
            "Resource": [
                "${artifact_s3_bucket_arn}",
                "${artifact_s3_bucket_arn}/*"
            ]
        },
        {
            "Sid": "ECRPullPolicy",
            "Effect": "Allow",
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ECRAuthPolicy",
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken"
            ],
            "Resource": "*"
        }
    ]
}

動作確認

作成したCodePipeineを実行し、S3に想定通りファイルがデプロイされていれば成功です。
また、実際にサイトにアクセスして、コンテンツが表示されることを確認しましょう。
デプロイ後のサイト表示確認

課題

ゴミファイルの残存

S3へのファイル配置の際に、既存ファイルは上書きされますが、一部ビルドの都度新規作成されるJSファイルがあるようです。
上書きされない古いファイルは放置するとゴミが永遠に残り続けてしまいます。
ライフサイクルルールの有効期限設定以外で削除をする方法を検討します。

CloudFrontキャッシュ

前回作成したCloudFrontはオリジンのキャッシュを有効にしていました。
デプロイしてもファイルの更新が正常に反映されないため、例えば以下のような対策を行う必要があります。

  • そもそもCloudFrontのキャッシュを無効にする
  • キャッシュの削除を手動(コンソール/CLI)で実行する
  • キャッシュの削除を行うLambda/StepFunctions等をパイプラインに組み込む

※キャッシュの削除はaws cloudfront create-invalidationコマンドで実行できます。
(参考)キャッシュされたファイルを CloudFront から削除する方法を教えてください。