CodeBuildでRustをコンパイルしてメール経由で配布してみた

2023.06.21

初めに

我が家ではメインの利用はWindows機となるのですが、そのマシンには開発環境のようなものはできる限り導入せず開発はMac環境で行っています。

一部Rustで開発しているものでWindows機でも利用するアプリがあるのですが、上記の関係でMac上でクロスコンパイルを行いGoogleドライブに手動でアップロードした上でダウンロードという形で行っていました。

数ヶ月に1回程のためSambaの受け口を開けたり、エージェントやスケジューラーを組み込んだりしたくはないのですが、とはいえもう少し楽したいと思い立った結果メールで配布することにしました(メーラーは常につけっぱなしの為)

構成

指定したリポジトリのmasterブランチにpushを行うとCodeBuild側でビルドを開始し、その結果をSNSを利用してメール経由で配信するようにしています。

Github Actionsでも良かったのですが、どちらにしろ一定量AWS側の設定を行う関係でAWS側にまとめてしまうのが楽そうなためCodeBuildを採用しました。

今回ビルド成果物がシングルバイナリのためLambdaの呼び出しはS3イベント通知を利用していますが(バケット名とキーがeventから取れるので楽)、複数配置が発生するようなケースではファイルごとにイベントが発生してしまうのでCodeBuildの完了をイベントにLambdaを起動する必要が出てきます。

またバイナリはサイズによっては受信側のメールサイズの上限を超えてしまい受信で気なくなるリスクがあるため、直接添付するのではなくS3のリンクを送りダウンロードする形式を採用しました。

設定

手動設定(Githubの認証)

CodeBuildとGithubとの連携のための認証は今回OAuthを利用していますが、この認証の場合は事前にマネジメントコンソール上から認証操作を行う必要があります。

プロジェクトの作成画面の以下の箇所より認証を行います。

一度認証を行ってしまえばそのプロジェクト自体の作成を完了せずとも認証状態は残るようなので、認証を実行したプロジェクトは作成を完了せずキャンセルしても問題ありません。

CloudFormation

今回実行を行う環境はWindowsのためWindows環境のビルドを利用しても良かったのですが、Windows環境の提供はリージョンやインスタンスタイプが限定的になるのでLinux環境でクロスコンパイルを実行する形を選択しました。

少し長いのでテンプレートは以下にたたみ込んでおきます。

テンプレート
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  AppName:
    Type: String
  RepoUrl:
    Type: String
  ArtifactNotificationAddress:
    Type: String
