Snyk 提供の Docker イメージから AWS CodeBuild プロジェクトを動かすために必要なものを調べてみた

Snyk 提供の Docker イメージから AWS CodeBuild プロジェクトを動かすために必要なものを調べてみた

Clock Icon2022.07.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!AWS 事業本部コンサルティング部のたかくに(@takakuni_)です。

今回は、Snyk から提供されている Docker イメージを使用して、CodeBuild プロジェクトを起動するにはどうすればいいのかご紹介します。

はじめに

Snyk では Docker Hub で Docker イメージを提供しています。イメージタグで解析する言語を切り替える仕組みとなっています。

今回は Snyk IaC(Snyk CLI)で利用するため、snyk/snyk:linuxをベースイメージとして選択します。

https://hub.docker.com/r/snyk/snyk

よく似たイメージで、snyk/snyk:cliがありますが「Deprecation Notice」いわく、非推奨イメージで将来的に削除される可能性あるため、今回はsnyk/snyk:linuxを利用します。

https://hub.docker.com/r/snyk/snyk-cli

前提条件

CodeBuild(CI)でsnyk test系のコマンドを実行するには「認証トークン」または「サービスアカウント」が必要になります。

今回は Snyk で推奨されている「サービスアカウント」を利用します。

https://docs.snyk.io/features/user-and-group-management/structure-account-for-high-application-performance/service-accounts

このブログを書くにあたって

このブログを書くにあたった背景をご紹介します。

先日、Qiita にて「AWS CodeBuild で Snyk IaC を使用してみた」というブログを書かせていただきました。重ねてご覧いただけますと幸いです。(LGTM もいただけると嬉しいです。)

https://qiita.com/stakakey/items/1cbcb74cfcf6f1fdeeba

この記事の中で、CodeBuild で使用するイメージは、aws/codebuild/amazonlinux2-x86_64-standard:3.0を使用することにしました。

理由は、snyk/snyk:linuxは CA 証明書が含まれていない Docker イメージであるため https 通信ができませんでした。

私は Docker Hub への認証情報を Secrets Manager に保存していたため、Secrets Manager エンドポイントへ通信できずINSTALLフェーズで CodeBuild がこけてしまいました。

[Container] 2022/07/17 13:45:59 Waiting for agent ping
[Container] 2022/07/17 13:46:00 Waiting for DOWNLOAD_SOURCE
[Container] 2022/07/17 13:46:00 Phase is DOWNLOAD_SOURCE
[Container] 2022/07/17 13:46:00 CODEBUILD_SRC_DIR=/codebuild/output/src815573339/src
[Container] 2022/07/17 13:46:00 YAML location is /codebuild/readonly/buildspec.yml
[Container] 2022/07/17 13:46:00 Processing environment variables
[Container] 2022/07/17 13:46:02 Phase complete: DOWNLOAD_SOURCE State: FAILED
[Container] 2022/07/17 13:46:02 Phase context status code: Secrets Manager Error Message: RequestError: send request failed
caused by: Post "https://secretsmanager.ap-northeast-1.amazonaws.com/": x509: certificate signed by unknown authority

さて、ここまででsnyk/snyk:linuxから直接、CodeBuild で起動できないことがわかりました。

それでは、CodeBuild で起動するにはどのようにすればいいのかをご紹介します。

やってみた

構成図

今回の構成は次のとおりです。snyk/snyk:linuxに CA 証明書をインストールしたイメージを ECR にプッシュして利用します。

Terraform のパイプラインを想定していますが、今回は Terraform が主題では無いのでterraform applyコマンド用の CodeBuild は作成しません。

CloudFormation テンプレート

今回使用した CloudFormation のテンプレートファイルです。

Snyk IaC パイプラインの作成にお役に立てれば幸いです。

ファイル全体(クリックで表示できます)
snyk_iac_pipeline.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Terraform pipeline template.
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: "Project Name Prefix"
      Parameters:
        - PrjPrefix
    - Label:
        default: "Repository Configuration"
      Parameters:
        - BranchName
    - Label:
        default: "Snyk Organization Profile"
      Parameters:
      - SnykOrganizationId
      - SnykOrganizationApiKey
    - Label:
        default: "Logs Configuration"
      Parameters:
        - BuildLogsRationDay
    - Label:
        default: "terraform build configuration"
      Parameters:
        - TerraformVersion