Resources:
  #----------------------
  #--- CodeBuild
  #----------------------
  BuildProject:
    Type: AWS::CodeBuild::Project
    Properties: 
      Name: !Sub ${AppName}-builder
      Artifacts: 
        Type: S3
        Location: !Ref ArtifactBucket
        OverrideArtifactName: True
      Cache: 
        Type: NO_CACHE
      Environment: 
       Type: LINUX_CONTAINER
       ComputeType: BUILD_GENERAL1_SMALL
       Image: aws/codebuild/standard:7.0
      LogsConfig: 
        CloudWatchLogs:
          Status: ENABLED
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source: 
        Auth:
          Type: OAUTH
        Type: GITHUB
        Location: !Ref RepoUrl
        BuildSpec: |
          (後述)
      Triggers: 
        BuildType: BUILD
        Webhook: True
        FilterGroups:
          - 
            - Type: EVENT
              Pattern: PUSH
            - Type: HEAD_REF
              Pattern: refs/heads/master
      Visibility: PRIVATE
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${AppName}-codebuild-role
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
      Policies:
        - PolicyName: artifact-delivery-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 's3:PutObject'
                Resource: !Sub ${ArtifactBucket.Arn}/*
        - PolicyName: log-create-and-writer-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${AppName}-builder
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${AppName}-builder:*
  #----------------------
  #--- S3
  #----------------------
  ArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${AppName}-builded-object-${AWS::AccountId}-bucket
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: True
            ServerSideEncryptionByDefault: 
              SSEAlgorithm: AES256
      NotificationConfiguration:
       LambdaConfigurations:
         - Event: s3:ObjectCreated:Put
           Function: !GetAtt NotificationFunction.Arn
           Filter:
            S3Key: 
              Rules:
                - Name: prefix
                  Value: x86_64-pc-windows-gnu/
    DependsOn:
      - NotificationFunctionPermission
  #----------------------
  #--- Lambda
  #----------------------
  NotificationFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: "-"
      FunctionName: !Sub ${AppName}-artifact-notification-func
      Handler: "index.lambda_handler"
      Role: !GetAtt NotificationFunctionRole.Arn
      Runtime: "python3.10"
      Timeout: "60"
      Code:
        ZipFile: |
          (後述)
      Environment:
        Variables:
          TOPIC_ARN: !Ref ArtifactNotificationTopic
  NotificationFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      RoleName: !Sub ${AppName}-artifact-notification-func-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - sts:AssumeRole
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: sns-publisher-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'sns:publish'
                Resource: !Ref ArtifactNotificationTopic
      ManagedPolicyArns:
        - !Sub "arn:aws:iam::aws:policy/AWSLambdaExecute"
  NotificationFunctionPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt NotificationFunction.Arn
      Principal: s3.amazonaws.com
      SourceArn: !Sub arn:aws:s3:::${AppName}-builded-object-${AWS::AccountId}-bucket
  #----------------------
  #--- SNS
  #----------------------
  ArtifactNotificationTopic:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: !Sub ${AppName}-artifact-notification
      DisplayName: !Sub ${AppName}-artifact-notification
  ArtifactNotificationEmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref ArtifactNotificationTopic
      Protocol: email
      Endpoint: !Ref ArtifactNotificationAddress

buildspec

一部言語の環境はマネージドランタイムが存在するためその指定をすることで環境構築の手間が省けるようですが、残念ながら執筆時点ではRustのマネージドランタイムは提供されていないためビルド環境は自分で整える必要があります。

buildspecはアプリ側の管轄な気もしますが、buildspec慣れておらず調整を繰り返すことが多かったので作業上楽であったCloudFormation側に直接設置しています。

version: 0.2
env:
  shell: bash
  variables:
    BUILD_TARGET: x86_64-pc-windows-gnu

phases:
  install:
    commands:
      - apt update && apt install -y mingw-w64 curl
      - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
      - source $HOME/.cargo/env
      - rustup target add $BUILD_TARGET
  pre_build:
    comands:
      - cargo test --target $BUILD_TARGET
  build:
    commands:
      - cargo build --release --target $BUILD_TARGET
artifacts:
  files:
    - target/${BUILD_TARGET}/release/*.exe
  name: ${BUILD_TARGET}
  discard-paths: yes

本来はAmazon Linux 2環境でビルドを走らせる予定だったのですが、CodeBuildで提供されているAmazon Linux 2イメージの環境ではamazon-linux-extrasがnot foundとなり解決も手間だったのでUbuntuイメージを利用しています。

Amazon Linux 2環境でRustをWindows向けにクロスコンパイルする場合はepelをなんとかして入れてmingw64-gccmingw64-winpthreads-staticをインストールする形になるはずです。

※イメージ

commands:
  - amazon-linux-extras install -y epel
  - yum install -y gcc mingw64-{gcc, winpthreads-static}
  - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
  - source $HOME/.cargo/env
  - rustup target add $BUILD_TARGET

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-caching.html#caching-local
ローカルキャッシュは、そのビルドホストのみが利用できるキャッシュをそのビルドホストにローカルに保存します。キャッシュはビルドホストですぐに利用できるため、この方法は大規模から中間ビルドアーティファクトに適しています。ビルドの頻度が低い場合、これは最適なオプションではありません。

今回は実行頻度がごく低いのでキャッシュの利用やECRへのinstallセクションを済ませたイメージの保持は行っていません。

Lambdaコード

機密性も高くなく自宅のIPは固定なのでバケットポリシー側でaws:SourceIpで制御をかけて通知だけでも良かったのですが、なんとなく永続的にアクセス可能なリンクを作りたくなかったので署名つきURLを発行した上でメール本文に載せることにしました。

import boto3
from botocore.client import Config
import os

s3 = boto3.client('s3', config=Config(signature_version='s3v4'))
sns = boto3.client('sns')

def lambda_handler(event, context):
    key = event['Records'][0]['s3']['object']['key']
    bucket = event['Records'][0]['s3']['bucket']['name']

    presigned_url = s3.generate_presigned_url(
      ClientMethod = 'get_object',
      Params = {
        'Bucket': bucket,
        'Key': key
      }
    )

    sns.publish(
        TopicArn=os.environ['TOPIC_ARN'],
        Subject="[Completed] Build {}".format(key),
        Message=presigned_url
    )

なおIAMロールを利用して発行する場合署名付きURLのリンクはロールの有効期限が切れるまで(設定次第で最大12時間)となるため、関係者等に配る関係で数日間リンクを維持させたいような場合はIAMユーザを作成しその認証情報を利用して署名する必要が出てきます(最大7日間)

今回は一息入れている間にダウンロードできるようになっていれば良い程度で考えているのでLambdaに付与されているIAMロールの認証情報で署名しています(デフォルト設定のため最大1時間)。

実行

以下の実行は実際のものではなくcargo initで作成したサンプルプロジェクトを対象に行なっています。

masterブランチにpushすることで

#無駄にcommitを増やしたくなかったのでresetとforce pushを繰り返していました
% git commit Cargo.toml -m "push test" && git push -f
[master 4a43a51] push test
 1 file changed, 2 insertions(+), 2 deletions(-)
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 376 bytes | 376.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:xxx
 + 61493a8...4a43a51 master -> master (forced update)
% date
2023年 6月21日 水曜日 14時49分10秒 JST

ビルドが実行され

終了のタイミングでメールで署名付きURLが飛んでくるのでリンクを踏んで保存します。

自分一人で管理して配布先も自分なのでうっかり署名付きURL有効期限らしてしまった場合、force pushをかけて無理やりビルドを走らせるか素直にS3まで取りに行きます。

終わりに

今回は設置したい側へのエージェントの導入等に制限があり頻度も低いのであまり自動化してもメリットは大きくないかな、と思っていましたが途中まで行うだけでも非常に楽になりました。

個人的には自動化するなら全部まとめてやりたいと思う気持ちもあり、なかなか趣味環境の部分自動化に手が伸びなかった部分がありましたが、実際部分的な部分でもやってみると気持ち的にものすごい楽になるのでもし似たような感じで手が伸びていない方がいたら一度試しに手をつけてみてはいかがでしょうか。

補足

記事を書き終わった後にamazonlinux2-x86_64-standard:5.0の環境にSession Managerで接続したところ/etc/os-releaseがAmazon Linux 2023相当のものととなっていました。

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-available.html
Amazon Linux 2 aws/codebuild/amazonlinux2-x86_64-standard:4.0 al2/standard/4.0
Amazon Linux 2023 aws/codebuild/amazonlinux2-x86_64-standard:5.0 al2/standard/5.0

どうやら5.0はAmazon Linux 2のカテゴリに入っているもののAmazon Linux 2023ベースのイメージになるようです。