Parameters:
  PrjPrefix:
    Type: String
    Description: "Enter resource prefix. (ex. terraform-dev)"
    Default: "terraform-dev"
  BranchName:
    Type: String
    Description: "Enter the name of the branch where you want to store .snyk file."
    Default: "main"
  SnykOrganizationId:
    Type: String
    Description: "Enter Snyk Organization ID."
    NoEcho: true
  SnykOrganizationApiKey:
    Type: String
    Description: "Enter Snyk Organization API Key."
    NoEcho: true
  BuildLogsRationDay:
    Type: Number
    Description: "Enter build log retention period."
    Default: 90
    AllowedValues:
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1827
      - 3653
  TerraformVersion:
    Type: String
    Description: "Enter the version of Terraform that use with CodeBuild."
    Default: "1.2.0"

Resources:
#################################
# KMS (CloudWatch Logs)
#################################
  KeyCWL:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline Build Logs Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
          - Sid: Allow use of the key from CWL
            Effect: "Allow"
            Principal:
              Service: !Sub "logs.${AWS::Region}.amazonaws.com"
            Action:
              - "kms:Encrypt"
              - "kms:Decrypt"
              - "kms:ReEncrypt"
              - "kms:GenerateDataKey"
              - "kms:Describe"
            Resource: "*"
            Condition:
              ArnLike:
                kms:EncryptionContext:aws:logs:arn: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-logs-key"

  KeyAliasCWL:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-logs-key"
      TargetKeyId: !Ref KeyCWL

#################################
# KMS (S3 arthifact)
#################################
  KeyS3Arthifact:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline arthifact Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-artifact-key"

  KeyAliasS3Arthifact:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-artifact-key"
      TargetKeyId: !Ref KeyS3Arthifact

#################################
# KMS (Secrets Manager)
#################################
  KeySecretsManager:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline Secrets Manager Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-secretsmanager-key"

  KeyAliasSecretsManager:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-secretsmanager-key"
      TargetKeyId: !Ref KeySecretsManager

#################################
# CodeCommit
#################################
  CodeCommit:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Sub "${PrjPrefix}-tf-repo"
      RepositoryDescription: !Sub "${PrjPrefix}-tf-repo"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-repo"

#################################
# Custom Resource
#################################
  PutExcludeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/AWSCodeCommitPowerUser"
      RoleName: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude-role"

  PutExcludeLog:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
      KmsKeyId: !GetAtt KeyCWL.Arn

  PutExcludeFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse

          def handler(event, context):
              try:
                  repository = event['ResourceProperties']['RepositoryName']
                  branch = event['ResourceProperties']['BranchName']
                  content = event['ResourceProperties']['FileContent'].encode()
                  path = event['ResourceProperties']['FilePath']

                  if event['RequestType'] == 'Create':
                      codecommit = boto3.client('codecommit')
                      response = codecommit.put_file(
                          repositoryName=repository,
                          branchName=branch,
                          fileContent=content,
                          filePath=path,
                          commitMessage='Initial Commit',
                          name='Your Lambda Helper'
                      )
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
                  if event['RequestType'] == 'Update':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
              except Exception as e:
                  print(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt PutExcludeRole.Arn
      Runtime: "python3.9"
      Timeout: 60
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
    DependsOn: PutExcludeLog

  PutExclude:
    Type: Custom::CodeCommitPutExclude
    Properties:
      ServiceToken: !GetAtt PutExcludeFunction.Arn
      RepositoryName: !GetAtt CodeCommit.Name
      BranchName: !Ref BranchName
      FileContent: "ignore:"
      FilePath: ".snyk"

#################################
# Secrets Manager
#################################
  SnykOrganizationProfile:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: "Snyk Web UI Organization profile for terraform pipelines"
      Name: !Sub "${PrjPrefix}/snyk_org"
      SecretString: !Sub '{"OrgId":"${SnykOrganizationId}","OrgApi":"${SnykOrganizationApiKey}"}'
      KmsKeyId: !GetAtt KeySecretsManager.Arn
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}/snyk_org"

#################################
# S3 Bucket (Artifact)
#################################
  BucketArtifacts:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub "${PrjPrefix}-tf-pipeline-artifacts"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "aws:kms"
              KMSMasterKeyID: !GetAtt KeyS3Arthifact.Arn
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-artifacts"

  BucketPolicyArtifacts:
      Type: AWS::S3::BucketPolicy
      Properties: 
        Bucket: !Ref BucketArtifacts
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Sid: "AllowSSLRequestsOnly"
              Effect: "Deny"
              Principal: "*"
              Action: "s3:*"
              Resource:
                - !GetAtt BucketArtifacts.Arn
                - !Sub
                  - "${BucketArn}/*"
                  - { BucketArn: !GetAtt BucketArtifacts.Arn }
              Condition:
                Bool:
                  aws:SecureTransport: "false"

#################################
# CloudWatch Logs (CodeBuild terraform plan)
#################################
  CWLTfplan:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-tfplan-project"
      RetentionInDays: !Ref BuildLogsRationDay
      KmsKeyId: !GetAtt KeyCWL.Arn
      Tags: 
        - Key: "Name"
          Value: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-tfplan-project"

#################################
# CodeBuild (terraform plan)
#################################
  PolicyTfplan:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-build-tfplan-project-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "BuildLogs"
            Effect: "Allow"
            Action:
              - "logs:CreateLogGroup"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
            Resource:
              - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CWLTfplan}"
              - !GetAtt CWLTfplan.Arn
          - Sid: "KmsKey"
            Effect: "Allow"
            Action:
              - "kms:Decrypt"
              - "kms:Encrypt"
              - "kms:ReEncrypt"
              - "kms:GenerateDataKey"
            Resource:
              - !GetAtt KeyS3Arthifact.Arn
          - Sid: "S3Artifact"
            Effect: "Allow"
            Action:
              - "s3:PutObject"
              - "s3:GetObject"
              - "s3:GetObjectVersion"
              - "s3:GetBucketAcl"
              - "s3:GetBucketLocation"
            Resource:
              - !GetAtt BucketArtifacts.Arn
              - !Sub
                - "${BucketArn}/*"
                - { BucketArn: !GetAtt BucketArtifacts.Arn }

  RoleTfplan:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-build-tfplan-project-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "codebuild.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicyTfplan
        - "arn:aws:iam::aws:policy/ReadOnlyAccess"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfplan-project-role"

  ProjectTfplan:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${PrjPrefix}-tf-build-tfplan-project"
      Description: "Execute terraform plan command."
      Source:
        Type: "CODEPIPELINE"
        BuildSpec: |
          version: 0.2
          env:
            exported-variables:
              - BUILD_URL
          phases:
            pre_build:
              commands:
                - "terraform init -input=false -no-color"
            build:
              commands:
                - "terraform plan -input=false -no-color -out=tfplan.binary"
            post_build:
              commands:
                - "terraform show -no-color -json tfplan.binary > tfplan.json"
                - "export BUILD_URL=`echo $CODEBUILD_BUILD_URL`"
          artifacts:
            files:
              - "tfplan.binary"
              - "tfplan.json"
      Artifacts:
        Type: "CODEPIPELINE"
      Cache:
        Type: "LOCAL"
        Modes:
          - "LOCAL_DOCKER_LAYER_CACHE"
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: !Sub "public.ecr.aws/hashicorp/terraform:${TerraformVersion}"
        ImagePullCredentialsType: "CODEBUILD"
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CWLTfplan
      EncryptionKey: !GetAtt KeyS3Arthifact.Arn
      ResourceAccessRole: !GetAtt RoleTfplan.Arn
      ServiceRole: !GetAtt RoleTfplan.Arn
      TimeoutInMinutes: 60
      Visibility: "PRIVATE"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfplan-project"

#################################
# CloudWatch Logs (CodeBuild Snyk IaC)
#################################
  CWLSnykIaC:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-snyk-iac-project"
      RetentionInDays: !Ref BuildLogsRationDay
      KmsKeyId: !GetAtt KeyCWL.Arn
      Tags: 
        - Key: "Name"
          Value: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-snyk-iac-project"

#################################
# CodeBuild (Snyk IaC)
#################################
  PolicySnykIaC:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-build-snyk-iac-project-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "BuildLogs"
            Effect: "Allow"
            Action:
              - "logs:CreateLogGroup"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
            Resource:
              - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CWLSnykIaC}"
              - !GetAtt CWLSnykIaC.Arn
          - Sid: "BuildReports"
            Effect: "Allow"
            Action:
              - "codebuild:CreateReportGroup"
              - "codebuild:CreateReport"
              - "codebuild:UpdateReport"
              - "codebuild:BatchPutTestCases"
              - "codebuild:BatchPutCodeCoverages"
            Resource:
              - !Sub "arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${PrjPrefix}-tf-build-snyk-iac-project-reports"
          - Sid: "GitPull"
            Effect: "Allow"
            Action: "codecommit:GitPull"
            Resource: !GetAtt CodeCommit.Arn
          - Sid: "GetSecretValue"
            Effect: "Allow"
            Action: "secretsmanager:GetSecretValue"
            Resource:
              - !Ref SnykOrganizationProfile
          - Sid: "KmsKey"
            Effect: "Allow"
            Action:
              - "kms:Decrypt"
              - "kms:DescribeKey"
              - "kms:Encrypt"
              - "kms:ReEncrypt"
              - "kms:GenerateDataKey"
            Resource:
              - !GetAtt KeyS3Arthifact.Arn
              - !GetAtt KeySecretsManager.Arn
          - Sid: "S3Artifact"
            Effect: "Allow"
            Action:
              - "s3:PutObject"
              - "s3:GetObject"
              - "s3:GetObjectVersion"
              - "s3:GetBucketAcl"
              - "s3:GetBucketLocation"
            Resource:
              - !GetAtt BucketArtifacts.Arn
              - !Sub
                - "${BucketArn}/*"
                - { BucketArn: !GetAtt BucketArtifacts.Arn }

  RoleSnykIaC:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-build-snyk-iac-project-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "codebuild.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicySnykIaC
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-snyk-iac-project-role"

  SnykIaCImageRepo:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub "${PrjPrefix}-snyk-iac"
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement:
          - Sid: "CodeBuildProject"
            Effect: "Allow"
            Principal:
              Service: "codebuild.amazonaws.com"
            Action:
              - "ecr:GetDownloadUrlForLayer"
              - "ecr:BatchGetImage"
              - "ecr:BatchCheckLayerAvailability"
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
                aws:SourceArn: !Sub "arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:project/${PrjPrefix}-tf-build-snyk-iac-project"
      ImageScanningConfiguration:
        ScanOnPush: true
      ImageTagMutability: "MUTABLE"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-snyk-iac"

  ProjectSnykIaC:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${PrjPrefix}-tf-build-snyk-iac-project"
      Description: "Analyze the code for vulnerabilities using Snyk IaC."
      Source:
        Type: "CODEPIPELINE"
        BuildSpec: |
          version: 0.2
          env:
            exported-variables:
              - BUILD_URL
          phases:
            pre_build:
              commands:
                - "printenv"
                - "mkdir -p ${SRC_DIR}"
                - "cp -p tfplan.json ${SRC_DIR}/"
                - "cd ${SRC_DIR}"
                - "echo Executing Snyk IaC"
            build:
              commands:
                - "snyk iac test tfplan.json --report || true"
            post_build:
              commands:
                - "export BUILD_URL=`echo $CODEBUILD_BUILD_URL`"
      Artifacts:
        Type: "CODEPIPELINE"
      Cache:
        Type: "LOCAL"
        Modes:
          - "LOCAL_DOCKER_LAYER_CACHE"
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: !GetAtt SnykIaCImageRepo.RepositoryUri
        # ImagePullCredentialsType: "CODEBUILD"
        ImagePullCredentialsType: "SERVICE_ROLE"
        EnvironmentVariables:
          - Name: "SNYK_TOKEN"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgApi"
          - Name: "SNYK_CFG_ORG"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgId"
          - Name: "SRC_DIR"
            Type: "PLAINTEXT"
            Value: !Sub "${PrjPrefix}-tf-build-snyk-iac-project"
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CWLSnykIaC
      EncryptionKey: !GetAtt KeyS3Arthifact.Arn
      ResourceAccessRole: !GetAtt RoleSnykIaC.Arn
      ServiceRole: !GetAtt RoleSnykIaC.Arn
      TimeoutInMinutes: 60
      Visibility: "PRIVATE"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfsec-project"

#################################
# CodePipeline
#################################
  PolicyTfPipeline:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-pipeline-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "S3Artifact"
            Effect: "Allow"
            Action:
              - "s3:GetObject*"
              - "s3:GetBucket*"
              - "s3:List*"
              - "s3:DeleteObject*"
              - "s3:PutObject"
              - "s3:Abort*"
            Resource:
              - !GetAtt BucketArtifacts.Arn
              - !Sub
                - "${BucketArn}/*"
                - { BucketArn: !GetAtt BucketArtifacts.Arn }
          - Sid: "KmsKey"
            Effect: "Allow"
            Action:
              - "kms:Decrypt"
              - "kms:DescribeKey"
              - "kms:Encrypt"
              - "kms:ReEncrypt*"
              - "kms:GenerateDataKey*"
            Resource:
              - !GetAtt KeyS3Arthifact.Arn
          - Sid: "CodeCommitRepo"
            Effect: "Allow"
            Action:
              - "codecommit:GetBranch"
              - "codecommit:GetCommit"
              - "codecommit:UploadArchive"
              - "codecommit:GetUploadArchiveStatus"
              - "codecommit:CancelUploadArchive"
            Resource:
              - !GetAtt CodeCommit.Arn
          - Sid: "CodeBuildProjects"
            Effect: "Allow"
            Action:
              - "codebuild:BatchGetBuilds"
              - "codebuild:StartBuild"
              - "codebuild:StopBuild"
            Resource:
              - !GetAtt ProjectSnykIaC.Arn
              - !GetAtt ProjectTfplan.Arn

  RoleTfPipelne:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-pipeline-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "codepipeline.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicyTfPipeline
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-role"

  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub "${PrjPrefix}-tf-pipeline"
      ArtifactStore:
        EncryptionKey:
          Id: !GetAtt KeyS3Arthifact.Arn
          Type: "KMS"
        Location: !Ref BucketArtifacts
        Type: "S3"
      RoleArn: !GetAtt RoleTfPipelne.Arn
      Stages:
        - Name: "Source"
          Actions:
            - Name: "CodeCommit_Source"
              ActionTypeId:
                Category: "Source"
                Owner: "AWS"
                Provider: "CodeCommit"
                Version: "1"
              Configuration:
                RepositoryName: !GetAtt CodeCommit.Name
                BranchName: !Ref BranchName
                PollForSourceChanges: false
              OutputArtifacts:
                - Name: "Artifact_Source_CodeCommit"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
        - Name: "terraform_plan_Stage"
          Actions:
            - Name: "terraform_plan_command"
              Namespace: TFPLAN
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectTfplan
              InputArtifacts:
                - Name: "Artifact_Source_CodeCommit"
              OutputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
            - Name: "terraform_plan_command_Manual_Review"
              ActionTypeId:
                Category: "Approval"
                Owner: "AWS"
                Provider: "Manual"
                Version: "1"
              Configuration:
                CustomData: "terraform plan review"
                ExternalEntityLink: "#{TFPLAN.BUILD_URL}"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 2
        - Name: "snyk_iac_Stage"
          Actions:
            - Name: "Terraform_Security_Analysis"
              Namespace: SNYKIAC
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectSnykIaC
              InputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
            - Name: "Terraform_Security_Analysis_Manual_Review"
              ActionTypeId:
                Category: "Approval"
                Owner: "AWS"
                Provider: "Manual"
                Version: "1"
              Configuration:
                CustomData: "snyk iac review"
                ExternalEntityLink: "#{SNYKIAC.BUILD_URL}"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 2

#################################
# EventBridge (CodeCommit State Change)
#################################
  PolicyEventBridgeCodeCommit:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-pipeline-event-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "CodePipelineExec"
            Effect: "Allow"
            Action: "codepipeline:StartPipelineExecution"
            Resource: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}"

  RoleEventBridgeCodeCommit:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-pipeline-event-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "events.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicyEventBridgeCodeCommit
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-event-role"

  EventsTfPipelineCodeCommit:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub "${PrjPrefix}-tf-pipeline-event"
      EventPattern:
        source:
          - "aws.codecommit"
        resources:
          - !GetAtt CodeCommit.Arn
        detail-type:
          - "CodeCommit Repository State Change"
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - "branch"
          referenceName:
            - !Ref BranchName
      State: "ENABLED"
      Targets:
        - Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}"
          Id: "TerraformPipeline"
          RoleArn: !GetAtt RoleEventBridgeCodeCommit.Arn

</details>

パラメータは以下を指定しました。

設定値 備考
BranchName main
BuildLogsRationDay 90
SnykOrganizationApiKey Service Account の API キー
SnykOrganizationId Snyk Organization の ID
PrjPrefix takakuni 任意の値
TerraformVersion 1.2.0 任意の値

Terraform コード

今回は次の Terraform のコードをsnyk iac testコマンドで解析します。

main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "4.23.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

data "aws_region" "current" {}
data "aws_caller_identity" "self" {}

resource "aws_ebs_volume" "example" {
  availability_zone = "${data.aws_region.current.name}a"
  size              = 40

  tags = {
    Name = "HelloWorld"
  }
}

EBS が暗号化されていない脆弱性が検出されるコードになります。

https://snyk.io/security-rules/SNYK-CC-TF-3

ca-certificates のインストール

Dockerfile のRUNを使用してエラーの原因である、CA 証明書のインストールを行います。

FROM snyk/snyk:linux
RUN apt update && apt install -y ca-certificates

イメージのビルド、ECR へプッシュします。

docker build -t 123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/takakuni-snyk-iac:latest .
docker image push 123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/takakuni-snyk-iac:latest

環境変数

サービスアカウントを使用して Snyk CLI を利用するには、CodeBuild の環境変数SNYK_TOKENに、API キーを設定する必要があります。

今回は、Secrets Manager 経由で CodeBuild から参照できるように設定しています。

また、組織 ID も環境変数SNYK_CFG_ORGで設定可能なため、Secrets Manager 経由で設定しています。

snyk_iac_pipeline.yaml(抜粋)
  ProjectSnykIaC:
    Type: AWS::CodeBuild::Project
    Properties:
    # 抜粋
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: !GetAtt SnykIaCImageRepo.RepositoryUri
        # ImagePullCredentialsType: "CODEBUILD"
        ImagePullCredentialsType: "SERVICE_ROLE"
        EnvironmentVariables:
          - Name: "SNYK_TOKEN"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgApi"
          - Name: "SNYK_CFG_ORG"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgId"

https://docs.snyk.io/snyk-cli/configure-the-snyk-cli

実行ディレクトリ

snyk iac testコマンドを--reportオプション付きで実行すると、Snyk Web UI に直接レポートを出力します。

https://dev.classmethod.jp/articles/snyk-iac-results-with-the-snyk-web-ui-report-option/

--report」オプションでは、実行しているディレクトリ名をプロジェクト名としてレポートを出力します。

デフォルトでは、CodeBuild はsrc配下でビルドを開始するため、レポート出力を行うディレクトリを分けないと、CodeBuild で実行したプロジェクト名は全てsrcになってしまいます。

複数の CodeBuild から同じ Snyk の組織に対して、レポートが出力されても問題ないように、新しいディレクトリを作成してsnyk iac testコマンドを実行します。

snyk_iac_pipeline.yaml(抜粋)
  ProjectSnykIaC:
    Type: AWS::CodeBuild::Project
    Properties:
    # 抜粋
      Source:
        Type: "CODEPIPELINE"
        BuildSpec: |
          version: 0.2
          env:
            exported-variables:
              - BUILD_URL
          phases:
            pre_build:
              commands:
                - "printenv"
                - "mkdir -p ${SRC_DIR}"
                - "cp -p tfplan.json ${SRC_DIR}/"
                - "cd ${SRC_DIR}"
                - "echo Executing Snyk IaC"
            build:
              commands:
                - "snyk iac test tfplan.json --report || true"
            post_build:
              commands:
                - "export BUILD_URL=`echo $CODEBUILD_BUILD_URL`"
    # 抜粋
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: !GetAtt SnykIaCImageRepo.RepositoryUri
        # ImagePullCredentialsType: "CODEBUILD"
        ImagePullCredentialsType: "SERVICE_ROLE"
        EnvironmentVariables:
          - Name: "SRC_DIR"
            Type: "PLAINTEXT"
            Value: !Sub "${PrjPrefix}-tf-build-snyk-iac-project"

出力結果

CodeBuild ログ、Snyk Web UI ともに結果が出力されていることが確認できました。

CodeBuild ログ

Snyk Web UI

(番外編)COPY コマンドを使用して Docker イメージを作成する

今回、snyk/snyk:linuxをベースイメージとしてRUNを使用して CA 証明書をインストールする方法をご紹介しましたが、COPYコマンドを使用して別のベースイメージで作成する方法もあります。

Dockerfile
FROM ubuntu
COPY --from=snyk/snyk:linux /usr/local/bin/snyk /usr/local/bin/snyk

https://hub.docker.com/r/snyk/snyk

まとめ

以上、「Snyk 提供の Docker イメージから AWS CodeBuild プロジェクトを動かすために必要なものを調べてみた」でした。

Amazon ECR Public Galleryにはまだ登録されていないですが、今後に期待ですね!

以上、AWS 事業本部コンサルティング部のたかくに(@takakuni_)でした!

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.